时间复杂度
时间复杂度
1. 分析算法
分析算法的好坏结果意味着预测算法需要的资源,这里的资源我们一般包括时间资源和空间资源,即时间复杂度和空间复杂度,如没做特殊说明一般算法复杂度指时间复杂度。
一般来说,算法需要的时间与输入的规模同步增长,所以通常把一个程序的运行时间描述成其输入规模的函数。
输入规模的最佳概念依赖于研究的问题。比如一般的排序问题,我们一般把输入中的项数,如数组大小规模n。有时,用两个数而不是一个数来描述输入规模可能更合适,例如图中的一些算法一般用顶点数和边数来描述。
一个算法在特定输入上的运行时间是指执行的基本操作或步数。这边假设计算机中常见的指令:算术指令,数据移动指令,控制指令等的所需时间视为常量。
2. 时间复杂度
时间频度
算法的运行时间从理论上是不可能算出来的,必须在机器上运行后才知道,而且对于不同机器每次运行也不相同,但我们没必要对每个算法都进行运行测试,我们只需要知道哪个算法的运行时间长,哪个算法的运行时间短。同时一个算法的运行时间与算法中的指令,语句的执行次数成正例。一个算法的语句执行次数称为语句频度或时间频度,记为T(n)。
时间复杂度
之前提到过T(n)是语句频度,对于不同的问题T(n)会是n的不同的函数,随着n的变化,T(n)也会不断变化,但是我们想知道T(n)的变化情况,所以我们可以引进辅助函数f(n)(f(n)一般是常见的函数或我们比较熟悉的函数),使得n趋近于无穷大时,T(n)/f(n)的极限为不等于零的常数,即T(n)和f(n)是一个数量级的,记作T(n) = O(f(n)),将O(f(n))称为算法的渐进时间复杂度,即时间复杂度。
最坏情况与平均情况
最坏情况运行时间即最坏情况下的算法运行时间。
平均情况运行时间即所有可能的输入情况均以等概率出现的情况下,算法的期望运行时间。
一般情况下,算法的复杂度是指最坏情况运行时间。这样做的理由如下:(1)一个算法的最坏情况运行时间给出了任何输入的运行时间的一个上界。(2)对某些算法,最坏情况经常出现。(3)平均情况往往与最坏情况大致一样差,或者说增长量级相同。
增长量级:算法的时间复杂度一般是输入规模的函数,但我们真正感兴趣的是运行时间的增长率或者增长量级。也就是函数公式中最重要的项,因为当n的值很大时,低阶项相对来说不太重要。
3. 大O表示法
像前面用O( )来体现算法时间复杂度的记法,我们称之为大O表示法。
算法复杂度可以从最理想情况、平均情况和最坏情况三个角度来评估,由于平均情况大多和最坏情况持平,而且评估最坏情况也可以避免后顾之忧,因此一般情况下,我们设计算法时都要直接估算最坏情况的复杂度。
大O表示法O(f(n)中的f(n)的值可以为1、n、logn、n²等,因此我们可以将O(1)、O(n)、O(logn)、O(n²)分别可以称为常数阶、线性阶、对数阶和平方阶。
时间复杂度计算
时间复杂度计算方法:
1. 用常数1代替运行时间中的所有加法常数
2. 修改后的运行次数函数中,只保留最高阶项
3. 去除最高阶项的系数
按数量级递增排列,常见的时间复杂度有:
常数阶O(1),对数阶O(log2n),线性阶O(n),
线性对数阶O(nlog2n),平方阶O(n^2),立方阶O(n^3),…,
k次方阶O(n^k),指数阶O(2^n)。
随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
这里对一些常见情况的计算进行简单的说明。
常数阶:
Int a = 0, b = 0; //执行一次
Int c = (a+b)/2; //执行一次
System.out.println(c) //执行一次
上面代码的执行步数或者次数是3,根据计算方法的规则1,将常数3换为1,所以该算法的时间复杂度是O(1)。如果我们再添加10条或者100条这种赋值,计算语句,因为这和输入的规模没有关系,计算步数是一个较大的常数而已,最后还是需要换为1,所以这个算法的复杂度仍然是O(1)。
对数阶:
for(int i = 1; i < n; i = i * 2){
//时间复杂度O(1)的算法
}
在上面代码的循环中,假设循环执行次数为m,则有2^m = n, 所以m = log
线性阶:
for(int i = 0; i < n; i++){
//时间复杂度O(1)的算法
}
上面代码中,循环执行了n次,所以算法的时间负责度为O(n)
线性对数阶:
merge_sort(A, p, r){
if(p < r){
q = (p + r) /2;
merge_sort(A, p, q)
merge_sort(A, q+1, r)
merge(A, p, q, r)//合并,时间复杂度O(n)
}
}
上面代码是归并排序的伪代码,其中用了递归的方式来划分排序合并。这边我们假设对于规模为n的问题,该算法的运行时间为T(n),则可以得到T(n) = 2T(n/2) + cn。通过代入法或者递归树我们可以得到该算法的时间复杂度,T(n) = 2(T(n/4)+cn)为O(nlgn)。
平方阶:
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
//时间复杂度O(1)的算法
}
}
上面代码中,外层循环执行了n次,对于每一次外层循环,内部循环也执行了n次,所以循环中的代码执行了n*n = n
下面我们看下冒泡排序中的一段算法:
for(int i=0;i< n - 1;i++){//外层循环控制排序趟数
for(int j=0;j<n-1-i;j++){//内层循环控制每一趟排序多少次
if(arr[j]>arr[j+1]){
//交换数组中两数位置
}
}
}
上面代码中,外层循环执行了n-1次,而内层循环每次执行的次数都不同,从n-1次到1次,所以总共的执行次数为:
(n-1) + (n-2) + … + 1 =
根据前面提到的计算规则,保留最高阶,去掉常数,所以该算法时间复杂度为O( n
参考资料:
1. 《算法导论》
2. https://blog.csdn.net/itachi85/article/details/54882603
相关阅读
1、时间复杂度 (1)时间频度一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对