「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
定义
算法:独立存在的解决问题的方法或者思想。
特性
- 输入:有 0 个或者多个输入(我们想解决问题,所以可能会有需要用到的数据输入)
- 输出:至少 1 个或者多个输出(算法解决问题肯定会有答案)
- 有穷性:在有限的步骤之后会自动结束而不会无限循环,并且每个步骤都在可接受的时间内完成(有限的步骤很好理解,但是后半句话也很有用,假如你一共执行三步,每一步需要 1000 年,那谁能等得上)
- 确定性:每一步骤都有确定的含义,不会出现二义性(最简单的理解就是每个步骤有确定的含义,把这个步骤给不同的人看,理解出的内容是一样的)
- 可行性:每一步都是可行的,也就是每一步能够执行有限的次数完成,并且能实现出来
案例
i+j+k=1000, 且 i^2+j^2=k^2,求 i、j、k 所有可能的组合?
代码一,直接按照题意,三重循环
import time
start = time.time()
for i in range(1,1001):
for j in range(1,1001):
for k in range(1,1001):
if i+j+k==1000 and i*i+j*j==k*k:
print(i,j,k)
print(time.time()-start)
打印:
200 375 425
375 200 425
214.20414304733276
代码二,稍微改动一下,根据题意将 k=1000-i-j 直接算出,省了一层的循环
start = time.time()
for i in range(1,1001):
for j in range(1,1001):
k = 1000-i-j
if i*i+j*j==k*k:
print(i,j,k)
print(time.time()-start)
打印:
200 375 425
375 200 425
0.5493438243865967
时间复杂度
思考:虽然我们能从直接的代码执行时间的长短来判断代码一和代码二两者之间,后者算法的执行时间更短,但是这就能说明它的算法效率更好吗?
答案:肯定不是的,我们的算法要考虑运行环境,假如我们用一个 80 年代的电脑运行代码二,它肯定比我用 2021 年的电脑运行代码一的时间更长,所以光从运行时间的长短来衡量代码的运行效率是不科学的,我们要定义一种能在隔离运行环境的基础上来理论地衡量算法运行效率,也就是时间复杂度。
为了理解时间复杂度,我们分步骤进行抽象:
-
第一步:我们可以看出代码一中有三个循环,最内层的代码有两个语句,大体看作是一个 if 语句和一个 print 语句,从语句的运算数量来看有 2*1000^3 ;代码二种有两个循环,最内层的代码也可以看做两个语句,一个 if 语句和一个 print 语句,从语句的运算数量来看有 2*1000^2 。
-
第二步:如果题目中将 1000 换成 2000 等数字,两个代码中的语句运算次数分别变成了 2*2000^3 和 2*2000^2 。
-
第三步:这里的“1000”可以看做是一个计算数据的规模,经过抽象可以看做是 n ,此时两个代码的运算数量分别为 2*n^3 和 2*n^2 。
-
第四步:尽管我们将一个 if 语句和一个 print 语句看做 2 次的执行次数,但是由于两个代码中的 if 和 print 语句的细节不同,所相差的时间不方便计算且对于大局无关轻重,我们只需要知道算法大体的运行量级或者趋势即可,所以两个代码省略了两个语句操作之后,运算数量为 n^3 和 n^2。就像上面运算的结果,一个是 200 多秒,一个是 0.5 秒,我们一看就知道这两个算法不在一个量级,多或者少个几秒根本不影响算法性能结果。
-
第五步:此时我们用 O 表示时间复杂度,分别为 O(n^3) 和 O(n^2)。
三种时间复杂度
假如我们有一个整数列表要排列成有序的,因为列表内的整数的混乱程度不同,我们在使用同一个算法 S 的时间复杂度是不一样的,有三种情况:
- 可能列表本身是有序的,此时我们是使用 S 的时间为最短的,被称为最优时间复杂度。
- 如果列表本身是最混乱的状态,那我们是使用 S 的时间为最长的,被称为最坏时间复杂度。
- 另外在这两者之间的时间复杂度,我们通常称为平均时间复杂度,它是对算法的一个客观全面的评价。
时间复杂度的基本计算规则
- 常数次数的基本操作时间复杂度为 O(1)
- 顺序结构的时间复杂度按加法进行计算
- 循环结构的时间复杂度按乘法进行计算
- 分支结构的时间复杂度取较大值
- 只关注操作次数的最高次项,忽略次要项和常数项
- 通常我们说的时间复杂度都为最坏的
例如上面的代码二,完整的语句操作次数大致抽象为:
n^2*(1+max(1,0))
n^2 表示的是二层循环,1 是 k = 1000-i-j 操作次数,max 是为了取 if 判断分支的最大值,要么就是执行 1 次 print 语句,要么就是不执行任何语句。然后去掉次要项和常数项,就得到了时间复杂度
O(n^2)
常见的时间复杂度举例
| 执行次数 | 时间复杂度 |
|---|---|
| 12 | O(1) |
| 2n+3 | O(n) |
| 3n^2+2n+1 | O(n^2) |
| 5logn+20 | O(logn) |
| 2n+3nlogn+19 | O(nlogn) |
| 6n^3+2n^2+3n+4 | 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)
timeit 模块
python 语言中的 timeit 模块可以用来测试 python 代码的执行速度。
import timeit
print(timeit.timeit('"-".join(str(n) for n in range(100))', number=10000))
print(timeit.timeit('"-".join([str(n) for n in range(100)])', number=10000))
打印:
0.8090579620002245
0.40545224999732454