Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第二十六 算法

算法(Algorithm)是解决特定问题的一系列明确、有限的计算步骤。如果说编程语言是工具,那么算法就是使用这些工具的方法论。好的算法能够在有限的时间和空间内,高效地解决问题。


一、算法基础

1.1 什么是算法

算法具有五个基本特性:

特性说明
有穷性算法必须在有限步骤后结束
确定性每一步都有明确的定义,无二义性
可行性每一步都可以通过基本运算实现
输入有零个或多个输入
输出至少有一个输出

1.2 算法效率的度量

评价算法好坏的核心指标是时间复杂度空间复杂度

大 O 记号(Big-O Notation)

大 O 记号描述算法运行时间随输入规模增长的趋势:

复杂度记号说明示例
常数时间O(1)与输入规模无关数组按索引访问
对数时间O(log n)每步将问题规模减半二分查找
线性时间O(n)与输入规模成正比遍历数组
线性对数O(n log n)线性乘以对数归并排序、快速排序
平方时间O(n²)双重循环冒泡排序
指数时间O(2ⁿ)每步问题规模翻倍穷举子集
阶乘时间O(n!)全排列旅行商问题(暴力)
#![allow(unused)]
fn main() {
// O(1) - 常数时间
fn get_first(arr: &[i32]) -> Option<i32> {
    arr.first().copied()  // 直接访问,与数组长度无关
}

// O(n) - 线性时间
fn find_max(arr: &[i32]) -> Option<i32> {
    arr.iter().max().copied()  // 需要遍历整个数组
}

// O(n²) - 平方时间
fn bubble_sort(arr: &mut [i32]) {
    let n = arr.len();
    for i in 0..n {
        for j in 0..n - i - 1 {
            if arr[j] > arr[j + 1] {
                arr.swap(j, j + 1);
            }
        }
    }
}
}

复杂度比较(n = 1000 时):

复杂度操作次数
O(1)1
O(log n)~10
O(n)1,000
O(n log n)~10,000
O(n²)1,000,000
O(2ⁿ)约 10³⁰¹

当 n 很大时,算法复杂度的差异会变得极其显著。O(n²) 算法处理 10⁶ 数据可能需要数小时,而 O(n log n) 算法只需数秒。


二、算法思想

2.1 枚举法(Brute Force)

枚举法是最直接的算法思想:遍历所有可能的情况,逐一验证是否满足条件。

// 例:找出 1~100 中的所有素数
fn find_primes(limit: u32) -> Vec<u32> {
    let mut primes = Vec::new();
    for n in 2..=limit {
        let mut is_prime = true;
        for i in 2..n {
            if n % i == 0 {
                is_prime = false;
                break;
            }
        }
        if is_prime {
            primes.push(n);
        }
    }
    primes
}

fn main() {
    println!("{:?}", find_primes(50));
    // [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
}

枚举法简单直接,但效率通常不高。上面的素数查找可以优化到 O(n log log n)(埃拉托斯特尼筛法)。

2.2 分治法(Divide and Conquer)

分治法的核心思想:将大问题分解为若干相似的子问题,递归解决子问题,再合并结果

分治法的三个步骤:

  1. 分解:将原问题分解为若干子问题
  2. 解决:递归解决子问题(若子问题足够小,直接求解)
  3. 合并:将子问题的解合并为原问题的解
// 归并排序(Merge Sort)- 经典分治算法
fn merge_sort(arr: &mut [i32]) {
    let len = arr.len();
    if len <= 1 {
        return;
    }

    let mid = len / 2;
    merge_sort(&mut arr[..mid]);
    merge_sort(&mut arr[mid..]);

    // 合并两个有序数组
    let mut temp = arr.to_vec();
    merge(&arr[..mid], &arr[mid..], &mut temp);
    arr.copy_from_slice(&temp);
}

fn merge(left: &[i32], right: &[i32], result: &mut [i32]) {
    let (mut i, mut j, mut k) = (0, 0, 0);

    while i < left.len() && j < right.len() {
        if left[i] <= right[j] {
            result[k] = left[i];
            i += 1;
        } else {
            result[k] = right[j];
            j += 1;
        }
        k += 1;
    }

    while i < left.len() {
        result[k] = left[i];
        i += 1;
        k += 1;
    }

    while j < right.len() {
        result[k] = right[j];
        j += 1;
        k += 1;
    }
}

