目录
我们在写代码时,尤其是在工程中开发程序时,如何来评判一个项目的效率高低?这需要引入时间和空间复杂度。
时间复杂度
时间复杂度用来计算执行操作的次数,比如通俗的讲一个for循环就是一个O(N)的复杂度,例如:
int func1(int n)
{
int count = 0;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
count++;
}
}
for (int i =0; i < 2 * n; i++)
{
count++;
}
int M = 20;
while (M--)
{
count++;
}
return count;
}
时间复杂度是一个函数,自变量是传入的参数,在这段代码中,时间复杂度F(n)=n*n+2*n+20。用大O的渐进表示法就是O(n^2)。
相较于n方,2*n+20在时间上的影响远比n方小,为了方便比较,采用大O的渐进表示法,只保留影响最大的项,即n方
需要注意的是:
- 最大项的常数系数要忽略。比如将上面那段代码双重循环再写一遍,时间复杂度F(N)=2*N*N+2*N+20,O(n^2),而不是O(2*n^2)。原因是当无限大时,系数可以忽略,也就是数据很多时,不会因为常数系数的影响使运行效率大幅减慢。
- 程序接受到的参数不论大小,只要全部执行一次,就是O(n)的复杂度,如果只执行了常数次,不论大小,就是O(1)的复杂度。例子如下:
时间复杂度是O(1),就算终止条件是一亿也是O(1)。int func2(int N) { for (int i = 0; i < 10000; i++) { N -= i; }return N; }
- 当有两个项时间复杂度相加时,不清楚两项的关系,取影响较大的一项,和取影响最大的一项原理相同:
int func3(int N,int M) { int count = 0; for (int i = 0; i < M; i++) { count++; } for (int i = 0; i < N; i++) { count++; } return count; }
是O(max(M,N))的复杂度;
时间复杂度实例
首先我们分析一下冒泡排序的时间复杂度
void BubbleSort(int* a, int n)
{
//Swap(int *a,int *b);
for (int i = 0; i < n - 1; i++)
{
for (int j = 0; j < n - i - 1; j++)
{
if (a[j] < a[j + 1])
{
Swap(&a[j], &a[j + 1]);
}
}
}
return;
}
我们再来看看这段快排代码:
int PartSort(int *a,int left,int right)
{
//void Swap(int *a,int *b);
//int midi=GetMidi(a,left,right);
int key = left;
while(left<right)
{
while (left<right && a[right]>=a[key])
{
right--;
}//找大
while (left < right && a[left] <= a[key])
{
left++;
}//找小
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[key]);
return left;
}
乍一看,双重循环嵌套,直接O(n^2),那就错啦。我们用画图来看一看代码逻辑
完成排序,left 和right指针合起来遍历完整个数组,所以时间复杂度为O(n);
显然,快排的效率比冒泡排序高得多。这是一个阶别的跨越。冒泡是平方阶,快排是线性阶。
然后我们来看看经典的斐波那契数列
int Fib(int N)
{
if (N <= 2)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
斐波那契数列类似于细胞分裂,一分二,二分四,四分八,……,指数级增加,但很容易错的是理所当然认为因为这样时间复杂度就是O(n^2),那就又错啦。
虽然斐波那契数列时间复杂度确实是O(n^2);但这里要计算等比数列求和,把每一层执行次数相加;就是2^0+2^1+2^2+2^3+2^4+2^5+……+2^n=2^n-2^0=2^n-1;所以时间复杂度为O(n^2)。
斐波那契数列是一个树的形状,但不是完整的树,右下角会缺一块,但不影响时间复杂度。
空间复杂度
空间复杂度是计算为运行程序而额外开辟的空间大小。现阶段O(1)和O(n)空间复杂度的算法较多,几乎没有O(n^2)及以上的。接下来我们以斐波那契数列为实例看看其空间复杂度。
空间复杂度实例
int Fib(int n) {
if (n == 1 || n == 0)
return 1;
return Fib(n - 1) + Fib(n - 2);
}
注意:空间可重复利用,时间一去不复返。Fib(n-1)和Fib(n-2)可使用同一块内存区。我们对此做一个验证
int Fib(int n) { int a = 1; printf("%p\n", &a); if (n == 1 || n == 0) return 1; return Fib(n - 1) + Fib(n - 2); } int Factor(int n) { int a = 1; printf("%p\n", &a); if (n == 0 || n == 1) return 1; return n * Factor(n - 1); } int main() { printf("/"); Fib(3); printf("/"); Factor(3); return 0; }
函数第一个变量的地址就是函数地址,我们用a来观察函数地址的变化。
由此可观察到斐波那契函数和阶乘函数中,在多层递归中,同层的函数地址相同,不同层的函数地址不同;
所以函数递归的最大开辟空间就是递归的最大层数,空间 复杂度就是O(N);
这里简单的介绍了时间空间复杂度,下篇博客仔细分析几个例题,来解决算法的应用。
另外,在学习数据结构时一定要重视画图,把图画好才能更好地理解,有更清晰的思路;