前言
众所周知,针对某一个问题,可能有多种算法解决,每个算法的作者都认为自己写的是最优秀的算法,这时候就会产生议论甚至是争吵,谁的算法才算好呢?口说无凭,要拿出让人信服的证据才足以服众。因此算法效率的衡量方法算是每个程序猿必不可少的技能。
现在最主要的两种衡量方法便是时间复杂度和空间复杂度,但用的最多的是时间复杂度,因为随着技术的革新,硬盘容量和内存容量也在不断扩大,对于空间复杂度而言已经没有以往那般重要了(想想以前的程序猿要在几KB甚至更小的内存里倒腾也是蛮佩服的),对于用户而言跟软件产品的好坏直接挂钩的是时间,响应快的软件总是格外受欢迎,所以时间性能是尤为重要的,那么啥又是时间复杂度捏?咋个计算呢?
时间复杂度
定义
(摘自严版《数据结构》)一般情况下,算法重基本操作重复执行的次数是问题规模的某个函数,算法的时间度量计作
它表示随问题规模的增大,算法执行时间的增长率和的增长率相同,称做算法的渐进时间复杂度,简称时间复杂度。
不是说时间度量吗?为什么用的是算法基本操作执行次数来表示?
因为不同计算机硬件不同、操作系统不同、编译器不同等问题都会影响到算法执行的时间,在天河一号上跑算法的时间和在自己家用计算机上跑同样算法出来的时间应该差距还蛮大的。所以就考虑用算法基本操作的次数替代具体时间,毕竟算法执行时间也就是每个基本操作执行时间的总和,重复执行的基本操作次数多了,花费的总时间肯定也相应的多了。
那为啥说是渐进的时间复杂度呢?
因为要程序猿一条指令一条指令的数也不太现实,尤其是有循环、递归等等复杂的算法,想快速数清楚基本操作难度比较大,怎么办呢,那就数容易数的循环就好了,因为每个循环体里的操作都要重复执行,谁的算法循环次数多(特别是循环嵌套),相应的花的时间肯定也多。对于只执行一次的操作而言,这种重复执行的循环体肯定次数更多一些,所以就忽略掉一部分了,如果两个时间复杂度是不同级别的时候就可以忽略n前面的系数和低阶的次数,就是所谓的渐进了。
级别
计算时间复杂度的基本步骤
-
确定算法中的基本操作以及问题的规模。多数情况下取最深层循环内的基本操作(就是嵌套循环最里层的循环体)。循环次数和结束条件有关,一般是有一个循环参数在控制,增加到上界或者减少到下界就停止循环,这个循环参数就是所说的规模。
- 根据基本操作执行情况计算出规模的函数,并确定时间复杂度为(中增长最快的项/此项的系数)
- 注意:算法基本操作次数不一定是固定读,比如循环次数是由用户输入决定的时候,执行次数是不同的,因此,考虑时间复杂度是按照使基本操作执行次数最多的输入来计算的,即所谓的最坏时间复杂度。
实例
题目:
最大子列和问题
给定K个整数组成的序列{ N1, N2, ..., NK },“连续子列”被定义为{ Ni, Ni+1, ..., Nj },其中 1≤i≤j≤K。“最大子列和”则被定义为所有连续子列元素的和中最大者。例如给定序列{ -2, 11, -4, 13, -5, -2 },其连续子列{ 11, -4, 13 }有最大的和20。现要求你编写程序,计算给定整数序列的最大子列和。
本题旨在测试各种不同的算法在各种数据情况下的表现。各组测试数据特点如下:
- 数据1:与样例等价,测试基本正确性;
- 数据2:102个随机整数;
- 数据3:103个随机整数;
- 数据4:104个随机整数;
- 数据5:105个随机整数;
输入格式:
输入第1行给出正整数K (≤100000);第2行给出K个整数,其间以空格分隔。
输出格式:
在一行中输出最大子列和。如果序列中所有整数皆为负数,则输出0。
输入样例:
6
-2 11 -4 13 -5 -2
输出样例:
20
解决算法:
1.暴力求解法
/*
* 列举每个子序列(都是从左边界到右边界求和),求解完不断更新最大子序列和
*/
void MaxSubsequSum1(int a[], int length)
{
int thisSum = 0,maxSum = 0;
int i,j,k;
for(i = 0; i < length; i++)
{
for(j = i; j < length; j++)
{
thisSum = 0;
for(k = i; k <= length; k++)
{
thisSum +=A[k];
if(thisSum > maxSum)
{
maxSum = thisSum;
}
}
}
}
printf("%d",maxSum);
}
可以看出算法里有三层循环,循环的结束条件都是当循环控制变量增长到数组长度时结束,如果数组长度设为n,那么此算法规模就是,所以。
2.类N!方法
/*
* 从左边开始求以左边开始的所有子序列和的最大值,然后不断将左边界右移,重复求最大值
*/
void MaxSubsequSum1(int a[], int length)
{
int thisSum = 0,maxSum = 0;
int i,j;
for(i = 0; i < length; i++)
{
thisSum = a[i];
for(j = i+1; j < length; j++)
{
thisSum += a[j];
if(thisSum > maxSum)
{
maxSum = thisSum;
}
}
}
printf("%d",maxSum);
}
规模:
时间复杂度:
3.分而治之
/*
* 所谓分治法:将一个问题的求解过程分解为两个大小相等的子问题进行求解,如果分解后的子问题本身也可
* 以分解的话,则将这个分解的过程进行下去,直至最后得到的子问题不能再分解为止。
* ->求最大子序列和,我们可以将求最大子序列和的序列分解为两个大小相等的子序列,然后在这两个大小相
* 等的子序列中,分别求最大子序列和,如果由原序列分解的这两个子序列还可以进行分解的话,进一步分
* 解,直到不能进行分解为止,使问题逐步简化,最后求最简化的序列的最大子序列和,沿着分解路径逐步回
* 退,合成为最初问题的解。
* 1.序列的左半部分的最大子序列和
* 2.序列的右半部分的最大子序列和
* 3.横跨序列左半部分和右半部分得到的最大子序列和
* 4.比较3者求最大值
* 算法中左半边和右半边最大序列和通过递归求得的。
* 算法中跨边界序列和是用从中间分点向两边叠加,得到最大值。
*/
int MaxSubsequSum2(int a[], int left, int right)
{
if(left == right)
{
return a[left];
}
//先分
int center = (left+right)/2;
int maxLeftSum = MaxSubsequSum2(a, left, center);
int maxRigthSum = MaxSubsequSum2(a, center+1, right);
int maxLeftBorderSum = 0 , leftBorderSum = 0;
//处理左序列
for(int i = center; i >= left; --i)
{
leftBorderSum += a[i];
if(leftBorderSum > maxLeftBorderSum)
maxLeftBorderSum = leftBorderSum;
}
//处理右序列
int maxRightBorderSum = 0, rightBorderSum = 0;
for(int j=center+1; j <= right; j++)
{
rightBorderSum += a[j];
if(rightBorderSum > maxRightBorderSum)
maxRightBorderSum = rightBorderSum;
}
int temp = maxLeftSum>maxRigthSum?maxLeftSum:maxRigthSum;
if(temp >= maxLeftBorderSum+maxRightBorderSum)
return temp;
else
return maxLeftBorderSum+maxRightBorderSum;
}
分析:左半边迭代规模是T(N/2)(因为把左半边的每个元素都访问一遍),中间跨边界序列和最大规模是T(N)(将整个序列都访问一遍)右半边迭代规模同左边T(N/2)
计算: 将T(N/2)迭代进T(N)一次。
其中 忽略,所以时间复杂度为
4.在线处理
/*
* 在线处理的思想就是从左边界开始循环向右求和,一旦碰到当前是负数或者序列和为负数就放弃,不断更新正
* 的最大序列和即为最大子序列和。
*/
int MaxSubsequSum3(int a[], int length)
{
int ThisSum = 0,MaxSum = 0;
for(int i = 0; i < length; i++)
{
ThisSum += a[i];
if(ThisSum < 0)
{
ThisSum = 0;
}else if(ThisSum > MaxSum)
{
MaxSum = ThisSum;
}
}
return MaxSum;
}
规模:n(因为只访问了一遍整个序列)
时间复杂度:
具体性能测试
可以看出时间复杂度不同的算法,处理问题的效率真的差别很大。
总结
时间复杂度可以很方便的看出今后的数据结构各种算法的效率,其实时间复杂度在我们平时自己写程序的模块函数时都能使用到,多尝试用时间复杂度计算,一眼看出算法优劣你也可以。(以后工作Boss让优化算法也是把算法时间复杂度朝着nlogn甚至是n努力!)