fn main() {
    let mut arr = [38, 27, 43, 3, 9, 82, 10];
    merge_sort(&mut arr);
    println!("{:?}", arr);  // [3, 9, 10, 27, 38, 43, 82]
}

时间复杂度分析: 归并排序的时间复杂度为 O(n log n),空间复杂度为 O(n)。

2.3 动态规划(Dynamic Programming)

动态规划(DP)的核心思想:将问题分解为重叠子问题,存储子问题的解以避免重复计算

DP 的两个关键要素:

  • 最优子结构:问题的最优解包含子问题的最优解
  • 重叠子问题:子问题会被重复计算多次
// 斐波那契数列 - 动态规划解法
fn fib_dp(n: u32) -> u64 {
    if n == 0 { return 0; }
    if n == 1 { return 1; }

    let mut dp = vec![0u64; (n + 1) as usize];
    dp[1] = 1;

    for i in 2..=n as usize {
        dp[i] = dp[i - 1] + dp[i - 2];
    }

    dp[n as usize]
}

// 空间优化版 - 只保留前两个状态
fn fib_dp_optimized(n: u32) -> u64 {
    if n == 0 { return 0; }
    if n == 1 { return 1; }

    let (mut prev, mut curr) = (0u64, 1u64);
    for _ in 2..=n {
        let next = prev + curr;
        prev = curr;
        curr = next;
    }
    curr
}

fn main() {
    println!("F(50) = {}", fib_dp(50));           // 12586269025
    println!("F(50) = {}", fib_dp_optimized(50));  // 12586269025
}

经典问题:0/1 背包问题

// 0/1 背包问题
// 有 n 个物品,每个物品有重量 weight[i] 和价值 value[i]
// 背包容量为 W,求能装入的最大价值

fn knapsack(weights: &[usize], values: &[usize], capacity: usize) -> usize {
    let n = weights.len();
    let mut dp = vec![vec![0; capacity + 1]; n + 1];

    for i in 1..=n {
        for w in 0..=capacity {
            if weights[i - 1] <= w {
                // 可以选择放入或不放入
                dp[i][w] = dp[i - 1][w].max(
                    dp[i - 1][w - weights[i - 1]] + values[i - 1]
                );
            } else {
                dp[i][w] = dp[i - 1][w];
            }
        }
    }

    dp[n][capacity]
}

fn main() {
    let weights = vec![2, 3, 4, 5];
    let values = vec![3, 4, 5, 6];
    let capacity = 8;

    println!("最大价值: {}", knapsack(&weights, &values, capacity));  // 10
}

时间复杂度: O(n × W),其中 n 是物品数量,W 是背包容量。

2.4 贪心算法(Greedy Algorithm)

贪心算法的核心思想:每一步都做出当前看起来最优的选择,希望最终得到全局最优解

贪心算法不一定能得到全局最优解,但在某些问题上是有效的。

// 找零钱问题(贪心解法)
// 假设硬币面额为 1, 5, 10, 25, 50
fn make_change(amount: usize) -> Vec<usize> {
    let coins = vec![50, 25, 10, 5, 1];
    let mut change = Vec::new();
    let mut remaining = amount;

    for &coin in &coins {
        while remaining >= coin {
            change.push(coin);
            remaining -= coin;
        }
    }

    change
}

fn main() {
    let amount = 87;
    let change = make_change(amount);
    println!("找 {} 分钱: {:?}", amount, change);
    // 找 87 分钱: [50, 25, 10, 1, 1]
    println!("硬币数量: {}", change.len());  // 5
}

注意: 贪心算法在标准硬币系统(如 1, 5, 10, 25)下能得到最优解,但在某些面额组合(如 1, 3, 4)下可能不是最优。此时需要使用动态规划。

2.5 回溯法(Backtracking)

回溯法的核心思想:系统地搜索问题的解空间,当发现当前路径不可能得到解时,回溯到上一步尝试其他路径

// 全排列问题
fn permutations(nums: &mut [i32]) -> Vec<Vec<i32>> {
    let mut result = Vec::new();
    backtrack(nums, 0, &mut result);
    result
}

