算法-时间复杂度
1、数据结构-算法
数据结构和算法是相辅相成的,数据结构是为算法服务的,算法要作用在特定的数据结构之上。所以二者的学习不能互相孤立。
2、算法复杂度分析
- 时间复杂度: 指算法需要消耗的时间资源。计算机算法是问题规模n 的函数f(n),算法的时间复杂度也因此记做。 T(n)=Ο(f(n))
- 空间复杂度:算法需要消耗的空间资源。其计算和表示方法与时间复杂度类似,一般都用复杂度的渐近性来表示。同时间复杂度相比,空间复杂度的分析要简单得多。
2.1、事后统计法
事后统计法:通过统计、分析等,计算出算法的实际执行时间和实际内存占用,从而得到算法的时间复杂度。
- 测试结果非常依赖测试环境。(服务器资源配置)
- 测试结果受限于数据规模
2.2、大O复杂度表示法
时间复杂度通俗来讲,就是代码运行的时间。大O表示法就是在不运算代码的情况下来分析一段代码的时间复杂度。
看下面的例子:
public void calculate(int n){
int sum = 0;
int i = 0;
for (;i<n;++i){
sum = sum + i;
}
}
从CPU的角度来看,代码的每一行都执行着类似的操作:读操作-运算-写操作。这里我们假设每行代码的执行时间一样,为unit_time,那么2,3行执行了1个unit_time,3,4行n个unit_time,总的执行时间就是(2n+2)*unit_time。
按照这个思路再来看下面一段代码:
public void cal(int n){
int sum = 0;
int i = 0;
int j = 0;
for (;i<n;i++){
j = 1;
for (;j<=n;j++){
sum += sum + i * j;
}
}
}
依旧假设单位时间为unit_time,2、3、4行执行时间分别为1unti_time,5、6加起来就是2n。7、8加起来就是2n²unit_time;总的时间就为(2n²+2n+3)unit_time.
这两段代码分析我们可得到一个重要的规律,那就是代码的执行时间T(n)和每段代码的执次数成正比,所以就得到了以下公式:
T(n)=O(f(n))
其中T(n)代表了代码的执行时间,n代表数据规模,f(n)代表每行代码执行的次数总和。因为是公式,所以用f(n)表达。公式中的O则表示代码执行时间T(n)和表达式f(n)成正比。
两个例子中的T(n)=O(2n+2)和T(n)=O(2n²+2n+3)就是大O时间复杂度表示法,它并不代表具体的执行时间,而是代表代码的执行时间随着数据规模增加的变化趋势,所以也叫渐进时间复杂度(asymptotic time complexity),简称时间复杂度。
当n很大时,表达式中的低阶、常量、系数等部分并不左右增长趋势,所以大O表示法可记为:T(n)=O(n)、T(n)=O(n²)。
2.3 常见复杂度分析方法
- 单段代码看高频:如循环
public void calculate(int n){
int sum = 0;
int i = 0;
for (;i<n;++i){
sum = sum + i;
}
}
- 多段代码取最高:如一段代码中存在单重循环和多重循环,则取多重循环。
public void cal(int n){
int temp = 0;
int a = 0;
for (;a<n;++a){
temp = temp + i;
}
int sum = 0;
int i = 0;
int j = 0;
for (;i<n;i++){
j = 1;
for (;j<=n;j++){
sum += sum + i * j;
}
}
}
- 嵌套循环取乘积:比如多重循环,递归等。
public void cal(int n){
int sum = 0;
int i = 0;
int j = 0;
for (;i<n;i++){
j = 1;
for (;j<=n;j++){
sum += sum + i * j;
}
}
}
- 多数据规模取和:O(n+m)。
public void cal(int n,int m){
int temp = 0;
int a = 0;
for (;a<n;++a){
temp = temp + i;
}
for (;i<m;i++){
sum += sum + i * j;
}
}
2.4 常见的时间复杂度

- 多项式阶,随着数据规模的增长,算法的执行时间和空间占用,按照多项式的比例增长。
- O(1)(常数阶)
常数阶的概念并不是只执行一行代码,一般情况下算法中不存在循环、递归等语句,即使有成千上万行代码,复杂度也是O(1) - O(logn)(对数阶)
当2^x>=n时,循环结束,所以这段代码的执行次数就是x= ;大O法记作:。那么再看下面这段代码:int i =1; while(i<n){ i = i * 2; }按照上面的逻辑,这段的复杂度为。二者之间是可以转换的;基于大O法的理论:在采用大O计数法时可以忽略系数等。则此类复杂度可统一记为O(logn)。int i =1; while(i<n){ i = i * 3; } - O(n)(线性阶)
- O(nlogn)(线性对数阶)
基于O(logn),那么O(nlogn)就容易理解了,就相当于上面的嵌套取乘积规则一样,就是O(logn)执行的n遍。 - O(n^2)(平方阶)、O(n^3)(立方阶)....、O(n^k)(K次方阶)
- 非多项式阶:随着数据规模的增长,算法的执行时间和空间占用暴增,这类算法性能极差。
- O(2^n)(指数阶)
- O(n!)(阶乘阶)
3 小结
虽然上面说的事后统计法有一定的缺点,但时间复杂度的分析与其并不是对立冲突的。时间复杂度是一个理论上的模型,可以比较直观的给我们一个算法的效率上的感性认知,只能提供粗略的分析。它是与宿主平台无关的,并不是说O(n)的效率一定就优于O(n²)。针对不同的宿主平台环境,不同的数据集,不同的数据规模,在实际应用上性能可能会各有不同,所以实际应用中进行一定的性能基准测试是有必要的。
综上所述,时间复杂度分析和性能测试是相辅相成的。但一个低阶的时间复杂度确实有极大的可能优于高阶的时间复杂度,所以在编程中时刻关心复杂度的趋势走向是很有必要的,而且能很大幅度的提升输出质量。因此在编程中具有这种复杂度分析的思维还是十分有必要的。