首先提出一个问题,如何衡量一个算法的复杂度?
算法的时间复杂度和空间复杂度统称为算法的复杂度。
一.时间复杂度
1.概念:
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数f(n),进而分析f(n)随n的变化情况并确定T(n)的数量级。这里用"O"来表示数量级,算法的时间复杂度表示为:T(n)=O(f(n));这个表达式表示随着问题规模的n的增大,算法的执行时间的增长率和f(n)的增长率相同,这称作算法的渐进时间复杂度,简称时间复杂度。而我们一般讨论的是最坏时间复杂度,这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的上界,分析最坏的情况以估算算法指向时间的一个上界。
简单来说,时间复杂度实际就是一个函数,该函数计算的是执行基本操作的次数。
2.计算时间复杂度的方法:
<1> 用常数1代替运行时间中的所有加法常数
<2>修改后的运行次数函数中,只保留最高阶项
<3>如果最高项系数存在且不为1,去除最高阶项的系数
3.按数量级递增排列,常见的时间复杂度有:
常数阶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>固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。
<2>可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。
3.递归算法的空间复杂度:
递归深度N*每次递归所要的辅助空间 (如果每次递归所需的辅助空间是常数,则递归的空间复杂度是 O(N)).
三.举例说明时间复杂度和空间复杂度
1.普通情况
<1>
x=100;
y=60;
while(y>0)
if(x>10)
{
x=x-3;
y--;
}
else
x++;
解答: T(n)=O(1)
<span style="color:#993399">这个程序看起来循环了很多次,但是并没有看到n,故这段程序的运行是和n无关的,只是一个常数阶的函数。</span>
<span style="color:#993399">如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数。</span>
<span style="color:#993399">此类算法的时间复杂度是O(1)</span>
<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);
由于辅助空间是常数级别的所以:
空间复杂度是O(1);
<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)
空间复杂度:O(log2N )
3.斐波那契数列的时间复杂度及空间复杂度。
<1>非递归
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).
时间复杂度O(n)
空间复杂度O(1)
<2>递归
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)