1677 字
8 分钟
数据结构与算法-复杂度分析

算法分析#

对算法所需要的计算机资源(时间或空间)进行估算分析

时间复杂性(time complexity)#

基本语句的执行次数*单次执行时间

以下面这一段代码为例

int count = 0
for (int i=0,i<N,i++)
if(a[i]==0)
count++;

分析这段代码中存在的操作,基本操作指的是单个执行步骤,在此例中可以理解为程序中的关键计算步骤。例如,赋值、比较、数组访问等。这些操作应该用来估计程序的运行时间。

基本操作次数执行时间
声明变量2次2 t1t_1
变量赋值2次2 t2t_2
比较操作<N+1次(N+1)t3(N+1)t_3
比较操作==N次Nt4Nt_4
数组访问操作NNt5Nt_5
++操作N到2N次Nt62Nt6Nt_6\sim 2Nt_6

再看一个例子:

int count = 0;
for (int i=0; i<N; i++) {
for (int j=i+1; j<N; j++) {
if (a[i]+[j] == 0) {
count++;
}
}
}
基本操作次数执行时间
声明变量N+2(N+2)t1(N+2)t_1
变量赋值2N+2(2N+2)t2(2N+2)t_2
比较操作<N(N+1)2+N+1\frac{N(N+1)}{2}+N+1(N(N+1)2+N+1)t3(\frac{N(N+1)}{2}+N+1)t_3
比较操作==N(N1)2\frac{N(N-1)}{2}N(N1)2t4\frac{N(N-1)}{2}t_4
数组访问操作N(N1)N(N-1)N(N1)t5N(N-1)t_5
++操作N(N1)2\frac{N(N-1)}{2}\sim N(N-1)N(N1)2t6\frac{N(N-1)}{2}t_6

在进行时间复杂度分析时,我们关注的是随着输入规模 NN 增长而增加的操作次数,对于与 NN 无关的常数项,或者与NN成线性相关的项,我们可以忽略。

因此保留下来的操作

基本操作次数执行时间
比较操作<N(N+1)2\frac{N(N+1)}{2}(N(N+1)2)t3(\frac{N(N+1)}{2})t_3
比较操作==N(N1)2\frac{N(N-1)}{2}N(N1)2t4\frac{N(N-1)}{2}t_4
数组访问操作N(N1)N(N-1)N(N1)t5N(N-1)t_5
++操作N(N1)2N(N1)\frac{N(N-1)}{2}\sim N(N-1)N(N1)2t6\frac{N(N-1)}{2}t_6

这些操作称为基本语句,基本语句的执行次数应该与算法的时间代价相当

时间代价函数#

时间复杂性分析的目标是将算法的时间代价表示为一个函数 f(N)f(N)(有的也记作 T(N)T(N)),其中 NN 代表输入规模(输入规模不是数值的大小,而是存储输入需要的位数),用基本语句的次数(而非对应操作的具体时间)来衡量时间复杂性。

以上面这个代码为例,算一下 f(N)f(N)

f(N)=(N(N+1)2)t3+N(N1)2t4+N(N1)t5+(N(N1)2N(N1))t6f(N) = (\frac{N(N+1)}{2})t_3 + \frac{N(N-1)}{2}t_4 + N(N-1)t_5 + (\frac{N(N-1)}{2}\sim N(N-1))t_6

忽略基本语句对应操作的时间 tit_i,用基本语句的执行次数来近似算法的时间代价

f(N)=N(N+1)2+N(N1)2+N(N1)+(N(N1)2N(N1))=52N212N3N2Nf(N) = \frac{N(N+1)}{2} + \frac{N(N-1)}{2} + N(N-1) + (\frac{N(N-1)}{2}\sim N(N-1))\\[5pt] = \frac{5}{2}N^2 - \frac{1}{2}N \sim 3N^2 - N

进一步,忽略低阶项和常数项,得到f(N)f(N)渐近表示,在这里引入大O符号(Big O Notation),表示至多同阶。

f(N)=O(N2)f(N) = O(N^2)

渐进表示符号#

渐进表示符号

