第3章 算法 (Algorithms)
一、导读
1.1 本章学习目标
完成本章学习后,你将能够:
理解算法的定义和基本特征:输入、输出、确定性、有限性、有效性
掌握算法的时间复杂度和空间复杂度分析方法
熟练使用大O记号、大Ω记号和大Θ记号描述算法的增长率
理解常见算法设计范式:穷举法、贪心算法、分治法、动态规划
掌握搜索算法(线性搜索、二分搜索)和排序算法(冒泡排序、插入排序、归并排序、快速排序)的原理与实现
能够分析递归算法的时间复杂度,建立和求解递推关系
理解算法正确性证明的基本方法,包括循环不变式
认识P类问题和NP类问题的基本概念
1.2 为什么本章如此重要
算法是计算机科学的核心。如果说数据结构是计算机程序的骨架,那么算法就是其灵魂。
在工程实践中:选择合适的算法可以将运行时间从几天缩短到几秒。例如,使用二分搜索而非线性搜索,可以在十亿条记录中只需约30次比较就能找到目标。
在面试与职业发展中:算法能力是科技公司招聘的核心考察点。理解算法不仅帮助通过面试,更是解决实际问题的基础技能。
在理论层面:算法分析将离散数学的工具(如求和、递推关系、归纳法)应用于实际问题,展示了数学的实用价值。
在系统设计层面:理解算法复杂度有助于评估系统的可扩展性。一个 O(n²) 的算法在小规模数据上可能表现良好,但在大规模数据上会成为瓶颈。
在日常编程中:即使不直接实现经典算法,算法思维也能帮助你写出更高效的代码。例如,选择合适的数据结构(哈希表 vs 列表)可以将查找操作从 O(n) 降低到 O(1)。
1.3 核心问题引导
在学习本章之前,请思考以下问题:
什么是算法?食谱可以算作算法吗?
为什么我们关心算法在最坏情况下的表现,而不是平均情况?
大O记号中的"O"代表什么?O(n) 和 O(2n) 有区别吗?
为什么二分搜索要求数据有序?它比线性搜索快多少?
最快的基于比较的排序算法能达到多快?为什么不能更快?
递归算法一定比迭代算法慢吗?
什么是"NP完全"问题?为什么它们如此特殊?
如何证明一个算法是正确的?测试足够吗?
二、核心概念详解
3.1 算法的定义与特征
什么是算法?
算法是解决特定问题的一系列明确指令。更正式地:
定义:算法是一个有限的、明确的指令序列,满足以下特征:
输入:算法接受零个或多个输入
输出:算法产生至少一个输出
确定性:每条指令都清晰无歧义
有限性:算法在执行有限步后终止
有效性:每条指令都是基本且可执行的
示例:欧几里得算法求最大公约数
输入:两个正整数 a, b
输出:gcd(a, b)
步骤:
1. 如果 b = 0,返回 a
2. 否则,返回 gcd(b, a mod b)这个算法满足所有特征:
- 输入:a, b
- 输出:最大公约数
- 确定性:每步操作明确
- 有限性:每次递归b减小,最终达到0
- 有效性:取模运算是基本操作
算法的描述方式
算法可以用多种方式描述:
- 自然语言:易于理解但可能有歧义
- 伪代码:介于自然语言和编程语言之间
- 流程图:可视化表示
- 程序代码:精确但依赖具体语言
本书主要使用伪代码和Python代码。
3.2 算法分析
为什么要分析算法?
对于同一个问题,可能存在多种算法。我们需要一种方法来比较它们的效率,而不依赖于:
- 具体的编程语言
- 编译器优化程度
- 硬件性能
- 具体输入数据
算法分析提供了这种独立于实现的比较方法。
时间复杂度
时间复杂度衡量算法运行时间随输入规模增长的变化趋势。
我们不关心精确的运行时间(因为这依赖于硬件),而是关心基本操作的执行次数。
示例:线性搜索
def linear_search(arr, target):
for i in range(len(arr)): # n次比较
if arr[i] == target:
return i
return -1- 最好情况:目标在第一个位置,1次比较
- 最坏情况:目标在最后或不存在,n次比较
- 平均情况:假设目标等概率出现在任何位置,约 n/2 次比较
空间复杂度
空间复杂度衡量算法所需内存随输入规模增长的变化趋势。
包括:
- 输入数据占用的空间
- 辅助变量占用的空间
- 递归调用栈的空间
示例:
- 线性搜索:O(1) 额外空间(只需要几个变量)
- 归并排序:O(n) 额外空间(需要临时数组)
3.3 渐近记号
大O记号(上界)
定义:f(n) = O(g(n)) 如果存在正常数 c 和 n₀,使得对所有 n ≥ n₀,有 f(n) ≤ c·g(n)。
直觉上,大O记号描述了函数的增长率上界。它告诉我们算法运行时间的"最坏情况"。
常见的大O复杂度(从小到大):
| 记号 | 名称 | 示例 |
|---|---|---|
| O(1) | 常数 | 数组访问 |
| O(log n) | 对数 | 二分搜索 |
| O(n) | 线性 | 线性搜索 |
| O(n log n) | 线性对数 | 归并排序 |
| O(n²) | 平方 | 冒泡排序 |
| O(n³) | 立方 | 矩阵乘法(朴素) |
| O(2ⁿ) | 指数 | 旅行商问题(暴力) |
| O(n!) | 阶乘 | 全排列枚举 |
重要性质:
- O(c·f(n)) = O(f(n)),常数因子可以忽略
- O(f(n) + g(n)) = O(max(f(n), g(n)))
- O(f(n) · g(n)) = O(f(n)) · O(g(n))
示例:
- 3n² + 5n + 2 = O(n²)
- 2ⁿ + n¹⁰⁰ = O(2ⁿ)(指数主导多项式)
大Ω记号(下界)
定义:f(n) = Ω(g(n)) 如果存在正常数 c 和 n₀,使得对所有 n ≥ n₀,有 f(n) ≥ c·g(n)。
大Ω记号描述了函数的增长率下界。
示例:
- 任何基于比较的排序算法至少需要 Ω(n log n) 次比较
- 线性搜索的最坏情况是 Ω(n)
大Θ记号(紧确界)
定义:f(n) = Θ(g(n)) 如果 f(n) = O(g(n)) 且 f(n) = Ω(g(n))。
即存在正常数 c₁, c₂ 和 n₀,使得对所有 n ≥ n₀,有 c₁·g(n) ≤ f(n) ≤ c₂·g(n)。
大Θ记号描述了函数的精确增长率。
示例:
- 线性搜索的最坏情况是 Θ(n)
- 二分搜索的最坏情况是 Θ(log n)
小o和小ω记号
- 小o:f(n) = o(g(n)) 意味着 f 的增长严格慢于 g
- 形式化:lim(n→∞) f(n)/g(n) = 0
- 示例:n = o(n²)
- 小ω:f(n) = ω(g(n)) 意味着 f 的增长严格快于 g
- 形式化:lim(n→∞) f(n)/g(n) = ∞
- 示例:n² = ω(n)
3.4 常见算法
搜索算法
线性搜索:逐个检查元素
def linear_search(arr, target):
"""时间复杂度: O(n), 空间复杂度: O(1)"""
for i in range(len(arr)):
if arr[i] == target:
return i
return -1二分搜索:每次排除一半
def binary_search(arr, target):
"""
前提:arr已排序
时间复杂度: O(log n), 空间复杂度: O(1)
"""
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1二分搜索的正确性依赖于数组有序这一前提。每次迭代将搜索范围减半,因此最多需要 log₂n 次迭代。
排序算法
冒泡排序:相邻元素交换
def bubble_sort(arr):
"""
时间复杂度: O(n²), 空间复杂度: O(1)
稳定排序
"""
n = len(arr)
for i in range(n):
swapped = False
for j in range(n - 1 - i):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped: # 优化:如果没有交换,提前终止
break插入排序:将每个元素插入已排序部分
def insertion_sort(arr):
"""
时间复杂度: O(n²), 空间复杂度: O(1)
稳定排序,对小数组高效
"""
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key归并排序:分治法
def merge_sort(arr):
"""
时间复杂度: O(n log n), 空间复杂度: O(n)
稳定排序
"""
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result快速排序:分区后递归排序
def quick_sort(arr):
"""
平均时间复杂度: O(n log n)
最坏时间复杂度: O(n²)(当pivot选择不当时)
空间复杂度: O(log n)(递归栈)
不稳定排序
"""
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)排序算法比较
| 算法 | 最好 | 平均 | 最坏 | 空间 | 稳定 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 是 |
| 插入排序 | O(n) | O(n²) | O(n²) | O(1) | 是 |
| 归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 是 |
| 快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n) | 否 |
定理:任何基于比较的排序算法的最坏情况时间复杂度下界为 Ω(n log n)。
证明思路:基于比较的排序可以看作决策树。n个元素有n!种排列,决策树至少有n!个叶节点。高度为h的二叉树最多有2ʰ个叶节点,所以 2ʰ ≥ n!,即 h ≥ log₂(n!) = Θ(n log n)。∎
3.5 递归与递推关系
递归算法的分析
递归算法的时间复杂度通常由递推关系描述。
示例:归并排序的递推关系
T(n) = 2T(n/2) + O(n)
解释:
- 2T(n/2):递归排序两个子数组
- O(n):合并两个子数组
主定理(Master Theorem)
对于形如 T(n) = aT(n/b) + f(n) 的递推关系,其中 a ≥ 1, b > 1:
令 $c_{crit} = \log_b a$
如果 f(n) = O(n^(c_crit - ε)) 对某个 ε > 0,则 T(n) = Θ(n^c_crit)
如果 f(n) = Θ(n^c_crit),则 T(n) = Θ(n^c_crit · log n)
如果 f(n) = Ω(n^(c_crit + ε)) 对某个 ε > 0,且 af(n/b) ≤ cf(n) 对某个 c < 1,则 T(n) = Θ(f(n))
应用:
- 归并排序:T(n) = 2T(n/2) + O(n),a=2, b=2, c_crit=1, f(n)=O(n),属于情况2,T(n) = Θ(n log n)
- 二分搜索:T(n) = T(n/2) + O(1),a=1, b=2, c_crit=0, f(n)=O(1),属于情况2,T(n) = Θ(log n)
3.6 算法正确性
循环不变式
循环不变式是在循环的每次迭代前后都为真的条件。它用于证明算法的正确性。
三个步骤:
初始化:在第一次迭代前,不变式为真
保持:如果某次迭代前不变式为真,则下次迭代前仍为真
终止:循环终止时,不变式给出期望的结果
示例:插入排序的正确性证明
循环不变式:在第i次迭代开始时,子数组 arr[0..i-1] 包含原数组前i个元素的排序版本。
初始化:i=1时,arr[0..0]只有一个元素,自然是有序的。
保持:第i次迭代将arr[i]插入到arr[0..i-1]的正确位置,使得arr[0..i]有序。
终止:当i=n时,arr[0..n-1]包含所有元素的排序版本。∎
三、程序员的视角
3.1 在计算机科学中的应用
标准库中的算法
大多数编程语言的标准库都实现了经典算法:
# Python内置排序(TimSort,混合归并排序和插入排序)
sorted_list = sorted(my_list)
# 二分搜索
import bisect
index = bisect.bisect_left(sorted_list, target)
# 堆操作
import heapq
heapq.heappush(heap, item)
heapq.heappop(heap)理解底层算法有助于正确使用这些工具。
性能优化
- 选择 O(n log n) 排序而非 O(n²) 排序处理大数据
- 使用哈希表实现 O(1) 查找而非 O(n) 线性搜索
- 用空间换时间:缓存计算结果(动态规划/记忆化)
算法设计模式
- 贪心算法:每一步做局部最优选择(如Dijkstra最短路径)
- 分治法:将问题分解为子问题(如归并排序)
- 动态规划:存储子问题解避免重复计算(如背包问题)
- 回溯法:尝试所有可能,剪枝无效分支(如N皇后问题)
3.2 常见陷阱与误区
忽视最坏情况
某些算法在平均情况下表现良好,但在最坏情况下可能很差。例如,快速排序的平均复杂度是 O(n log n),但如果每次都选到最小或最大元素作为pivot,最坏情况是 O(n²)。
混淆 O 和 Θ
说"快速排序是 O(n²)"虽然技术上正确(因为 O 是上界),但更有意义的说法是"快速排序的平均情况是 Θ(n log n)"。
忽视常数因子
大O记号忽略常数因子,但在实际应用中,常数因子可能很重要。例如,O(n) 的算法如果常数很大,可能在中小规模数据上不如 O(n log n) 的算法。
过早优化
Donald Knuth说过:"过早优化是万恶之源。"首先确保代码正确和清晰,然后在必要时进行优化。
3.3 与其他层级的联系
- 硬件层:缓存友好性影响实际性能
- 操作系统层:虚拟内存、页面置换影响大规模数据处理
- 网络层:分布式算法考虑通信成本
- 应用层:用户体验响应时间要求
四、代码实战
4.1 算法复杂度可视化工具
"""
算法复杂度可视化:比较不同增长率的函数
"""
import math
import matplotlib.pyplot as plt
def plot_complexities():
"""绘制常见复杂度函数的增长曲线"""
n_values = list(range(1, 101))
complexities = {
'O(1)': [1] * len(n_values),
'O(log n)': [math.log2(n) for n in n_values],
'O(n)': n_values.copy(),
'O(n log n)': [n * math.log2(n) for n in n_values],
'O(n²)': [n ** 2 for n in n_values],
'O(2ⁿ)': [min(2 ** n, 10**6) for n in n_values], # 限制最大值以便显示
}
plt.figure(figsize=(12, 8))
for name, values in complexities.items():
plt.plot(n_values, values, label=name, linewidth=2)
plt.xlabel('输入规模 n', fontsize=12)
plt.ylabel('操作次数', fontsize=12)
plt.title('算法复杂度比较', fontsize=14)
plt.legend(fontsize=11)
plt.grid(True, alpha=0.3)
plt.ylim(0, 1000) # 限制y轴范围以便观察
plt.tight_layout()
plt.savefig('complexity_comparison.png', dpi=150)
plt.show()
print("复杂度比较图已保存为 complexity_comparison.png")
def compare_sorting_algorithms():
"""比较不同排序算法的实际运行时间"""
import random
import time
def generate_random_array(size):
return [random.randint(0, 10000) for _ in range(size)]
def benchmark(func, arr):
start = time.perf_counter()
func(arr.copy())
end = time.perf_counter()
return end - start
sizes = [100, 500, 1000, 2000, 5000]
print("排序算法性能比较(秒):")
print(f"{'大小':>8} {'冒泡':>10} {'插入':>10} {'归并':>10} {'快速':>10}")
print("-" * 50)
for size in sizes:
arr = generate_random_array(size)
bubble_time = benchmark(bubble_sort_impl, arr)
insert_time = benchmark(insertion_sort_impl, arr)
merge_time = benchmark(merge_sort_impl, arr)
quick_time = benchmark(quick_sort_impl, arr)
print(f"{size:>8} {bubble_time:>10.6f} {insert_time:>10.6f} "
f"{merge_time:>10.6f} {quick_time:>10.6f}")
# 排序算法的实现(用于基准测试)
def bubble_sort_impl(arr):
n = len(arr)
for i in range(n):
for j in range(n - 1 - i):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
def insertion_sort_impl(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
def merge_sort_impl(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort_impl(arr[:mid])
right = merge_sort_impl(arr[mid:])
return merge_impl(left, right)
def merge_impl(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
def quick_sort_impl(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort_impl(left) + middle + quick_sort_impl(right)
if __name__ == "__main__":
plot_complexities()
print()
compare_sorting_algorithms()4.2 二分搜索及其变体
"""
二分搜索及其变体的完整实现
"""
def binary_search_exact(arr, target):
"""
标准二分搜索:查找精确匹配
返回: 目标索引,未找到返回-1
"""
left, right = 0, len(arr) - 1
while left <= right:
mid = left + (right - left) // 2 # 防止溢出
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
def binary_search_lower_bound(arr, target):
"""
查找下界:第一个大于等于target的元素位置
返回: 下界索引
"""
left, right = 0, len(arr)
while left < right:
mid = left + (right - left) // 2
if arr[mid] < target:
left = mid + 1
else:
right = mid
return left
def binary_search_upper_bound(arr, target):
"""
查找上界:第一个大于target的元素位置
返回: 上界索引
"""
left, right = 0, len(arr)
while left < right:
mid = left + (right - left) // 2
if arr[mid] <= target:
left = mid + 1
else:
right = mid
return left
def binary_search_first_occurrence(arr, target):
"""
查找目标第一次出现的位置(数组可能有重复元素)
返回: 第一次出现的索引,未找到返回-1
"""
left, right = 0, len(arr) - 1
result = -1
while left <= right:
mid = left + (right - left) // 2
if arr[mid] == target:
result = mid
right = mid - 1 # 继续在左半部分查找
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return result
def binary_search_last_occurrence(arr, target):
"""
查找目标最后一次出现的位置(数组可能有重复元素)
返回: 最后一次出现的索引,未找到返回-1
"""
left, right = 0, len(arr) - 1
result = -1
while left <= right:
mid = left + (right - left) // 2
if arr[mid] == target:
result = mid
left = mid + 1 # 继续在右半部分查找
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return result
def demonstrate_binary_search_variants():
"""演示二分搜索的各种变体"""
arr = [1, 2, 2, 2, 3, 4, 5, 5, 6, 7]
target = 2
print(f"数组: {arr}")
print(f"目标: {target}")
print()
exact = binary_search_exact(arr, target)
print(f"精确匹配索引: {exact}")
lower = binary_search_lower_bound(arr, target)
print(f"下界索引(第一个≥{target}): {lower}")
upper = binary_search_upper_bound(arr, target)
print(f"上界索引(第一个>{target}): {upper}")
first = binary_search_first_occurrence(arr, target)
print(f"第一次出现: {first}")
last = binary_search_last_occurrence(arr, target)
print(f"最后一次出现: {last}")
# 验证
print("\n验证:")
print(f" arr[{lower}:{upper}] = {arr[lower:upper]}")
print(f" 所有元素都等于{target}: {all(x == target for x in arr[lower:upper])}")
def find_peak_element(arr):
"""
查找峰值元素:比邻居都大的元素
使用二分搜索思想,时间复杂度 O(log n)
返回: 峰值索引
"""
left, right = 0, len(arr) - 1
while left < right:
mid = (left + right) // 2
if arr[mid] < arr[mid + 1]:
left = mid + 1 # 峰值在右边
else:
right = mid # 峰值在左边或就是mid
return left
if __name__ == "__main__":
demonstrate_binary_search_variants()
print("\n" + "=" * 50)
print("峰值查找示例:")
arr = [1, 2, 3, 1, 2, 3, 1]
peak_idx = find_peak_element(arr)
print(f"数组: {arr}")
print(f"峰值索引: {peak_idx}, 峰值: {arr[peak_idx]}")4.3 递归算法与记忆化
"""
递归算法与记忆化优化
"""
import time
from functools import lru_cache
# --- 斐波那契数列 ---
def fibonacci_naive(n):
"""
naive递归:指数时间复杂度 O(2^n)
"""
if n <= 1:
return n
return fibonacci_naive(n - 1) + fibonacci_naive(n - 2)
@lru_cache(maxsize=None)
def fibonacci_memo(n):
"""
记忆化递归:线性时间复杂度 O(n)
"""
if n <= 1:
return n
return fibonacci_memo(n - 1) + fibonacci_memo(n - 2)
def fibonacci_iterative(n):
"""
迭代方法:线性时间复杂度 O(n),常数空间 O(1)
"""
if n <= 1:
return n
prev, curr = 0, 1
for _ in range(2, n + 1):
prev, curr = curr, prev + curr
return curr
# --- 爬楼梯问题 ---
def climb_stairs_naive(n):
"""
每次可以爬1或2阶,爬到第n阶有多少种方法?
naive递归:指数时间
"""
if n <= 2:
return n
return climb_stairs_naive(n - 1) + climb_stairs_naive(n - 2)
@lru_cache(maxsize=None)
def climb_stairs_memo(n):
"""记忆化版本"""
if n <= 2:
return n
return climb_stairs_memo(n - 1) + climb_stairs_memo(n - 2)
# --- 硬币找零问题 ---
def coin_change_greedy(coins, amount):
"""
贪心算法:不一定得到最优解
返回: 最少硬币数,无法找零返回-1
"""
coins.sort(reverse=True)
count = 0
remaining = amount
for coin in coins:
while remaining >= coin:
remaining -= coin
count += 1
return count if remaining == 0 else -1
def coin_change_dp(coins, amount):
"""
动态规划:保证最优解
时间复杂度: O(amount * len(coins))
空间复杂度: O(amount)
返回: 最少硬币数,无法找零返回-1
"""
# dp[i] 表示凑成金额i所需的最少硬币数
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for coin in coins:
if i >= coin:
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
def demonstrate_recursion_optimization():
"""演示递归优化的效果"""
n = 30
print(f"斐波那契数列 F({n}):")
# Naive递归(仅对小n执行,否则太慢)
if n <= 30:
start = time.perf_counter()
result_naive = fibonacci_naive(n)
time_naive = time.perf_counter() - start
print(f" Naive递归: {result_naive} (耗时: {time_naive:.4f}s)")
# 记忆化递归
start = time.perf_counter()
result_memo = fibonacci_memo(n)
time_memo = time.perf_counter() - start
print(f" 记忆化递归: {result_memo} (耗时: {time_memo:.6f}s)")
# 迭代
start = time.perf_counter()
result_iter = fibonacci_iterative(n)
time_iter = time.perf_counter() - start
print(f" 迭代方法: {result_iter} (耗时: {time_iter:.6f}s)")
print(f"\n验证一致性: {result_memo == result_iter}")
# 清除缓存
fibonacci_memo.cache_clear()
def demonstrate_coin_change():
"""演示硬币找零问题"""
coins = [25, 10, 5, 1] # 美分硬币
amount = 63
print(f"\n硬币找零问题:")
print(f" 硬币面额: {coins}")
print(f" 目标金额: {amount}")
greedy_result = coin_change_greedy(coins.copy(), amount)
dp_result = coin_change_dp(coins, amount)
print(f" 贪心算法: {greedy_result} 枚硬币")
print(f" 动态规划: {dp_result} 枚硬币")
# 展示贪心不是最优的例子
print("\n 贪心非最优的例子:")
coins_special = [1, 3, 4]
amount_special = 6
greedy_special = coin_change_greedy(coins_special.copy(), amount_special)
dp_special = coin_change_dp(coins_special, amount_special)
print(f" 硬币面额: {coins_special}")
print(f" 目标金额: {amount_special}")
print(f" 贪心算法: {greedy_special} 枚(4+1+1)")
print(f" 动态规划: {dp_special} 枚(3+3)← 更优")
if __name__ == "__main__":
demonstrate_recursion_optimization()
demonstrate_coin_change()五、深入思考
5.1 历史背景
算法的概念可以追溯到古代。欧几里得算法(约公元前300年)是最早的已知算法之一。花拉子米(Al-Khwarizmi, 约825年)的名字演变为"algorithm"一词。
现代算法理论的发展:
- 艾伦·图灵(1936):图灵机模型定义了可计算性
- 约翰·冯·诺依曼:存储程序概念
- 唐纳德·克努特:《计算机程序设计艺术》系统化算法分析
- 斯蒂芬·库克和理查德·卡普:NP完全性理论(1971-1972)
5.2 前沿方向
- 量子算法:Shor算法(因数分解)、Grover算法(搜索)
- 近似算法:为NP难问题寻找接近最优的解
- 在线算法:输入逐步到达时的决策
- 并行与分布式算法:利用多核和集群加速
- 机器学习算法:从数据中学习模式
5.3 延伸阅读
- 《The Art of Computer Programming》by Donald E. Knuth
- 《Introduction to Algorithms》by Cormen, Leiserson, Rivest, Stein (CLRS)
- 《Algorithm Design Manual》by Steven S. Skiena
六、实验笔记
实验1:排序算法性能分析
目标:实证分析不同排序算法的性能特征。
任务:
实现冒泡排序、插入排序、归并排序、快速排序
生成不同规模的随机数组(100, 1000, 10000, 100000)
测量每种算法在不同规模下的运行时间
绘制运行时间 vs 输入规模的图表
验证理论复杂度与实际测量的一致性
测试特殊情况:已排序数组、逆序数组、全部相同元素
提交物:源代码、性能图表、分析报告。
实验2:算法可视化工具
目标:创建一个交互式工具,可视化算法的执行过程。
任务:
选择一种算法(如排序或搜索)
实现逐步执行功能,每步暂停并显示当前状态
使用图形界面展示数组状态、指针位置等
允许用户控制执行速度
(进阶)支持多种算法切换
技术建议:使用matplotlib动画、tkinter或web前端。
提交物:可运行的程序、使用说明。
七、本章小结
本章介绍了算法的基本概念和分析方法。算法是解决问题的明确指令序列,必须满足输入、输出、确定性、有限性和有效性五个特征。
算法分析关注时间和空间复杂度,使用渐近记号(大O、大Ω、大Θ)描述增长率。大O记号给出上界,大Ω给出下界,大Θ给出紧确界。常见复杂度从低到高包括:O(1)、O(log n)、O(n)、O(n log n)、O(n²)、O(2ⁿ)、O(n!)。
搜索和排序是两类基本算法。线性搜索时间复杂度为O(n),二分搜索为O(log n)但要求数据有序。排序算法中,基于比较的排序下界为Ω(n log n),归并排序和快速排序达到这一下界。
递归算法的分析涉及递推关系,主定理提供了求解特定形式递推关系的系统方法。算法正确性可以通过循环不变式来证明。
理解算法不仅是理论要求,更是实践需要。选择合适的算法可以显著提升程序性能,而算法思维有助于设计优雅的解决方案。
八、习题
习题1(基础):确定下列函数的大O表示:
(a) f(n) = 3n² + 5n + 2
(b) f(n) = 2ⁿ + n³
(c) f(n) = n log n + n
(d) f(n) = 100
习题2(基础):对数组 [64, 34, 25, 12, 22, 11, 90] 执行:
(a) 手动模拟冒泡排序的前两轮
(b) 手动模拟插入排序的前三轮
(c) 手动模拟归并排序的完整过程
习题3(中等):求解下列递推关系:
(a) T(n) = T(n/2) + 1, T(1) = 1
(b) T(n) = 2T(n/2) + n, T(1) = 1
(c) T(n) = 3T(n/3) + n, T(1) = 1
习题4(中等):证明二分搜索的时间复杂度为 O(log n)。
习题5(中等):设计一个算法,在 O(n) 时间内找到数组中的最大值和最小值。你的算法需要多少次比较?
习题6(较难):证明任何基于比较的排序算法的下界为 Ω(n log n)。(提示:使用决策树模型)
习题7(较难):修改快速排序,使用"三数取中"法选择pivot,分析其对最坏情况的影响。
习题8(挑战):研究并实现Strassen矩阵乘法算法,分析其时间复杂度为何优于朴素的 O(n³) 算法。
习题9(挑战):证明或反驳:存在一个算法,能在 O(n) 时间内判断一个数组中是否有重复元素。(假设只能使用比较操作)
习题10(开放):选择一个你感兴趣的算法问题(如图论、字符串处理、几何等),研究现有算法并撰写报告,包括问题定义、算法思路、复杂度分析和实际应用。