fn backtrack(nums: &mut [i32], start: usize, result: &mut Vec<Vec<i32>>) {
    if start == nums.len() {
        result.push(nums.to_vec());
        return;
    }

    for i in start..nums.len() {
        nums.swap(start, i);           // 做选择
        backtrack(nums, start + 1, result);  // 递归
        nums.swap(start, i);           // 撤销选择(回溯)
    }
}

fn main() {
    let mut nums = [1, 2, 3];
    let perms = permutations(&mut nums);
    for p in &perms {
        println!("{:?}", p);
    }
    // [1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], [3, 1, 2]
}

三、搜索算法

3.1 深度优先搜索(DFS)

DFS 沿着一条路径尽可能深地搜索,直到无法继续才回溯。

// 图的深度优先搜索
use std::collections::{HashMap, HashSet};

struct Graph {
    adj: HashMap<i32, Vec<i32>>,
}

impl Graph {
    fn new() -> Self {
        Graph { adj: HashMap::new() }
    }

    fn add_edge(&mut self, u: i32, v: i32) {
        self.adj.entry(u).or_insert_with(Vec::new).push(v);
        self.adj.entry(v).or_insert_with(Vec::new).push(u);
    }

    fn dfs(&self, start: i32) -> Vec<i32> {
        let mut visited = HashSet::new();
        let mut result = Vec::new();
        self.dfs_helper(start, &mut visited, &mut result);
        result
    }

    fn dfs_helper(&self, node: i32, visited: &mut HashSet<i32>, result: &mut Vec<i32>) {
        visited.insert(node);
        result.push(node);

        if let Some(neighbors) = self.adj.get(&node) {
            for &neighbor in neighbors {
                if !visited.contains(&neighbor) {
                    self.dfs_helper(neighbor, visited, result);
                }
            }
        }
    }
}

fn main() {
    let mut graph = Graph::new();
    graph.add_edge(0, 1);
    graph.add_edge(0, 2);
    graph.add_edge(1, 3);
    graph.add_edge(1, 4);
    graph.add_edge(2, 5);

    println!("DFS: {:?}", graph.dfs(0));  // [0, 1, 3, 4, 2, 5]
}

3.2 广度优先搜索(BFS)

BFS 逐层扩展搜索,先访问所有邻居,再访问邻居的邻居。

// 图的广度优先搜索
use std::collections::{HashMap, HashSet, VecDeque};

struct Graph {
    adj: HashMap<i32, Vec<i32>>,
}

impl Graph {
    fn new() -> Self {
        Graph { adj: HashMap::new() }
    }

    fn add_edge(&mut self, u: i32, v: i32) {
        self.adj.entry(u).or_insert_with(Vec::new).push(v);
        self.adj.entry(v).or_insert_with(Vec::new).push(u);
    }

    fn bfs(&self, start: i32) -> Vec<i32> {
        let mut visited = HashSet::new();
        let mut queue = VecDeque::new();
        let mut result = Vec::new();

        visited.insert(start);
        queue.push_back(start);

        while let Some(node) = queue.pop_front() {
            result.push(node);

            if let Some(neighbors) = self.adj.get(&node) {
                for &neighbor in neighbors {
                    if !visited.contains(&neighbor) {
                        visited.insert(neighbor);
                        queue.push_back(neighbor);
                    }
                }
            }
        }

        result
    }
}

fn main() {
    let mut graph = Graph::new();
    graph.add_edge(0, 1);
    graph.add_edge(0, 2);
    graph.add_edge(1, 3);
    graph.add_edge(1, 4);
    graph.add_edge(2, 5);

    println!("BFS: {:?}", graph.bfs(0));  // [0, 1, 2, 3, 4, 5]
}

DFS vs BFS 对比:

  • DFS 使用栈(递归或显式栈),适合寻找路径、拓扑排序
  • BFS 使用队列,适合寻找最短路径(无权图)

四、经典排序算法

