《数据结构与编程之美》学习笔记2:算法复杂度那些事儿

“算法复杂度分析是整个算法学习的精髓,只要掌握了它,数据结构和算法的内容基本上就掌握了一半。”这句话虽然夸张了些,但是也表达出了复杂度分析的重要性。本篇博文介绍算法复杂度的大O表示法、最好、最坏、平均和均摊时间复杂度。

一、如何分析、统计算法的执行效率和资源消耗?

1. 为什么需要复杂度分析?

  • 事后统计法 有很大的局限性
    • 测试结果非常依赖于环境,不同配置的计算机,会产生截然不同的测试结果。
    • 测试结果受数据规模的影响很大。例如,对于小规模的数据,插入排序可能会比快速排序还要快。

因此我们需要一个不用具体的测试数据来测试,就可以粗略的估计算法的执行效率的方法。即 时间、空间复杂度分析方法。

2. 大O复杂度表示法

计算机执行的代码时间 T ( n ) T(n) T(n) 与每行代码的执行次数 n n n 成正比 规律总结成公式如下:

T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n))

其中 T ( n ) T(n) T(n) 表示代码执行时间; n n n 表示数据规模; f ( n ) f(n) f(n) 表示每行代码执行的次数总和;大O是一个数学符号,表示代码的执行时间 T ( n ) T(n) T(n) f ( n ) f(n) f(n) 表达式成正比。

大O时间复杂度实际上并不具体表示代码真正的执行时间,而是表示 代码执行时间随数据规模增长的变化趋势 ,所以也叫做 渐进时间复杂度 (Asymptotic time complexity),简称 时间复杂度

3. 时间复杂度分析

三个实用分析方法

  • 只关注循环执行次数最多的一段代码
  • 加法法则:总复杂度等于量级最大的那段代码的复杂度
  • 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

4. 几种常见的时间复杂度分析

代码千差万别,但是常见的复杂度量级并不多。
复杂度量级(按照数量级递增)

  • 常数阶 O ( 1 ) O(1) O(1)
  • 对数阶 O ( l o g n ) O(logn) O(logn)
  • 线性阶 O ( n ) O(n) O(n)
  • 线性对数阶 O ( n l o g n ) O(nlogn) O(nlogn)
  • 平方阶 O ( n 2 ) O(n^2) O(n2)、立方阶 O ( n 3 ) O(n^3) O(n3) … k次方阶 O ( n k ) O(n^k) O(nk)
  • 指数阶 O ( 2 n ) O(2^n) O(2n)
  • 阶数阶 O ( n ! ) O(n!) O(n!)

以上的复杂度量级可以分为两类, 多项式量级非多项式量级 。其中非多项式量级只有两个 O ( 2 n ) O(2^n) O(2n) O ( n ! ) O(n!) O(n!) 。由于非多项式量级算法的执行时间会随着数据规模 n n n的增大而急剧增加,因此我们主要关注 多项式时间复杂度

4.1 O ( 1 ) O(1) O(1)

一般情况下,只要算法中不存在循环语句、递归语句、即使有成千上万行的代码,其时间复杂度也是 O ( 1 ) O(1) O(1)

4.2 O ( l o g n ) O(logn) O(logn) O ( n l o g n ) O(nlogn) O(nlogn)

举例

i = 1;
while (i <= n) {
    i = i * 2;
}

变量从 i=1 开始,每循环一次就乘以2。当 i 大于 n 时循环结束。设循环进行了 x 次,可得 2 x = n 2^x=n 2x=n ,解得 x = l o g 2 n x=log_2n x=log2n ,所以这段代码的复杂度就是 O ( l o g 2 n ) O(log_2n) O(log2n)

当代码变为

i = 1;
while (i <= n) {
    i = i * 3;
}

这段代码的时间复杂度为 O ( l o g 3 n ) O(log_3n) O(log3n) ,实际上不管是以 2 为底还是以 3 为底,或者以 10 为底,我们都将对数阶的时间复杂度记为 O ( l o g n ) O(logn) O(logn) 。因为对数之间可以利用 换底公式 互相转换。


对于 a , c ∈ ( 0 , 1 ) ∪ ( 1 , + ∞ ) a,c \in (0,1)\cup(1,+\infty) a,c(0,1)(1,+) b ∈ ( 0 , + ∞ ) b \in (0,+\infty) b(0,+) ,有

l o g a b = l o g c b l o g c a log_ab=\frac{logcb}{logca} logab=logcalogcb


