时间复杂度
在我们进行编程学习的初期,肯定编写过一个程序是求1+2+3+…+100,C语言代码为
#include<stdio.h>
int main(void){
int i, sum = 0, n = 100;
for(i = 1; i <= n; i++){
sum = sum + i;
}
printf("%d", sum);
}
但是我们也可以使用等差数列求和来进行对100个连续整数求和的解
#include<stdio.h>
int main(void){
int sum = 0, n = 100;
sum = (1 + n) * n / 2;
printf("%d", sum);
}
算法的定义
算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且没条指令表示一个或多个操作
在上面的例子可以看出,对于给定的问题,可以由多种算法来进行解决
算法的特性
- 输入输出
算法有0个或多个输入,对大多数算法来说,输入都是必要的,但是在打印“hello world”这样的语句时,不需要输入参数,所以算法的输入可以是0个
算法至少有一个或多个输出,算法一定要有输出,不需要输出,算法就没有意义了
- 有穷性
有穷性是指算法在执行有限的步骤后,自动结束而不会出现无限循环,并且每一个步骤都在可以接受的时间内完成,当然实际运用中的有限是指合理的可以接受的,不然一个算法计算机计算了50年,虽然计算出了结果,但是时间消耗太多了,意义不大
- 确定性
确定性要求算法的每一条执行步骤都有意义,不会出现二义性
- 可行性
算法的每一步都必须是可行的,每一步都能通过执行有限的次数完成
算法设计的要求
- 正确性
算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性、能正确反映问题的需求、能够得到问题的正确答案
算法的正确大体分为以下四个层次:
1.算法程序没有语法错误
2.算法程序对于合法的输入数据能够产生满足要求的输出结果
3.算法程序对于非法的输入数据能够得出满足规格说明的结果
4.算法程序对于进行精心选择的,甚至刁难的测试数据都有满足要求的输出结果
我们一般将层次3作为一个算法是否正确的标准
- 可读性
可读性是算法设计的另外一个目的,可以便于阅读、理解和交流
- 健壮性
健壮性:当输入数据不合法时,算法也能做出相应的处理,而不是产生异常或者莫名其妙的结果
- 时间效率高和存储量低
算法效率的度量方法
事后统计法
这种方法主要通过设计好的测试程序和数据,利用计算机计时器对不同的算法编制的程序运行时间进行比较,从而确定算法效率的高低,但是使用此统计方法存在一些缺陷:
- 必须依照算法事先编制好程序,这通常需要消耗大量的时间和精力,如果编制出来发现很糟糕,那么之前的工作就白费
- 时间的比较比较依赖计算机硬件和软件等环境因素,有时会掩盖算法本身的优劣,因为现在的计算机和几十年的老爷爷辈的机器相比,在处理算法的运行速度上,是不能相提并论的,就算是同一台机器,CPU使用率和内存占用情况不一样也会导致细微的差异
- 算法测试数据设计困难,而且程序的运行时间往往还与测试数据的规模有关
事前统计法
事前统计法就是在计算机编制程序前,依据统计方法对算法进行估算
经分析,我们发现,一个程序在计算机上运行消耗的时间取决于下列因素:
- 算法采用的方法
- 编译产生的代码量
- 问题的输入规模
- 机器执行指令的速度
第1条是算法好坏的根本,第2条由软件来支持,第4条取决于硬件性能,抛开软硬件性能的影响,一个程序的运行时间依赖于算法的好坏和问题的输入规模
在这里我们只考虑输入规模对运行消耗时间的影响
我们看看之前举过的例子
第一种算法:
#include<stdio.h>
int main(void){
int i, sum = 0, n = 100; //执行了1次
for(i = 1; i <= n; i++){ //执行了n+1次
sum = sum + i; //执行了n次
}
printf("%d", sum); //执行了1次
}
第一种算法执行了2n+3次
第二种算法:
#include<stdio.h>
int main(void){
int sum = 0, n = 100; //执行了1次
sum = (1 + n) * n / 2; //执行了1次
printf("%d", sum); //执行了1次
}
第二种算法执行了3次
我们忽略循环的索引的递增、递减,循环的终止条件判断、变量声明、打印结果、赋值等,对关键的基本操作的执行次数进行统计,那么第一种算法和第二种算法就是n和1的区别。
因为对消耗时间的基本操作的执行次数的统计可以大概估算出程序运行时间,我们可以认为计算机每执行一次基本操作都会消耗相同的时间,那么通过对基本操作执行次数的统计就可以比较两种算法的优劣
我们用计算机分别运行两种算法,可能因为我们将规模n变量设置为了100,所以两种算法运行结束的时间肯定不相上下,但是如果我们将n设置为百万、千万级别我们就可以看到两个算法的明显差别,第一种算法明显比第二种算法要慢好多
我们对之前的算法进行改进
#include<stdio.h>
int main(void){
int i, j, x = 0, sum = 0, n = 100;
for(i = 1; i <= n; i++){
for(j = 1; j <= n; j++){
x++;
sum = sum + x;
}
}
printf("%d", sum);
}
这个程序实际上执行的是从1+2+3+…10000的命令,循环体实际上执行的次数是n*n次,和之前两种算法相比,虽然规模n都为100,但是改进后的算法实际上消耗的时间要比之前的两种算法长很多。
不同算法的操作数量对比:
可以看到随着规模n越来越大,他们在时间效率上的差异也越来越大
函数的渐进增长
1.假设有算法一和算法二,两个算法的输入规模都是n,算法一做2n+3次操作,可以理解为先做了一个n次的for循环,循环完成后执行了两次赋值语句;算法二做3n+1次操作,可以理解为在一个n次的for循环中有两条基本操作的语句
我们将算法的规模和执行的次数之间的表格列出来
次数 | 算法一(2n+3) | 算法一’(2n) | 算法二(3n+1) | 算法二’(3n) |
---|---|---|---|---|
n=1 | 5 | 2 | 4 | 3 |
n=2 | 7 | 4 | 7 | 6 |
n=3 | 9 | 6 | 10 | 9 |
n=10 | 23 | 20 | 31 | 30 |
n=100 | 203 | 200 | 301 | 300 |
当n=1,算法一不如算法二,当n>2时,算法一优于算法二,随着n的增加,算法一越来越好了
算法的渐进增长:给定两个函数f(n)和g(n),如果存在一个整数N使得对于所有的n>N,f(n)总是比g(n)大,那么我们说f(n)的增长渐进快于g(n)
我们发现,随着n的增大,算法一和算法二后面的+3还是+1其实不影响算法变化,我们可以忽略这些常数
2.我们再假设两个算法:算法三和算法四
次数 | 算法三(4n+8) | 算法三’(n) | 算法四(3n*n+1) | 算法四’(n*n) |
---|---|---|---|---|
n=1 | 12 | 1 | 3 | 1 |
n=2 | 16 | 2 | 9 | 4 |
n=3 | 20 | 3 | 19 | 9 |
n=10 | 48 | 10 | 201 | 100 |
n=100 | 408 | 100 | 20001 | 10000 |
n=1000 | 4008 | 1000 | 2000001 | 1000000 |
我们发现去掉和最高项相乘的常数后,对算法时间效率好坏的判断的结果并没有太大影响,所以和最高项相乘的常数可以直接去掉
3.我们再来假设两个算法:
算法五是2nn,算法六是3n+1,算法七是2nn+3n+1
次数 | 算法五(2nn) | 算法六(3*n+1) | 算法七(2nn+3*n+1) |
---|---|---|---|
n=1 | 2 | 4 | 5 |
n=2 | 8 | 7 | 15 |
n=5 | 50 | 16 | 66 |
n=10 | 200 | 31 | 231 |
n=100 | 20000 | 301 | 20301 |
n=1000 | 2000000 | 3001 | 2003001 |
n=10000 | 200000000 | 30001 | 200030001 |
n=100000 | 20000000000 | 300001 | 20000300001 |
n=1000000 | 2000000000000 | 3000001 | 2000003000001 |
当n越来越大时,算法六和算法五相差越来越大,最后可以忽略不计,所以算法五最终趋近与算法七,所以在我们分析一个算法的时间效率时我们就可以忽略函数中的常数项和其他次项,更应该关注最高次项
通过对上面知识的讲解,我们就可以对时间复杂度进行讲解
算法时间复杂度
算法时间复杂度的定义:
在进行算法分析时,语句总的执行总次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况而确定T(n)的数量级,也就是O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称为时间复杂度,f(n)是问题规模n的某个函数,我们这里把它当做计算机的执行次数
上面的定义中也 讲解了大O记法来计算时间时间复杂度的方法
推导大O阶方法:
- 用常数1代替运行次数表达式中的所有加法常数
- 只保留最高次项
- 去掉和最高项相乘的常数
得到的结果就是大O阶
后续补充