
算法 | 最好情况 | 平均情况 | 最坏情况 |
|---|---|---|---|
冒泡排序 | O(n) | O(n²) | O(n²) |
快速排序 | O(n log n) | O(n log n) | O(n²) |
归并排序 | O(n log n) | O(n log n) | O(n log n) |
二分查找 | O(1) | O(log n) | O(log n) |
线性查找 | O(1) | O(n) | O(n) |
二叉树遍历 | O(n) | O(n) | O(n) |
哈希表查找 | O(1) | O(1) | O(n) |
算法是解决问题的指令集。我们分析算法的主要目的是估计资源消耗(通常是时间),以便: ● 优化程序,让运行时间从天/年降低到秒。 ● 判断算法在大规模数据下的可行性。
这些是我们学习算法基础的核心概念
符号 | 名称 | 含义 | 类比 | 公式 |
|---|---|---|---|---|
O | 大O符号 | 上界(最坏情况) | 最高不超过 | T(n) ≤ c·f(n) |
Ω | 大Ω符号 | 下界(最好情况) | 至少不低于 | T(n) ≥ c·f(n) |
Θ | 大Θ符号 | 紧确界(平均情况) | 既上又下 | c₁·f(n) ≤ T(n) ≤ c₂·f(n) |
o | 小o符号 | 非紧上界 | 严格小于 | T(n) < c·f(n) |
ω | 小ω符号 | 非紧下界 | 严格大于 | T(n) > c·f(n) |
下面是我们的计算法则:
法则 1:加法与乘法法则 ● 加法规则:O(f(N)) + O(g(N)) = O(max(f(N), g(N))) ● 解读:当你有两个操作,一个耗时 N^2,一个耗时 N,总耗时是 O(N^2)。高阶项统治一切。 ● 乘法规则:O(f(N)) times O(g(N)) = O(f(N) times g(N)) ● 解读:嵌套循环通常是乘法关系。 法则 2:多项式法则 ● 如果 T(N) 是一个 k 次多项式,则 T(N) = Theta(N^k)。 ● 例子:3N^2 + 2N + 1 = Theta(N^2)。 法则 3:对数的增长 ● 对于任意常数 k,log^k N = O(N)。 ● 解读:对数增长非常缓慢。哪怕是对数的平方、立方,增长速度也远慢于线性 N。
规则 | 说明 | 示例 |
|---|---|---|
加法常数忽略 | O(n+1000) → O(n) | 循环1000次 + 循环n次 → O(n) |
乘法常数忽略 | O(2n) → O(n) | 2n → n |
低阶项忽略 | O(n² + n) → O(n²) | 只保留最高阶 |
不同代码段相加 | 取最大值 | O(n) + O(n²) → O(n²) |
● 目的:为了客观地分析算法,我们需要一个标准的“计算模型”。 ● 模型假设: ● 指令系统:模型拥有简单的指令集(加、乘、比较等)。 ● 执行方式:指令是顺序执行的。 ● 时间单位:每条简单指令(如加法、赋值)花费一个固定的时间单位。 ● 内存:假设内存是无限的,且访问时间是固定的(忽略缓存、磁盘IO等现实中的复杂因素)。 ● 意义:这个理想化的模型让我们可以抛开具体硬件的差异,专注于算法本身的逻辑效率。
要分析的问题 关注点:算法分析主要关注运行时间,而运行时间主要受算法本身和输入规模 N 的影响。 三种情形: 最坏情况 (T_{text{worst}}(N)):算法在任何输入下运行时间的上界。这是最常分析的情况,因为它给出了性能的保证。 平均情况 (T_{text{avg}}(N)):所有可能输入下运行时间的期望值。计算通常比较困难,且依赖于输入的分布假设。 最好情况:通常意义不大,因为它不能代表算法的典型性能。 结论:除非特别说明,通常分析的是最坏情况的运行时间。
运行时间计算 ● 目标:估算一个程序片段的运行时间,通常用大 O 表示法来描述。 ● 分析原则: 忽略常数项和低阶项。 关注随着输入规模 N 增大,运行时间增长的趋势。 计算法则: 法则 1 (For 循环):一个 For 循环的运行时间是循环体内部语句的运行时间乘以循环次数。 法则 2 (嵌套循环):从里向外分析。嵌套循环内部语句的总运行时间是该语句运行时间乘以所有循环次数的乘积。例如,两层循环通常对应 O(N^2)。 法则 3 (顺序语句):将各个语句的运行时间求和。最终结果取其中最大的项。例如,先执行一个 O(N) 的循环,再执行一个 O(N^2) 的循环,总时间是 O(N^2)。 法则 4 (If/Else 语句):运行时间不超过判断条件的时间加上 S1 和 S2 中运行时间较长者的总和。
一个简单的例子 代码功能:计算 sum_{i=1}^{N} i^3。 详细分析: 变量声明、赋值等简单操作记为 1 个时间单位。 循环体内部的 partialSum += i * i * i; 包含乘法、加法和赋值,记为 4 个时间单位。 循环执行 N 次,加上循环控制的开销,总时间约为 6N + 4。 简化结果:根据大 O 表示法,忽略常数项,最终结果为 O(N)。
例子 1:阶乘 (factorial) 这是一个简单的线性递归。每次调用只产生一次新的递归调用。 时间复杂度:O(N)。 例子 2:斐波那契数列 (fib) 这是一个低效的递归实现。每次调用 fib(n) 会产生两次新的调用 fib(n-1) 和 fib(n-2) 。 分析难点:不能简单地看代码行数,需要建立递推关系式 T(n) = T(n-1) + T(n-2) + O(1)。 结果:这是一个指数级增长的算法(约为 O(2^N)),效率极低。例如,当 N=40 时,运行时间会非常惊人。 递归的陷阱:斐波那契数列 问题:分析 fib(n) 函数的运行时间。 现象:看似简单的递归代码,实际运行效率极低。 分析: fib(n) 会递归调用 fib(n-1) 和 fib(n-2) 。这意味着它会重复计算大量的子问题。 数学推导:运行时间 T(N) 满足递推关系 T(N) = T(N-1) + T(N-2) + 2。 结论:运行时间呈指数级增长(约为 O(1.618^N))。例如,当 N=40 时,计算量巨大,效率极低。 因此:递归并不总是高效的,尤其是存在大量重复计算时。可以考虑用循环(动态规划)来优化。
总结 前面我们系统地介绍了算法分析的基本方法论:从建立理想化的计算模型出发,确立了分析最坏情况时间复杂度的目标,并给出了计算循环、顺序结构和递归结构时间复杂度的具体法则。通过具体的代码示例(求和、阶乘、斐波那契),展示了如何将这些法则应用到实际编程中,从而量化算法的性能。
这个问题旨在寻找一个数组中连续子序列的最大和。展示了四种不同的算法,体现了算法优化的层次。
我们的 思路:使用三重嵌套循环。外层循环确定子列起点 i,中层循环确定终点 j,内层循环计算从 i 到 j 的和。 ● 分析:三重循环,时间复杂度为 O(N^3)。 ● 特点:逻辑最直观,但效率最低。
思路:利用数学规律 S_{k=1}^{j} A_k = A_j + S_{k=1}^{j-1} A_k。内层循环不再重新计算和,而是在上一次结果的基础上加上当前元素 A_j。 ● 分析:两重循环,时间复杂度降为 O(N^2)。 ● 特点:通过消除冗余计算,效率提升了一个数量级。
分:将数组从中间分为左右两部分。 治:递归求解左半部分的最大子列和、右半部分的最大子列和。 合:求解跨越中间分界线的最大子列和(这是关键步骤,需要向两边扫描)。 分析:递推公式为 T(N) = 2T(N/2) + O(N),解得 T(N) = O(N log N)。 特点:利用递归将大问题分解,效率较高,但代码相对复杂。
思路:只扫描数组一次。维护一个当前和 thisSum 。如果当前和为负数,则丢弃(因为负数只会拖累后续的和),并重置为 0。如果当前和大于最大和 maxSum ,则更新最大和。 分析:单层循环,时间复杂度为 O(N)。 特点:这是最优解。它只需要一次扫描,空间复杂度为 O(1),是“联机算法”的典范。
我们的问题是:在一个有序数组中查找某个元素。 比较中间元素与目标值。 如果相等,找到目标。 如果目标值小于中间元素,递归查找左半部分。 如果目标值大于中间元素,递归查找右半部分。
分析:每次迭代都将问题规模减半。循环次数为 log_2 N,因此时间复杂度为 O(log N)。 特点:效率极高。例如,对于包含 100 万个元素的数组,二分查找最多只需要 20 次比较。
总结 通过具体的代码和数学分析,生动地展示了算法优化的魅力: 1. 避免重复计算:如斐波那契数列和最大子列和算法 1 -> 2 的优化。 2. 利用问题特性:如最大子列和算法 4 利用了“负数和无益于最大和”的特性。 3. 分治策略:如最大子列和算法 3,将问题分解以降低复杂度。 4. 对数级优化:如二分查找,通过每次减半问题规模实现高效查找。
核心问题:计算两个整数的最大公约数(gcd)。 算法逻辑:利用定理 gcd(M, N) = gcd(N, M mod N) 。不断用较小数去除较大数取余数,直到余数为0,此时的除数就是最大公约数。 关键难点:余数序列的递减规律不是线性的。 定理 :如果 M > N,则 M bmod N < M/2。这意味着每次迭代后,较大的数至少减少一半。 结论:由于每次迭代规模减半,迭代次数至多是 2log N,因此运行时间复杂度为 O(log N)。 特点:这是一个非常高效的古老算法,即使在现代计算机中也表现优异。
核心问题:计算 X^N,其中 N 可能非常大。 朴素算法:使用 N-1 次乘法,时间复杂度为 O(N)。 高效算法(快速幂): 思路:利用分治思想。如果 N 是偶数,则 X^N = (X^2)^{N/2};如果 N 是奇数,则 X^N = X cdot X^{N-1}。 代码实现:书中图2-11展示了递归实现。关键在于第8行和第10行,通过递归调用 pow(x*x, n/2) 将问题规模减半。 复杂度:每次递归 N 减半,因此时间复杂度为 O(log N)。 常见错误分析:书中列举了几个看似正确但实际上错误的代码变体(8a, 8b, 8c),强调了在递归中参数传递的严谨性(如防止无限递归、避免不必要的重复计算)。
悲观估计:在算法分析中,我们经常使用最坏情况(Worst-case)分析。这种分析有时会过于悲观(Overestimate)。 平均情况 vs 最坏情况: 平均情况:分析通常非常复杂,且依赖于输入数据的分布假设。 最坏情况:虽然可能比实际运行慢,但提供了性能的上限保证,是“已知的最佳解析结果”。 下界分析 (Lower Bound):证明任何算法的运行时间不可能低于某个界限(例如比较排序算法的下界是 Omega(N log N))。这是算法分析中最难的部分。