今天我们来详细讲讲时间复杂度和空间复杂度,途中如果有不懂的地方可翻阅我之前文章。
个人主页:小八哥向前冲~-CSDN博客
数据结构专栏:数据结构【c语言版】_小八哥向前冲~的博客-CSDN博客
c语言专栏:c语言_小八哥向前冲~的博客-CSDN博客
在平常写代码时,碰见一个问题,有时候第一种思路代码量相对较多,而第二种思路代码量相对较小。这时候你可能会选择代码量较小的思路进行解答。但真的代码量越少,代码运行效率更快嘛?
那可不一定,有时候代码量大的反而更好,算法效率并不取决于代码量的,衡量一个算法或一个思路好不好,主要看的是时间复杂度和空间复杂度的。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。
我们率先讲讲时间复杂度。
时间复杂度概念
我们先来看看它的定义:
在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一 个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知 道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个 分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
也就是说:算法中基本操作的执行次数就是时间复杂度。
我们来看个例子:
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N ; ++ i)
{
for (int j = 0; j < N ; ++ j)
{
++count;
}
}
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
要计算它的时间复杂度,我们不妨计算count执行了多少次,显然执行了N*N+2*N+10次,到这里你不会就以为它的空间复杂度就是N*N+2*N+10了吧。那可就大错特错了!
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这 里我们使用大O的渐进表示法。
大o的渐进表示法
这里我们不妨看图:
不难看出F(N)的值主要取决于N*N,那么我们说这个算法的时间就是o(N*N),也就是说我们只取对这个算法影响最深的一项。
也就是说大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
但是有时候我们的算法会出现最好情况,最坏情况。什么意思呢?我们看个例子(二分查找):
// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n-1;
// [begin, end]:begin和end是左闭右闭区间,因此有=号
while (begin <= end)
{
int mid = begin + ((end-begin)>>1);
if (a[mid] < x)
begin = mid+1;
else if (a[mid] > x)
end = mid-1;
else
return mid;
}
return -1;
}
这个例子的最坏情况:一次就找到。最坏情况:log2N次。为什么呢?我们分析一下:查找一次对半砍,两次对半砍两次,一直到砍到只剩最后一个值了或者最后一个值都不是要找的数,这就是最坏情况。
我们计算一下:
注意:如果有多种情况的话,我们一般算最坏情况。
我们来看看其他例子。
计算时间复杂度例题
冒泡排序例子
// 计算BubbleSort的时间复杂度?
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end)
{
int exchange = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i-1] > a[i])
{
Swap(&a[i-1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
我们来计算一下:N个数排序,第一次N-1趟冒泡,第二次N-2次冒泡,依次递减,其中每趟冒泡两两比较,我们不难算出N-1+N-2+N-3+N-4+........+1,也就是等差数列,我们算出为N*(N-1)/2,到这里是不是豁然开朗?最终为o(N*N)。
常数例子
我们再来看一下另一个常数例子:
// 计算Func4的时间复杂度?
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}
我们不难看出执行了100次(非常准确),但不会你以为时间复杂度为o(100)吧?其实不然,确实执行行了100次,但因为是常数次,我们一律规定为o(1)。
注意:这里的o(1)并不是只执行了1次,而是代表执行了常数次!
递归例子
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
if(0 == N)
return 1;
return Fac(N-1)*N;
}
刚刚接触递归例子是不是有点摸不着头脑?别急!
我们知道当N为0时就停止,当N>0时就一直调用这个函数,调用多少次呢?调用到0就停止!不难算出调用了N次,时间复杂度也就是o(N)!
我们可以上图理解一下:
我们来看一个另一个典型例子----斐波拉及数列!
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
我们不难看出:
F(N)调用F(N-1)和F(N-2),F(N-1)调用F(N-2)和F(N-3)........直到调用到N<3就停止。
既然这样仍然看不出执行次数,我们上图看看:
这样看是不是更加清楚呢?但是仍然存在一些瑕疵和疏漏:有些项并没有那么多项。
我们再上图:
值得注意的是不影响算时间复杂度!
显然这个递归算法不实用!因为次数一旦大一点,计算机就算不出来(要耗费一点时间,不信可以自行试试!)
空间复杂度概念
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。 空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。 空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
一般理解了时间复杂度,空间的复杂度就手到擒来了!
我们来看一些例题感受一下!
计算空间复杂度例题
常数例子
一个最常见的常数例子就是冒泡排序了,我们来看看:
// 计算BubbleSort的空间复杂度?
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end)
{
int exchange = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i-1] > a[i])
{
Swap(&a[i-1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
不难看出:这个排序开辟了常数个额外空间,也就是o(1)。
常见例子
long long* Fibonacci(size_t n)
{
if(n==0)
return NULL;
}
long long * fibArray = (long long *)malloc((n+1) * sizeof(long long));
fibArray[0] = 0;
fibArray[1] = 1;
for (int i = 2; i <= n ; ++i)
{
fibArray[i] = fibArray[i - 1] + fibArray [i - 2];
}
return fibArray;
不难看出:开辟了n+1个额外空间,也就是o(N)。
递归例子
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
if(N == 0)
return 1;
return Fac(N-1)*N;
}
不难看出:递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。空间复杂度为O(N)。
常见复杂度对比
算法有很多种,只要存在就有它的道理----存在即合理!只要我们理解它的底层逻辑,那么什么都不在话下!
好了,我们下期见!