首先提出一个问题,如何衡量一个算法的复杂度?
算法的时间复杂度和空间复杂度统称为算法的复杂度。
一.时间复杂度
1.概念:
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数f(n),进而分析f(n)随n的变化情况并确定T(n)的数量级。这里用"O"来表示数量级,算法的时间复杂度表示为:T(n)=O(f(n));这个表达式表示随着问题规模的n的增大,算法的执行时间的增长率和f(n)的增长率相同,这称作算法的渐进时间复杂度,简称时间复杂度。而我们一般讨论的是最坏时间复杂度,这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的上界,分析最坏的情况以估算算法指向时间的一个上界。
简单来说,时间复杂度实际就是一个函数,该函数计算的是执行基本操作的次数。
2.计算时间复杂度的方法:
<1> 用常数1代替运行时间中的所有加法常数
<2>修改后的运行次数函数中,只保留最高阶项
<3>如果最高项系数存在且不为1,去除最高阶项的系数
常数阶O(1)
,对数阶O(log2n)
,线性阶O(n)
, 线性对数阶O(nlog2n)
,平方阶O(n^2)
,立方阶O(n^3)
,…,
k次方阶O(n^k)
,指数阶O(2^n)
。
随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
二.空间复杂度
1.概念:
空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度,记为S(n)=O(f(n))。它是指运行完一个程序所需内存的大小。
对于一个算法来说,空间复杂度和时间复杂度往往是相互影响的。当追求一个较好的时间复杂度时,可能会使空间复杂度的性能变差,即可能导致占用较多的存储空间;反之,当追求一个较好的空间复杂度时,可能会使时间复杂度的性能变差,即可能导致占用较长的运行时间。有时我们可以用空间来换取时间以达到目的。
2.程序执行时所需存储空间包括以下两部分:
<1>固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。
3.递归算法的空间复杂度:
三.举例说明时间复杂度和空间复杂度
1.普通情况
<1>
x=100; y=60; while(y>0) if(x>10) { x=x-3; y--; } else x++; 解答: T(n)=O(1)
这个程序看起来循环了很多次,但是并没有看到n,故这段程序的运行是和n无关的,只是一个常数阶的函数。
如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数。
此类算法的时间复杂度是O(1)
<2>
for(int i = 0; i < n; i++){ printf("%d ",i); } //运行次数n //时间复杂度O(n)
<3>
for(int i = 0; i < n; i++){ for(int j = 0; j < n; j++){ printf("%d ",i); } } //运行次数n^2,时间复杂度O(n^2)
<4>
for(int i = 0; i < n; i++){ for(int j = i; j < n; j++){ printf("%d ",i); } } //运行次数为(1+n)*n/2 //时间复杂度O(n^2)
<5>
int i = 1, n = 100; while(i < n){ i = i * 2; } //设执行次数为x. 2^x = n 即x = log2n //时间复杂度O(log2n)
2.求二分法的时间复杂度和空间复杂度。
<1>非递归:
#include<stdio.h> #include<assert.h> { assert(number>=0); int left = 0; int right = number-1; while (right >= left) { int mid = (left&right) + ((left^right)>>1); if (array[mid] > data) { right = mid - 1; } else if (array[mid] < data) { left = mid + 1; } else { return (array + mid); } } return NULL; }
分析如下
假设最坏的情况下,循坏x次找到,有n/(2^x)=1,则x=log2 n
循环的基本次数是log2 n,所以:
时间复杂度是O(log2 n);
<2> 递归:
#include<stdio.h> #include<assert.h> { assert(left); assert(right); if (right >=left) { T* mid =left+(right-left)/2; if (*mid == data) return mid; else return *mid > data ? BinarySearch(left, mid - 1, data) : BinarySearch(mid + 1, right, data); } else { return NULL; } }
分析画图等同非递归情况,假设最坏的情况下,循坏x次找到,有n/(2^x)=1,则x=log2 n
递归的次数和深度都是log2 N,每次所需要的辅助空间都是常数级别的:
时间复杂度:O(log2 N)
int fib(int a,int b,int num) { int c; if (num <= 0) return -1; else if (num == 1) return a; else if (num == 2) return b; else { while (num - 2) { c = a + b; a = b; b = c; num--; } return c; } } int main() { int n; int result; printf("Input n\n"); scanf("%d", &n); result = fib(2, 3, n);//可自定义输入第一个数和第二个数 if (result == -1) { printf("Input Error!\n"); } else { printf("n is %d\n", result); } return 0; }
时间复杂度分析:
从n(>2)开始计算,用F(n-1)和F(n-2)两个数相加求出结果,这样就避免了大量的重复计算,它的效率比递归算法快得多,算法的时间复杂度与n成正比,即算法的时间复杂度为O(n).
int fib(int num) { if (num < 0) return -1; if (num <= 2 && num > 0) return 1; else return fib(num - 1) + fib(num - 2); } int main() { int n; int result; printf("Input n\n"); scanf("%d", &n); result = fib(n); if (result == -1) printf("Input Error!\n"); else printf("Result is %d\n", result); return 0; }
在递归调用过程中Fib(3)被计算了2次,Fib(2)被计算了3次。Fib(1)被调用了5次,Fib(0)中被调用了3次。所以,递归的效率低下,但优点是代码简单,容易理解。
递归算法时间复杂度为(二叉树的节点个数):O()=(2^h)-1=2^n。空间复杂度为树的高度:h即o(n).
时间复杂度O(2^N)
空间复杂度O(N)