算法平均时间最坏时间空间稳定性特点
冒泡排序O(n²)O(n²)O(1)稳定简单,效率低
选择排序O(n²)O(n²)O(1)不稳定交换次数少
插入排序O(n²)O(n²)O(1)稳定对小数据高效
归并排序O(n log n)O(n log n)O(n)稳定稳定,需额外空间
快速排序O(n log n)O(n²)O(log n)不稳定平均最快
堆排序O(n log n)O(n log n)O(1)不稳定空间效率高
// 快速排序
fn quick_sort(arr: &mut [i32]) {
    if arr.len() <= 1 {
        return;
    }

    let pivot_index = partition(arr);
    let (left, right) = arr.split_at_mut(pivot_index);
    quick_sort(left);
    quick_sort(&mut right[1..]);
}

fn partition(arr: &mut [i32]) -> usize {
    let len = arr.len();
    let pivot = arr[len - 1];
    let mut i = 0;

    for j in 0..len - 1 {
        if arr[j] <= pivot {
            arr.swap(i, j);
            i += 1;
        }
    }

    arr.swap(i, len - 1);
    i
}

fn main() {
    let mut arr = [64, 34, 25, 12, 22, 11, 90];
    quick_sort(&mut arr);
    println!("{:?}", arr);  // [11, 12, 22, 25, 34, 64, 90]
}

五、经典问题选讲

5.1 八皇后问题

在 8×8 的棋盘上放置 8 个皇后,使它们互不攻击(不在同一行、列、对角线)。

fn solve_n_queens(n: usize) -> Vec<Vec<String>> {
    let mut board = vec![vec!['.'; n]; n];
    let mut result = Vec::new();
    backtrack_queen(&mut board, 0, &mut result);
    result
}

fn backtrack_queen(board: &mut Vec<Vec<char>>, row: usize, result: &mut Vec<Vec<String>>) {
    let n = board.len();
    if row == n {
        result.push(board.iter().map(|r| r.iter().collect()).collect());
        return;
    }

    for col in 0..n {
        if is_valid(board, row, col) {
            board[row][col] = 'Q';
            backtrack_queen(board, row + 1, result);
            board[row][col] = '.';
        }
    }
}

fn is_valid(board: &Vec<Vec<char>>, row: usize, col: usize) -> bool {
    let n = board.len();

    // 检查列
    for i in 0..row {
        if board[i][col] == 'Q' {
            return false;
        }
    }

    // 检查左上对角线
    for i in 1..=row {
        if col >= i && board[row - i][col - i] == 'Q' {
            return false;
        }
    }

    // 检查右上对角线
    for i in 1..=row {
        if col + i < n && board[row - i][col + i] == 'Q' {
            return false;
        }
    }

    true
}

fn main() {
    let solutions = solve_n_queens(8);
    println!("8 皇后问题共有 {} 种解法", solutions.len());  // 92
}

5.2 最长公共子序列(LCS)

// 动态规划求解最长公共子序列
fn longest_common_subsequence(text1: &str, text2: &str) -> usize {
    let m = text1.len();
    let n = text2.len();
    let mut dp = vec![vec![0; n + 1]; m + 1];

    let chars1: Vec<char> = text1.chars().collect();
    let chars2: Vec<char> = text2.chars().collect();

    for i in 1..=m {
        for j in 1..=n {
            if chars1[i - 1] == chars2[j - 1] {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]);
            }
        }
    }

    dp[m][n]
}

fn main() {
    println!("{}", longest_common_subsequence("ABCDE", "ACE"));     // 3 ("ACE")
    println!("{}", longest_common_subsequence("ABC", "DEF"));       // 0
}

六、算法学习资源

资源说明
TheAlgorithms开源算法库,支持多种编程语言
VisuAlgo算法可视化网站,直观理解算法执行过程
LeetCode算法练习题库
3Blue1Brown数学与算法的可视化讲解

七、总结

算法思想核心思想适用场景经典问题
枚举遍历所有可能解空间小素数查找
分治分解、解决、合并可分解为独立子问题归并排序、快速排序
动态规划存储子问题解最优子结构、重叠子问题背包问题、LCS
贪心局部最优具有贪心选择性质找零钱、活动选择
回溯深度搜索+剪枝解空间树搜索八皇后、全排列

算法是编程的灵魂。掌握这些基本算法思想,能够帮助我们更高效地解决各种计算问题。

练习建议:

  1. 实现一个二分查找算法
  2. 用动态规划解决斐波那契数列问题
  3. 尝试解决 LeetCode 上的经典算法题