算法效率分析分为两种:第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率被称作 空间复杂度。 时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间, 在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计 算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
在计算机科学中,算法的时间复杂度是一个数学函数,它定量描述了该算法的运行时间。一个 算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我 们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
我们来计算下以下代码,来看下以下的时间复杂度:
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;
}
printf("%d\n", count);
}
前面也说了一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。首先我们的代码是包含4个循环3个for和一个while,每执行一次操作就是一次,可以看出来第一个for再嵌套了一个for 执行n*n次,第二个for执行2*n次,while的m=10,执行了10次
那么func1 执行的基本操作次数 :F(n) = n^2 + 2*n +10
如果用大O渐近表示法就是O(n^2)
为什么这样表示呢?
因为实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数
推导大O阶方法
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
所以上述代码的时间复杂度就为O(n^2)。
上面的表达式为例,当N为不同的值时,表达式的结果是
n=100 F(n)=10210
n=1000 F(n)=1002010
n=10000 F(n)=100020010
我们发现,当n不断变大时,表达式的值也不断变大,而对表达式的结果影响最大的一项就是这个表达式中阶数最高的那一项。
通过上面我们会发现大O的渐进表示法就是去掉了那些对结果影响不大的项。
另外有些算法的时间复杂度存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数(上界) 平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)
常见的时间复杂:
(1)O(1):常量阶,运行时间为常量
(2)O(logn):对数阶,如二分搜索算法
(3)O(n):线性阶,如n个数内找最大值
(4)O(nlogn):对数阶,如快速排序算法
(5)O(n^2):平方阶,如选择排序,冒泡排序
(6)O(n^3):立方阶,如两个n阶矩阵的乘法运算
(7)O(2^n):指数阶,如n个元素集合的所有子集的算法
(8)O(n!):阶乘阶,如n个元素全部排列的算法
上面从上至下依次的时间复杂度越来越大,执行的效率越来越低。
下面选几个常见的进行讲解:
(1) O(1): 常量阶
int count = 0;
for(int i = 0; i < 100; i++){
count++;
}
上述代码一共执行了100次, 是常数阶, 运行时为常量,不管是10000,还是100000, 那这个时间复杂度都是O(1)
(2) O(logn): 对数阶
int i = 1;
while(i < n){
i = i*2;
}
在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了,所以每执行一次乘以 2
,假设循环x次之后,也就是2^x次方, 也就是说 2 ^x =n,那么 x = log2^n ,所以说当循环 log2^n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(logn)
(3) O(n): 线性阶
int count = 0;
for (int i = 0; i < n; i++) {
count++;
}
这个代码有个for,执行了n次,所以很明显是时间复杂度为: O(n)
(4) O(n^2):平方阶
int count = 0;
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
count++;
}
}
上述代码是1个for循环嵌套1个for, 相当是外循环执行1次内循环执行n次, 那么外循环执行n次, 就相当是执行了n*n次, 所以时间复杂度为: O(n^2)
(5) O(nlogn):对数阶
for(int i = 0; i < n; i++)
{
int j = 1;
while(j < n)
{
j = j * 2;
}
}
对于O(nlogn)也很好理解, 就前说的O(logn), 那么执行n次logn次,时间复杂度就是O(nlogn)
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少bytes的空 间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也 使用大O渐进表示法。
空间复杂度比较常用的有:O(1)、O(n)...
那就用前面讲过代码来举例下
int count = 0;
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
count++;
}
}
现在看一下它的空间复杂度,根据定义我们知道,空间复杂度是用来算的是变量的个数,也就是占用空间内存的大小, 那么我们就可以根据算法中创建的变量的个数来表示算法的空间复杂度, 那么看下这个代码, 一共创建了三个,count, i, j,他们分配的空间都不随着处理数据量变化, 根据大O的线性表示法, 所以空间复杂度为: O(1)
再来看下这个斐波那契数的代码,用的是递归的方法, 计算它的空间复杂度是多少?
long factorial(int N) {
return N < 2 ? N : factorial(N-1)*N;
}
我们知道每一次调用,都会开辟了1个栈帧, 就会为调用的那个函数创建一块空间, 那么执行n次,也就是说开辟了n个栈帧, 那么它的空间复杂度为: O(n)