目录
时间复杂度为什么不使用时间来衡量而使用基本语句的运行次数来衡量?
如何衡量一个算法的好坏?
-
什么是算法?
- 算法就是定义良好的计算过程;例如取出一组值作为输出,产生一个值作为输入;
- 算法就是一系列的计算步骤,用来将输入数据转换成输出结果;
-
算法优劣衡量:
- 正确性,可读性,健壮性,时间效率高,空间使用率低,简单;
在我看来,要评价一个算法的好坏有四个要点:
第一是算法正确性,这个算法要可以正确解答问题,不能在运行中频繁出现误差;
第二是具有良好的可读性和可维护性,这样有利于后期维护;
第三就是时间复杂度,即一个算法中的语句执行次数称为语句频度或时间频度。记为T(n);
第四是空间复杂度,一个程序的空间复杂度是指运行完一个程序所需内存的大小;
对一个算法的评价主要从时间复杂度和空间复杂度来考虑 ,算法在时间的高效性和空间的高效性之间通常是矛盾的 。所以一般只会取一个平衡点。通常我们假设程序运行在足够大的内存空间中,所以研究更多的是算法的时间复杂度.
什么是时间复杂度?
时间复杂度简介:
在不同的运行环境中,一个程序的运行时间会完全不同,但是单个语句的执行次数是可以计算的;一个算法花费的时间与算法中的基本操作语句的执行次数成正比例,算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度,记为T(n)。
在刚才提到的语句频度中,n称为问题的规模,当n不断变化时,语句频度T(n)也会不断变化。但有时我们想知道它的变化呈现什么规律。为此,我们引入时间复杂度概念。
T(n) 不同,但时间复杂度可能相同。 如:T(n)=n²+5n+6 与 T(n)=3n²+3n+2 它们的T(n) 不同,但时间复杂度相同,都为O(n²);
简单来讲,我们只需要计算大概执行次数,这里使用大O的渐进表示法;
时间复杂度为什么不使用时间来衡量而使用基本语句的运行次数来衡量?
- 一个程序在不同的运行环境,执行时间不同;
- 但是程序单语句的执行次数不变;
时间复杂度的O渐进表示法 :
- 根据大O渐进法原则:
- 用常数1取代运行时间中所有加法常数;
- 修改后的运行次数函数中,只保留最高项;
- 最高项存在且不是1,则取出与最高项相乘的常数,得到的结果就是大O阶;
- 例如下面这个简单代码:
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++;
}
printf("%d", count);
}
- 基本操作次数为N^2+2*N;
- 用大O渐进法表示,它的时间复杂度为: O(N^2);
时间复杂度最优,平均,最差情况:
- 最优情况:任意输入规模下最下运行次数;
- 最差情况:任意输入规模下最大运行次数;
- 平均情况:任意输入规模的期望运行次数;
- 例如:
- 在一个长度为N数组中搜索一个数据X;
- 最优情况:1次找到
- 最差情况:N次找到
- 平均情况:(1+2+3+4+........N)/2N,用渐进法表示,为N/2次;
为什么时间复杂度看的是最差情况?
- 一般不特别说明,讨论的时间复杂度均是最坏情况下的时间复杂度。
- 这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的上界,这就保证了算法的运行时间不会比任何更长。
- 如果最差情况下的复杂度符合我们的要求,我们就可以保证所有的情况下都不会有问题。
常见时间复杂度计算:
二分查找:
- 对一个有序数列进行数据搜索查找,每次折半查找;
- 二分查找函数代码示例:
int BinarySearch(int *a, int n, int x) {
assert(a);
int begin = 0;
int end = n - 1;
while (begin < end) {
int mid = begin + ((begin + end) >> 1);
if (a[mid] < x)
begin = mid + 1;
else if (a[mid] > x)
end = mid;
else
return mid;
}
return -1;
}
- 为了说清楚二分查找语句执行次数,我列了一张表:
- 假设共有N个元素:
-
第几次查找 剩余待查找元素 1 N/2 2 N/(2^2) 3 N/(2^3) ........ ....... k N/(2^k)
-
按照最坏的情况,当剩余元素为1则为待查找元素;
则令 N/(2^K)=1
=> N=2^K
=> K=log2N
所以二分查找的时间复杂度为O(log2N);
递归算法求阶乘:
- 递归:函数自身调用自身
-
long long Fac(size_t n) { return n < 2 ? n : Fac(n - 1)*n; }
分析基本语句有:
if (n == 1 || n == 0)
return 1;
else
return Fac(n - 1) * n;
- 首先,基本语句次数为:1
- 然后计算递归语句,当n>=2时,计算Fac(n-1)*n;
- 图解:
- 得到递归语句计算次数为N;
- 则阶乘递归算法总时间复杂度为O(n);
-
第几次计算 得到数据 1 fac(n)=Fac(n-1)*n 2 Fac(n-1)=Fac(n-2)*(n-1) 3 fac(n-2)=Fac(n-3)*(n-2) 4 fac(n-3)=Fac(n-4)*(n-3) ......... ....... n fac(1)=1
递归算法求斐波那契:
- 首先,贴上递归函数写的求斐波那契代码:
-
int fib(int n){ if (n =1||n=2){ return 1; } else return fib(n - 1) + fib(n - 2); //所求的数是它前两个数之和 } int main(){ int n = 1; printf("求第n个斐波那契数:"); scanf("%d", &n); printf("第%d个斐波那契数是%d\n", n,fib(n)); system("pause"); return 0; }
分析函数语句:
if (n =1||n= 2){
return 1;
}
else
return fib(n - 1) + fib(n - 2); //所求的数是它前两个数之和
- 单语句执行次数:1;
- 递归语句执行次数;以6为例,画图详解:
- 想要计算Fac(6),需要计算Fac(4)和Fac(5);
- 然后分别计算Fib(4)和Fib(5);则需要分别计算Fib(2),Fib(3)和Fib(3)和Fib(4);
- 逐一计算,计算次数分别为1,2,4,8,
- 则Fac(n)的递归语句计算次数为2^0+2^1+2^2+2^3+…………2^(n-2);
- 则求得总次数为2^(n-3)*1
- 根据时间复杂度大O的渐进算法得,递归求斐波那契数的时间复杂度为O(n);
- 递归函数的时间复杂度为基本语句单次递归次数*每次递归的总次数
什么是空间复杂度?
- 空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。空间复杂度不是计算程序占用了多少空间,而是变量的个数;
- 空间复杂度也使用大O渐进表示法;
- 用常数1取代运行时间中所有加法常数;
- 修改后的运行次数函数中,只保留最高项;
- 最高项存在且不是1,则取出与最高项相乘的常数,得到的结果就是大O阶;
普通函数空间复杂度:
冒泡排序算法:
- 这里以冒泡排序为例:
- 外循环是遍历每个元素,每次都放置好一个元素;
- 内循环是比较相邻的两个元素,把大的元素交换到后面
- 代码如下:
void BubbleSort(int *a, int *b) {
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]) {
swep(&a[i - 1]), &a[i])
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
- 空间复杂度就是看算法有无借助辅助空间;
- 直接在原始数据上排,不需要额外的存储空间;
- 数组不属于函数开辟空间;
- 所以冒泡排序的 空间复杂度为O(1);
递归函数空间复杂度:
递归算法斐波那契数列:
- 这次我们来看一个求斐波那契数列的另一份代码:
- 递归算法的空间复杂度=递归深度N*每次递归所要的辅助空间(这里我认为是每次创建新变量的个数);
long long*fib(size_t n) {
if (n == 0)
return NULL;
long long*fibArray = new long long[n + 1];
fibArray[0] = 0;
fibArray[1] = 1;
for(int i = 2; i <= n; ++i){
fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
}
return fibArray;
}
- 根据 long long*fibArray = new long long[n + 1];,创建了变量,申请了一段long long 空间;
- 递归深度,以计算fib(5)为例,
- 递归深度为n-1;
- 则根据大O渐进法,空间复杂度是O(n);
- 此时时间复杂度也是O(n);
伪递归求斐波那契数列:
- 假如求前6项;
- 则:
-
long Fib(long first, long second, long n){ if (n < 3) return 1; if (3 == n){ return first + second; } return Fib(second, first + second, n - 1); }
则分别return 的值为:
-
第几次 return(second,first+second,n-1) 0 return(1,1,6) 1 return(1,2,5) 2 return(2,3,4) 3 return(3,5,3) 4 return 8; 自身调用自己,无其他运算;
- 则空间复杂度为O(1);
- 时间复杂度为O(n);
循环算法求斐波那契数列:
long Fib(long first, long second, long n){
int third = 0;
if (n < 3)
return 1;
while (n >3){
int temp = second;
second = first + second;
first = temp;
n--;
}
third = first + second;
return third;
}
- 此时时间复杂度达到了O(n),空间复杂度达到了O(1).
常见时间复杂度 :
执行次数 | 时间复杂度 |
12 | O(1) |
3*n+5 | O(N) |
2*n^2+5N+6 | O(n^2) |
log2n+10 | O(log2n) |
5n^3+6 | O(n^3) |
2^n | O(2^n) |
常见时间复杂度大小:
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
晚安;真正值得守护的,只有这个世界上的爱与阳光;