渐进表示符号其实是一个数学概念,形式化定义如下:

  1. 大O符号(Big O Notation)对于给定的函数 g(n)g(n) ,用 O(g(n))\mathbf{O}(g(n)) 表示函数 f(n)f(n) 的集合,并且称 g(n)g(n)f(n)f(n) 的一个渐进上确界

    f(n)=O(g(n))    c>0,n0>0,nn0,0f(n)cg(n)f(n) = \mathbf{O}(g(n)) \iff \exists c > 0, n_0 > 0, \forall n \geq n_0, 0 \leq f(n) \leq c \cdot g(n)
  2. Ω\Omega 符号(Big Omega Notation):对于给定的函数 g(n)g(n),用 O(g(n))\mathbf{O}(g(n)) 表示函数 f(n)f(n) 的集合,并且称 g(n)g(n)f(n)f(n) 的一个渐进下确界

    f(n)=Ω(g(n))    c>0,n0>0,nn0,0cg(n)f(n)f(n) = \Omega(g(n)) \iff \exists c > 0, n_0 > 0, \forall n \geq n_0, 0 \leq c \cdot g(n) \leq f(n)
  3. Θ\Theta 符号(Big Theta Notation):对于给定的函数 g(n)g(n),用 Θ(g(n))\mathbf{\Theta}(g(n)) 表示函数 f(n)f(n) 的集合,并且称 g(n)g(n)f(n)f(n) 的一个渐进紧确界

    f(n)=Θ(g(n))    c1>0,c2>0,n0>0,nn0,0c1g(n)f(n)c2g(n)f(n) = \Theta(g(n)) \iff \exists c_1 > 0, c_2 > 0, n_0 > 0, \forall n \geq n_0, 0 \leq c_1 \cdot g(n) \leq f(n) \leq c_2 \cdot g(n)
  4. 小o符号表示 f(n)f(n) 的增长率严格小于 g(n)g(n)

    f(n)=o(g(n))    c>0,n0>0,nn0,0f(n)<cg(n)f(n) = o(g(n)) \iff \forall c > 0, \exists n_0 > 0, \forall n \geq n_0, 0 \leq f(n) < c \cdot g(n)
  5. ω\omega 符号表示 f(n)f(n) 的增长率严格大于 g(n)g(n)

    f(n)=ω(g(n))    c>0,n0>0,nn0,0cg(n)<f(n)f(n) = \omega(g(n)) \iff \forall c > 0, \exists n_0 > 0, \forall n \geq n_0, 0 \leq c \cdot g(n) < f(n)
  • 这里的等号不是严格的数学等号,而是类似 \in 的一种符号
  • 这里面的常数 c,c1,c2c, c_1, c_2n0n_0 都是正数,函数 f(n)f(n)g(n)g(n) 都是非负函数(严格来说叫渐进非负,就是当n足够大时满足非负)
  • 确定常数 c,c1,c2c, c_1, c_2n0n_0 的的过程依赖于具体的函数 f(n)f(n)g(n)g(n)
  • 小o符号和小 ω\omega 符号,表示严格小于和严格大于,注意这两个定义中 g(n)g(n) 的倍数因子 cc 满足任意性

算法分析中,常用以下三种符号来表示代价函数输入规模达到一定程度后渐近增长行为:

  1. 大O符号(至多同阶),若m次多项式 A(n)=amnm+am1nm1++a1n+a0A(n)=a_mn^m+a_{m-1}n^{m-1}+\dotsb+a_1n+a_0 ,则 A(n)=O(nm)A(n)=\mathbf{O}(n^m)

    • 忽略低阶项和常数项,关注输入规模 NN 增长
    • 可以把 O(Nn)\mathbf{O}(N^n) 理解成阶数不超过n的函数族或多项式集合
    • O(nm)O(n^m) 描述代价函数的最坏情况,阶数越代价函数增长的上界精确
  2. Ω\Omega 符号(至少同阶),若m次多项式 A(n)=amnm+am1nm1++a1n+a0A(n)=a_mn^m+a_{m-1}n^{m-1}+\dotsb+a_1n+a_0 ,则 A(n)=Ω(nm)A(n)=\mathbf{\Omega}(n^m)

    • Ω(nm)\Omega(n^m) 描述代价函数阶数的最好情况,阶数越代价函数增长下界精确
  3. Θ\Theta 符号(同阶),若m次多项式 A(n)=amnm+am1nm1++a1n+a0A(n)=a_mn^m+a_{m-1}n^{m-1}+\dotsb+a_1n+a_0 ,则 A(n)=Θ(nm)A(n)=\mathbf{\Theta}(n^m)

    • 这里的等号类似 \in,把 Θ(Nn)\mathbf{\Theta}(N^n) 理解成阶数等于n的函数族或多项式集合
    • Θ(nm)\Theta(n^m) 表示代价函数的增长率与 c0nmc1nmc_0n^m\sim c_1n^m 一样快
    • 如果 f(n)=O(g(n))f(n)=O(g(n))f(n)=Ω(g(n))f(n)=\Omega(g(n)) ,则 f(n)=Θ(g(n))f(n)=\Theta(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]) {
// 交换 arr[j] 和 arr[j+1]
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
solution

主要是两层嵌套循环,外层i从0到n-2,内层j从0到n-i-2,外层执行n-1次,内层执行

(n1)+(n2)++1=(n1)n2(n-1)+(n-2)+\dotsb+1 = \frac{(n-1)n}{2}

次,所以时间复杂度为O(n2)O(n^2)

例二:求阶乘的时间复杂度

int factorial(int n) {
if (n == 0 || n == 1) {
return 1;
}
return n * factorial(n - 1);
}
solution

递归调用n次,所以时间复杂度为 O(n)O(n)

例三:二分法查找的复杂度

int binarySearch(int arr[], int size, int target) {
int left = 0;
int right = size - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
solution

每次循环将查找范围缩小一半,假设初始范围为n,经过k次循环后,范围变为 n2k\frac{n}{2^k} ,当范围缩小到1时,停止循环,即 n2k=1\frac{n}{2^k}=1 ,解得 k=log2nk=\log_2 n ,所以时间复杂度为 O(logn)O(\log n) 这种“每次操作都将问题规模缩小一半”的算法,复杂度通常都是 O(logn)O(\log n)

空间复杂性#

空间复杂性是指算法在运行过程中需要的内存空间,包括变量、数据结构、函数调用栈等所占用的空间。

数据结构与算法-复杂度分析
https://biscuit0613.github.io/posts/cs-core/algorithm/
作者
Biscuit
发布于
2025-09-05
许可协议
CC BY-NC-SA 4.0
cmake
cpp赛博扫盲日记