算法分析#
对算法所需要的计算机资源(时间或空间)进行估算分析
时间复杂性(time complexity)#
基本语句的执行次数*单次执行时间
以下面这一段代码为例
分析这段代码中存在的操作,基本操作指的是单个执行步骤,在此例中可以理解为程序中的关键计算步骤。例如,赋值、比较、数组访问等。这些操作应该用来估计程序的运行时间。
| 基本操作 | 次数 | 执行时间 |
|---|
| 声明变量 | 2次 | 2 t1 |
| 变量赋值 | 2次 | 2 t2 |
| 比较操作< | N+1次 | (N+1)t3 |
| 比较操作== | N次 | Nt4 |
| 数组访问操作 | N | Nt5 |
| ++操作 | N到2N次 | Nt6∼2Nt6 |
再看一个例子:
for (int i=0; i<N; i++) {
for (int j=i+1; j<N; j++) {
| 基本操作 | 次数 | 执行时间 |
|---|
| 声明变量 | N+2 | (N+2)t1 |
| 变量赋值 | 2N+2 | (2N+2)t2 |
| 比较操作< | 2N(N+1)+N+1 | (2N(N+1)+N+1)t3 |
| 比较操作== | 2N(N−1) | 2N(N−1)t4 |
| 数组访问操作 | N(N−1) | N(N−1)t5 |
| ++操作 | 2N(N−1)\sim N(N-1) | 2N(N−1)t6 |
在进行时间复杂度分析时,我们关注的是随着输入规模 N 增长而增加的操作次数,对于与 N 无关的常数项,或者与N成线性相关的项,我们可以忽略。
因此保留下来的操作
| 基本操作 | 次数 | 执行时间 |
|---|
| 比较操作< | 2N(N+1) | (2N(N+1))t3 |
| 比较操作== | 2N(N−1) | 2N(N−1)t4 |
| 数组访问操作 | N(N−1) | N(N−1)t5 |
| ++操作 | 2N(N−1)∼N(N−1) | 2N(N−1)t6 |
这些操作称为基本语句,基本语句的执行次数应该与算法的时间代价相当
时间代价函数#
时间复杂性分析的目标是将算法的时间代价表示为一个函数 f(N)(有的也记作 T(N)),其中 N 代表输入规模(输入规模不是数值的大小,而是存储输入需要的位数),用基本语句的次数(而非对应操作的具体时间)来衡量时间复杂性。
以上面这个代码为例,算一下 f(N)
f(N)=(2N(N+1))t3+2N(N−1)t4+N(N−1)t5+(2N(N−1)∼N(N−1))t6忽略基本语句对应操作的时间 ti,用基本语句的执行次数来近似算法的时间代价
f(N)=2N(N+1)+2N(N−1)+N(N−1)+(2N(N−1)∼N(N−1))=25N2−21N∼3N2−N进一步,忽略低阶项和常数项,得到f(N)的渐近表示,在这里引入大O符号(Big O Notation),表示至多同阶。
f(N)=O(N2)渐进表示符号#
渐进表示符号
渐进表示符号其实是一个数学概念,形式化定义如下:
-
大O符号(Big O Notation)对于给定的函数 g(n) ,用 O(g(n)) 表示函数 f(n) 的集合,并且称 g(n) 是 f(n) 的一个渐进上确界
f(n)=O(g(n))⟺∃c>0,n0>0,∀n≥n0,0≤f(n)≤c⋅g(n)
-
大 Ω 符号(Big Omega Notation):对于给定的函数 g(n),用 O(g(n)) 表示函数 f(n) 的集合,并且称 g(n) 是 f(n) 的一个渐进下确界
f(n)=Ω(g(n))⟺∃c>0,n0>0,∀n≥n0,0≤c⋅g(n)≤f(n)
-
大 Θ 符号(Big Theta Notation):对于给定的函数 g(n),用 Θ(g(n)) 表示函数 f(n) 的集合,并且称 g(n) 是 f(n) 的一个渐进紧确界
f(n)=Θ(g(n))⟺∃c1>0,c2>0,n0>0,∀n≥n0,0≤c1⋅g(n)≤f(n)≤c2⋅g(n)
-
小o符号表示 f(n) 的增长率严格小于 g(n)
f(n)=o(g(n))⟺∀c>0,∃n0>0,∀n≥n0,0≤f(n)<c⋅g(n)
-
小 ω 符号表示 f(n) 的增长率严格大于 g(n)
f(n)=ω(g(n))⟺∀c>0,∃n0>0,∀n≥n0,0≤c⋅g(n)<f(n)
- 这里的等号不是严格的数学等号,而是类似 ∈ 的一种符号
- 这里面的常数 c,c1,c2 和 n0 都是正数,函数 f(n) 和 g(n) 都是非负函数(严格来说叫渐进非负,就是当n足够大时满足非负)
- 确定常数 c,c1,c2 和 n0 的的过程依赖于具体的函数 f(n) 和 g(n)
- 小o符号和小 ω 符号,表示严格小于和严格大于,注意这两个定义中 g(n) 的倍数因子 c 满足任意性:
在算法分析中,常用以下三种符号来表示代价函数在输入规模达到一定程度后渐近增长行为:
-
大O符号(至多同阶),若m次多项式 A(n)=amnm+am−1nm−1+⋯+a1n+a0 ,则 A(n)=O(nm)
- 忽略低阶项和常数项,关注输入规模 N 增长
- 可以把 O(Nn) 理解成阶数不超过n的函数族或多项式集合
- O(nm) 描述代价函数的最坏情况,阶数越低代价函数增长的上界越精确
-
大 Ω 符号(至少同阶),若m次多项式 A(n)=amnm+am−1nm−1+⋯+a1n+a0 ,则 A(n)=Ω(nm)
- Ω(nm) 描述代价函数阶数的最好情况,阶数越高代价函数增长下界越精确
-
大 Θ 符号(同阶),若m次多项式 A(n)=amnm+am−1nm−1+⋯+a1n+a0 ,则 A(n)=Θ(nm)
- 这里的等号类似 ∈,把 Θ(Nn) 理解成阶数等于n的函数族或多项式集合
- Θ(nm) 表示代价函数的增长率与 c0nm∼c1nm 一样快
- 如果 f(n)=O(g(n)) 且 f(n)=Ω(g(n)) ,则 f(n)=Θ(g(n))
例题:求冒泡排序的时间复杂度
void bubblesort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
solution
主要是两层嵌套循环,外层i从0到n-2,内层j从0到n-i-2,外层执行n-1次,内层执行
(n−1)+(n−2)+⋯+1=2(n−1)n次,所以时间复杂度为O(n2)
例二:求阶乘的时间复杂度
return n * factorial(n - 1);
solution
递归调用n次,所以时间复杂度为 O(n)
例三:二分法查找的复杂度
int binarySearch(int arr[], int size, int target) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
} else if (arr[mid] < target) {
solution
每次循环将查找范围缩小一半,假设初始范围为n,经过k次循环后,范围变为 2kn ,当范围缩小到1时,停止循环,即 2kn=1 ,解得 k=log2n ,所以时间复杂度为 O(logn) 这种“每次操作都将问题规模缩小一半”的算法,复杂度通常都是 O(logn) 。
空间复杂性#
空间复杂性是指算法在运行过程中需要的内存空间,包括变量、数据结构、函数调用栈等所占用的空间。