目录
算法的复杂度
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源。因此**衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,**即时间复杂度和空间复杂度。
**时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。**在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空法的空间复杂度。
时间复杂度
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数(数学的函数表达式) 它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。
计算一下下面这个函数的时间复杂度:
cpp
void Func1()
{
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);
}
所以他时间复杂度的函数式就是 F(N) = N*N + 2*N +10
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。(估算)
根据表达式我们可以知道,N越大,后两项对结果影响越小,所以我们可以使用大O渐进表示法估算时间复杂度为:
O(N^2)
大O的渐进表示法
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
以下是一些计算实例:
实例1
cpp
//计算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\n", count);
}
时间复杂度:O(N)
实例2:
cpp
//计算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\n", count);
}
时间复杂度:
如果题目没有说明M和N的大小关系:O(M+N)
如果M >> N:O(M)
如果N >> M:O(N)
如果M和N差不多大:O(M) 或者 O(N) 都行
实例3:
cpp
//计算Func4的时间复杂度?
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}
时间复杂度:O(1)
实例4:
cpp
//计算strchr的时间复杂度?
const char * strchr ( const char * str, int character );
//strchr的大概实现:
while(*str)
{
if(*str == character)
{
return str;
}
else
++str;
}
假设要查找hello world里的一个字符:
当一个算法随着输入不同,时间复杂度不同,时间复杂度做悲观预期,看最坏的情况。
所以这个函数的时间复杂度为:O(N)
冒泡排序的时间复杂度
cpp
// 计算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-3次,一直到最后一趟1次比较,这是一个等差数列,总共比较次数为 N*(N-1)/2 。所以:
精确:F(N) = N*(N-1)/2
时间复杂度:O(N^2)
二分查找的时间复杂度
cpp
//计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n;
while (begin < end)
{
int mid = begin + ((end-begin)>>1);
if (a[mid] < x)
begin = mid+1;
else if (a[mid] > x)
end = mid;
else
return mid;
}
return -1;
}
时间复杂度:O(log2 N) (以2为底的对数)
算时间复杂度不能只去看时几层循环,而要去看他的思想。
最好的情况:O(1)
最坏的情况(比如要找的数字在头尾):O(log2 N)
递归的时间复杂度
cpp
//计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
if(0 == N)
return 1;
return Fac(N-1)*N;
}
递归算法的时间复杂度:递归次数*每次递归调用的次数
时间复杂度:O(N)
斐波那契的时间复杂度
cpp
//计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
总体调用次数如上图所示。
可见接近于一个等比数列。
精确:F(N) = 2^0 + 2^1 + 2^2 + ... + 2^(N-1) - X = 2^N-1 - X(注:X为右边画圈空的一块,因为有一些递归分支会更早结束,所以会少一些递归调用)
X远小于2^N-1,所以:
时间复杂度:O(2^N)
可以看出来,斐波那契用递归写法是很垃圾的,2^N 时间复杂度太大了。
空间复杂度
空间复杂度也是一个数学函数表达式,是对一个算法在运行过程中临时额外占用存储空间大小的量度。
空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。 空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间乘确定。
实例1:
cpp
//计算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) //函数内总共新定义两个变量,end和i,2个,即常数个,常数个就是O(1)。
实例2:
cpp
//计算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;
}
很明显,空间复杂度是O(N),因为函数内需要一块N个元素的数组的空间,然后将这个空间的起始地址返回,忽略掉大O表达式内的常数后就是O(N)。
实例3:阶乘的空间复杂度是多少
cpp
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
if(N == 1)
return 1;
return Fac(N-1)*N;
}
递归每次调用建立一个栈帧都是O(1),调用了N次,建立了N个栈帧那就是O(N)。所以空间复杂度是O(N)
所以对于递归的空间复杂度,要看递归的深度。
斐波那契的空间复杂度
cpp
//计算斐波那契递归Fib的空间复杂度?
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
分析问题之前,需要记得:
空间是可以重复利用的,不累计的。
时间是一去不复返的,累计的。
如上图,首先会进行 Fib(N-1) 的运算,当Fib(N-1)以及下面分支计算完成时会将空间全部返回给系统,然后再将这部分空间给Fib(N-2)以及它下面的分支使用。这就是空间的重复利用。
而同一时间,建立栈帧最多的就是左边的一列,一共n个栈帧,每个栈帧都是O(1),n个,即O(N)。
这也正如上文所说,递归的空间复杂度要看递归的深度。
常见复杂度(时间)对比
(全文完)