3离散数学及其应用(第8版)

算法

用数学分析算法

算法设计复杂度分析大 O 记号正确性证明

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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 算法分析

为什么要分析算法?

对于同一个问题,可能存在多种算法。我们需要一种方法来比较它们的效率,而不依赖于:

  • 具体的编程语言
  • 编译器优化程度
  • 硬件性能
  • 具体输入数据

算法分析提供了这种独立于实现的比较方法。

时间复杂度

时间复杂度衡量算法运行时间随输入规模增长的变化趋势。

我们不关心精确的运行时间(因为这依赖于硬件),而是关心基本操作的执行次数。

示例:线性搜索

python
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 常见算法

搜索算法

线性搜索:逐个检查元素

python
def linear_search(arr, target):
    """时间复杂度: O(n), 空间复杂度: O(1)"""
    for i in range(len(arr)):
        if arr[i] == target:
            return i
    return -1

二分搜索:每次排除一半

python
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 次迭代。

排序算法

冒泡排序:相邻元素交换

python
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

插入排序:将每个元素插入已排序部分

python
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

归并排序:分治法

python
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

快速排序:分区后递归排序

python
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
# 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 算法复杂度可视化工具

python
"""
算法复杂度可视化:比较不同增长率的函数
"""

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 二分搜索及其变体

python
"""
二分搜索及其变体的完整实现
"""


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 递归算法与记忆化

python
"""
递归算法与记忆化优化
"""

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(开放):选择一个你感兴趣的算法问题(如图论、字符串处理、几何等),研究现有算法并撰写报告,包括问题定义、算法思路、复杂度分析和实际应用。

3/13算法
0%