一、时间复杂度:
针对某一具体的需求,往往有多种不同的实现思路,而我们要选择的显然是时间效率更高的思路,那如何进行选择呢?是分别将其实现再看其具体的效率吗?很显然逐个实现,再将程序放在机器上跑一下进行比较,耗费的时间精力是比较大的,也是不可取的;那我们能不能依照提出的思路分析其基本操作的执行次数呢?这种方案是可行的,通过事前的计算比较,可以推算出最优的解决思路,这便是我们需要掌握时间复杂度的意义。
1. 请计算一下Func1中++count语句总共执行了多少次?
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;
}
}
Func1 的基本操作的执行次数:
F(N)= N*N+2*N+10
当N=10时,F(N)=130
当N=100时,F(N)=10210
当N=1000时,F(N)=1002010
由此可见,随着基数的增大,除最大项以外的项数对该算法的执行次数的影响越来越小,当达到一个特别大的量级的时候,除最高项外,其余各项对算法的影响几乎忽略不计了,算法的时间复杂度用大O的渐进表示法表示,规则如上图,去掉对结果影响不大的项,那么Func1的最大项为N*N,简洁的表示出了执行次数,所以其时间复杂度为O(N*N)
同时,我们也知道了O的渐进表示法是估算,是计算大概次数所属的量级
2.计算Func2的时间复杂度?
void Func2(int N)
{
int count = 0;
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d", count);
}
基本操作执行次数 F(N)=2*N+10
时间复杂度O(N)
这里最高阶为2N,其系数为2,要去除这个2,因为当N的量级很大的时候,系数对其影响不大
3.计算Func3的时间复杂度?
void Func3(int N, int M)
{
int count = 0;
for (int k = 0; k < M; ++ k)
{
++count;
}
for (int k = 0; k < N ; ++ k)
{
++count;
}
printf("%d", count);
}
基本操作执行次数 M+N
时间复杂度O(M+N)
最高项分别为M和N这两个未知数
4.计算Func4的时间复杂度?
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d", count);
}
基本操作执行次数 100
时间复杂度O(1)
最高项为常数100,要用常数1来取代运行时间中的加法常数,用O(1)代表常数次,最根本的底气在于CPU的运算速度足够快,例如:
#include <stdio.h>
#include <time.h>
int main()
{
size_t begin = clock();
for (int i = 0; i < 10000000; i++)
{
;
}
size_t end = clock();
printf("%d毫秒", end - begin);
return 0;
}
从这里可以看到基本操作执行一千万次和一百次之间只相差4毫秒,这种时间上的差异是可以忽略不计的,O(1)来表示常数次也说明了计算机CPU运算速度之快。
5.计算strchr的时间复杂度?
const char * strchr ( const char * str, char character );
这里strchr实现如下:
const char* strchr(const char* s,char c)
{
while(*s != '' && *s != c)
{
++s;
}
return *s == c ?s:NULL;
}
该算法存在最好、平均和最坏情况
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
在实际中一般情况关注算法的最坏情况,也就是说,该算法的时间复杂度为O(N)
6.计算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个数,第一次,a[0]数分别和后面的数两两比较,共执行 n-1次
第二次,a[0]和后面的数两两比较,共执行n-2次
第三次,执行n-3次
......
最后一次,执行1次
共执行了假设每一次为一轮比较,共比较了n轮
则基本执行次数F(N)=[(N-1)+ 1 ]* N/2 = N*N/2
所以冒泡排序的时间复杂度为O(N^2)
7.计算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;
}
二分查找法,查找x次,每次除二,缩小查找空间一半的范围,假设空间大小为N,找到要找的数时,空间大小为1
第一次,N/2
第二次,N/2/2
第三次,N/2/2/2
......
第x次,N/2/2.../2=1
所以二分查找这个基本查找次数x=logN
时间复杂度为O(logN)
需要注意的是logN在算法分析中表示底数为2,N为真数,有些地方会写成lgN(但不建议这样写),而以其他数为底数的时候,底数不可以省略,但是也一般很少出现其他底数
同时,二分查找的速度比较快:
比如查找在1000个人中查找张三,仅需10次左右;
1000000个人,需要大概20次;
1000000000个人,需要大概30次;
2000000000个人,需要大概31次;
由此可见,二分查找法运算效率高。
8.计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
if(0 == N)
return 1;
return Fac(N-1)*N;
}
函数的调用可以认为不影响时间复杂度
递归时间复杂度计算方法和技巧:
每次递归调用的执行次数累加
第一次调用 执行1次
第二次调用 执行1次
第三次调用 执行1次
......
第N次调用 执行1次
累加起来,共执行N次
所以该递归的时间复杂度为O(N)
二、空间复杂度
空间复杂度计算算法运行所需额外空间,在计算机发展早期,计算机的存储容量很小,对空间复杂度比较在乎,随着计算机行业的迅速发展,其存储容量已经到达了很大的高度,如今我们不需要再特别关注一个算法的空间复杂度。
需要注意的是:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时显示申请的额外空间来确定。
1.计算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;
}
}
该算法中有2个额外的变量
所以空间复杂度为O(1)
2.计算Fibonacci的空间复杂度?
// 返回斐波那契数列的前n项
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;
}
malloc开辟(n+1)个空间的变量
所以空间复杂度为O(N)
3.计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
if(N == 0)
return 1;
return Fac(N-1)*N;
}
递归时间复杂度的计算方法与技巧:
每次递归调用的变量个数累加
在这个阶乘中,每次调用创建一个栈桢,每个栈桢中有常数个变量,共调用N次栈桢
所以空间复杂度为O(N)
三.计算斐波那契递归Fib的时间复杂度和空间复杂度?
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
递归调用计算时间复杂度和空间复杂度我们已经知道,只需要累加起递归调用时的执行次数和变量的个数。
如上图可知,斐波那契递归是以2的N次方量级进行累加的,忽略掉一些提前结束递归的分支,粗略的计算可得基本执行次数大概为2^(N-1)-1,所以其时间复杂度为O(2^N)
通过上述计算我们得知斐波那契递归的时间复杂度类似细胞分裂一样,是个等比数列,从中我们看到,里面有很多冗余的计算,像Fib(N-3)+Fib(N-4)被计算了多次,诸如此类的增加了算法运行时间,在实际的操作中,这种算法是要避免的,像这种只提供了理论层面的价值,在实践中价值为无,因为耗时太长了,这里给我们提供了借鉴意义,要选择时间复杂度相对较低的算法。
那该算法的空间复杂度又是如何呢?
大家都知道,时间是一去不复返的,所以在计算时间复杂度时只能进行时间的累加,但是空间的使用是不同的,空间不是一次性的,可以重复利用,因此在斐波那契递归算法中,在某一空间创建一系列栈帧之后,某一分支递归调用结束后依次返回时,栈帧虽然销毁了,但是下次开辟的栈帧还是在当前所在的空间开辟的,并不会占用新的空间,一共开辟了n-1层栈帧,最多会使用N层栈帧,所以该算法的空间复杂度为O(N)。