l o g 3 n log_3n log3n 就等于 l o g 3 2 ∗ l o g 2 n log_32*log_2n log32log2n ,所以 O ( l o g 3 n ) = O ( C ∗ l o g 2 n ) O(log_3n)=O(C*log_2n) O(log3n)=O(Clog2n) ,其中 C = l o g 3 2 C=log_32 C=log32 是一个常量。在采用大O标记复杂度时可以忽略系数,且我们忽略对数的“底”,统一表示为 O ( l o g n ) O(logn) O(logn)

同理对于 O ( n l o g n ) O(nlogn) O(nlogn) 来说,其实就是把复杂度为 O ( l o g n ) O(logn) O(logn) 的代码循环了 n 遍。 O ( n l o g n ) O(nlogn) O(nlogn) 也是一种非常常见的算法时间复杂度。比如 归并排序快速排序 的时间复杂度都是 O ( n l o g n ) O(nlogn) O(nlogn)

4.3 O ( m + n ) O(m+n) O(m+n) O ( m ∗ n ) O(m*n) O(mn)

这个可以直接看出来,代码的复杂度是 由两个数据的规模 来决定的,m 和 n 表示的是两个数据的规模,当无法事先评估 m 和 n 的量级更大时,我们两个都保留。

5. 空间复杂度分析

空间复杂度全称为 渐进空间复杂度 (Asympotic space complexity),表示算法的存储空间与数据规模之间的增长关系。常见的空间复杂度也就只有 O ( 1 ) O(1) O(1) O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) ,像 O ( l o g n ) O(logn) O(logn) O ( n l o g n ) O(nlogn) O(nlogn) 这样的对数阶平时用不到。
时间复杂度

二、最好、最坏、平均、均摊时间复杂度

当然复杂度相关的知识点远不止上面这些,下面介绍最好情况时间复杂度(best case time complexity)、最坏情况时间复杂度(worst case time complexity)、平均情况 时间复杂度(average case time complexity)、均摊时间复杂度(amortized time complexity)。

1. 最好、最坏、平均情况时间复杂度

先看代码:

int find(int[] array, int n, int x) {
    int i = 0;
    int pos = -1;
    for(; i < n; i++) {
        if (array[i] == x) {
            pos = i;
            break;
        }
    }
    return pos;
}

由于 break 的存在,导致该算法的复杂度变得有些不同,当数组的第 1 个元素正好是要查找的变量 x ,那么就不需要遍历剩下的 n-1 个数据了,那么时间复杂度就是 O ( 1 ) O(1) O(1) 。如果数组中不存在变量 x ,那么就需要把整个数组遍历一遍,时间复杂度就成了 O ( n ) O(n) O(n) 。所以不同的情况下,这段代码的时间复杂度是不一样的。

此处引入三个概念,最好情况时间复 杂度、最坏情况时间复杂度和平均情况时间复杂度。

  • 最好情况时间复杂度就是,在最理想的情况下,执行这段代码的时间复杂度。
  • 最坏情况时间复杂度就是,在最糟糕的情况下,执行这段代码的时间复杂度。
  • 根据不同情况的概率甲醛平均得出的复杂度称为平均时间复杂度,全称叫做加权平均时间复杂度。

2. 均摊时间复杂度

先举个例子

// array 表示一个长度为 n 的数组 
// 代码中的 array.length 就等于 n 
int[] array = new int[n]; 
int count = 0;
void insert(int val) {
	if (count == array.length) { 
        int sum = 0; 
        for (int i = 0; i < array.length; ++i) { 
            sum = sum + array[i];
        } 
        array[0] = sum; count = 1;
	}
	array[count] = val; 
    ++count;
}

这段代码实现了一个往数组中插入数据的功能。当数组满了之 后,也就是代码中的 count == array.length 时,我们用 for 循环遍历数组求和,并清空数组,将求和之后的 sum 值放到数组的第一个位置,然后再将新的数据插入。但如果数组 一开始就有空闲空间,则直接将数据插入数组。

  • 最好的情况时间复杂度为 O ( 1 ) O(1) O(1)
  • 最坏的情况时间复杂度为 O ( n ) O(n) O(n)
  • 平均的情况时间复杂度为 O ( 1 ) O(1) O(1)

利用摊还分析法分析,只有一种情况下该算法的时间复杂度为 O ( n ) O(n) O(n)即 count==array.length,在count!=array.length情况下,代码的时间复杂度为 O ( 1 ) O(1) O(1) 。因此该算法的均摊时间复杂度就为 O ( 1 ) O(1) O(1) 。均摊时间复杂度就是一种特殊的平均时间复杂度。这个概念了解一下即可。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值