1. 大 O 表示法
大 O
表示法指出了算法有多快,让你能够比较操作数,它指出了算法运行时间的增速,而并非以秒为单位的速度。大 O
表示法指出了最糟情况下的运行时间。大 O
表示法在讨论运行时间时,log
指的都是 log2
。
2. 复杂度概念
复杂度是一个关于输入数据量 n
的函数。假设你的代码复杂度是 f(n)
,那么就用个大写字母 O
和括号,把 f(n)
括起来就可以了,即 O(f(n))
。例如,O(n)
表示的是,复杂度与计算实例的个数 n
线性相关;O(logn)
表示的是,复杂度与计算实例的个数 n
对数相关。
通常,复杂度的计算方法遵循以下几个原则:
- 复杂度与具体的常系数无关;
例如 O(n)
和 O(2n)
表示的是同样的复杂度。我们详细分析下,O(2n)
等于 O(n+n)
,也等于 O(n) + O(n)
。也就是说,一段 O(2n)
复杂度的代码只是先后执行两遍 O(n)
,其复杂度是一致的。
- 多项式级的复杂度相加的时候,选择高者作为结果;
例如 O(n²)+O(n)
和 O(n²)
表示的是同样的复杂度。具体分析一下就是,O(n²)+O(n) = O(n²+n)
。随着 n
越来越大,二阶多项式的变化率是要比一阶多项式更大的。因此,只需要通过更大变化率的二阶多项式来表征复杂度就可以了。
O(1)
也是表示一个特殊复杂度,含义为某个任务通过有限可数的资源即可完成。此处有限可数的具体意义是,与输入数据量n
无关。
例如,你的代码处理 10 条数据需要消耗 5 个单位的时间资源,3 个单位的空间资源。处理 1000 条数据,还是只需要消耗 5 个单位的时间资源,3 个单位的空间资源。那么就能发现资源消耗与输入数据量无关,就是 O(1)
的复杂度。
3. 常见复杂度
常见的复杂度结论:
- 一个顺序结构的代码,时间复杂度是
O(1)
; - 二分查找,或者更通用地说是采用分而治之的二分策略,时间复杂度都是
O(logn)
; - 一个简单的
for
循环,时间复杂度是O(n)
; - 两个顺序执行的
for
循环,时间复杂度是O(n)+O(n)=O(2n)
,其实也是O(n)
; - 两个嵌套的
for
循环,时间复杂度是O(n²)
;
下面按从快到慢的顺序列出了经常会遇到的 5 种时间复杂度:
-
O(log n)
,也叫对数时间,这样的算法包括二分查找; -
O(n)
,也叫线性时间,这样的算法包括简单查找; -
O(n*logn)
,这样的算法包括快速排序 —— 一种速度较快的排序算法; -
O(n2)
,这样的算法包括选择排序 —— 一种速度较慢的排序算法; -
O(n!)
,这样的算法包括旅行商问题的解决方案 —— 一种非常慢的算法;
3.1 O(1)
比如操作一步到位这是常数级别的时间复杂,即为 O(1)
。
a = 100 / 2 + 101 * 10 + 5
3.2 O(logn)
如下操作,每次 n
会被除以 3,遍历次数等于以 3 为底 logn
次。在大 O
标记体系中,以 3 为底和以 5 为底没有区别,所以统一标记为 O(logn)
。
def f(n):
while n > 0:
print(n)
n /= 3
3.3 o(n)
线型复杂度也是很理想的情况,如下面的操作,每次遍历,n
减去 3,这样总共遍历 (n-1)/3 + 1
次后算法终止,根据大 O
标记法,算法的时间复杂为 O(n)
。
def f(n):
while n > 0:
print(n)
n -= 3
3.4 O(nlogn)
如下面所示的两层循环,复杂度便是 nlogn
。外层循环的运行次数为 n
,里层循环的运行次数为 logn
次,所以一共需要 nlogn
次。
def f(n):
for i in range(n):
n /= 2
for j in range(1,n):
print(i*j)
3.5 O(n^2)
O(n^2)
是多项式时间复杂度的代表,此类算法的时间复杂度已经难以划分到高效算法集合中,它只能是问题的有效解,而不是高效解。如下两层for
循环,时间复杂度就是 O(n^2)
。
def f(n):
for i in range(n):
for j in range(n):
print(i*j)
3.6 O(2^n)
时间复杂度为 O(2^n)
的算法是指数级增长的,此类复杂度下求解的问题往往都是难题,因为随着问题规模 n
的增长,指数级的增长速度是惊人的。
例如经典的旅行商问题,商人要去 n
个地方拜访,如何规划拜访顺序才能使得旅行距离最短。如果仅拜访肉眼可见的两三个地方时,我们还能穷举所有拜访的组合,进而找到最短路径。
但是当问题规模 n
变大时,目前所有的计算机资源总和都难以在有限的时间里计算出最优的最短路径,这类问题的时间复杂度都为指数级,属于 NP
难问题。