数据结构与算法

数据结构与算法是计算机科学的核心基础。数据结构解决「数据怎么组织」,算法解决「问题怎么解决」,二者结合决定了程序的效率与正确性。

本教程内容

模块内容
基础概念复杂度分析、线性结构、哈希表
经典算法分治、贪心、动态规划
排序插入/希尔/归并/快排及其变种
堆堆存储、上浮下沉、堆排序、索引堆
树二分搜索树、遍历、删除
并查集查找、合并、优化、路径压缩
图表示、DFS/BFS、寻路、最短路径

为什么重要

  • 面试必考:大厂技术面试的核心环节
  • 写出高效代码:同样的功能,数据结构选对性能差百倍
  • 理解底层:数据库索引(树)、缓存(哈希表)、网络(图)
✅ 学习方法
本教程的代码以 Python / C++ 演示核心逻辑。建议动手敲代码、画图理解过程,而不是死记结论。

数据结构与算法简介

认识数据结构与算法的基本概念,建立全局认知框架。

数据结构

数据结构是数据的组织方式。选择合适的数据结构决定操作效率:

结构特点典型场景
数组连续内存、按下标 O(1) 访问静态列表、矩阵
链表动态插入删除快LRU 缓存、撤销栈
栈后进先出 (LIFO)函数调用、括号匹配
队列先进先出 (FIFO)任务调度、BFS
哈希表O(1) 增删查缓存、去重、索引
树层级关系、有序文件系统、数据库索引
图节点与边的关系社交网络、地图导航

算法

算法是解决问题的步骤描述。好的算法特征:

  • 正确性:所有输入都能给出正确结果
  • 高效性:时间、空间复杂度可控
  • 可读性:逻辑清晰易维护

一个简单示例

# 线性查找 vs 二分查找(有序数组)
def linear_search(arr, target):
    for i, v in enumerate(arr):
        if v == target:
            return i
    return -1

def binary_search(arr, target):
    lo, hi = 0, len(arr) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        if arr[mid] == target: return mid
        elif arr[mid] < target: lo = mid + 1
        else: hi = mid - 1
    return -1
💡 核心
数据结构与算法是「一体两面」:数据结构是算法的载体,算法是数据结构的运用。学习时把两者结合理解。

算法基础与分析方法

掌握时间复杂度与空间复杂度分析,学会评估算法优劣。

时间复杂度 (Big-O)

描述算法运行时间随输入规模 n 的增长趋势(忽略常数与低阶项):

复杂度含义示例
O(1)常数时间数组按下标访问
O(log n)对数时间二分查找
O(n)线性时间遍历数组
O(n log n)线性对数归并/快排
O(n²)平方时间冒泡、嵌套循环
O(2ⁿ)指数时间暴力枚举子集

分析步骤

def sum_all(arr):
    total = 0              # O(1)
    for v in arr:          # n 次
        total += v         # 每次 O(1)
    return total
# 总复杂度: O(n)

def nested(arr):
    n = len(arr)
    for i in range(n):        # n 次
        for j in range(n):    # n 次
            print(i, j)       # O(1)
# 总复杂度: O(n²)

常见复杂度对比

n=1000 时大致操作次数:
O(log n)  ~10     ✅ 极快
O(n)      ~1000   ✅ 快
O(n log n)~10000  👍 可接受
O(n²)     ~10⁶    ⚠️ 慢
O(2ⁿ)     ~10³⁰⁰  ❌ 不可行

空间复杂度

def clone(arr):
    return arr[:]        # 额外 O(n) 空间

def inplace(arr):
    arr.reverse()        # O(1) 额外空间
    return arr
✅ 速算技巧
看循环嵌套层数定「数量级」:1 层≈O(n)、2 层≈O(n²);每次规模减半≈O(log n);分治递归≈O(n log n)。

线性数据结构

线性数据结构:数组、链表、栈、队列——元素按线性顺序排列,是其他结构的基础。

数组 (Array)

arr = [3, 1, 4, 1, 5]
arr[0]        # O(1) 访问
arr.append(9) # 均摊 O(1) 尾部插入
# 中间插入/删除 O(n)(需移动元素)

链表 (Linked List)

class Node:
    def __init__(self, val):
        self.val = val
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None

    def prepend(self, val):   # O(1) 头部插入
        node = Node(val)
        node.next = self.head
        self.head = node

    def traverse(self):
        cur = self.head
        while cur:
            print(cur.val, end=" ")
            cur = cur.next

栈 (Stack, LIFO)

stack = []
stack.append(1)    # push
stack.append(2)
stack.pop()        # 弹出 2 (后进先出)

# 应用: 括号匹配
def is_valid(s):
    pairs = {")": "(", "]": "[", "}": "{"}
    stack = []
    for c in s:
        if c in pairs:
            if not stack or stack.pop() != pairs[c]:
                return False
        else:
            stack.append(c)
    return not stack

队列 (Queue, FIFO)

from collections import deque
q = deque()
q.append(1)      # enqueue
q.append(2)
q.popleft()      # 弹出 1 (先进先出)

# 应用: BFS 层级遍历
✅ 对比
数组:访问快、插入删慢;链表:插入删快、访问慢;栈/队列是受限的线性结构,专用于特定顺序处理。

哈希表

哈希表(Hash Table)通过哈希函数把键映射到存储位置,实现 O(1) 平均复杂度的增删查。

原理

键 → 哈希函数 → 数组下标 → 存储值

例: hash("apple") = 3 → table[3] = "苹果"
例: hash("banana") = 7 → table[7] = "香蕉"

Python 内置

# dict / set 就是哈希表
d = {}
d["name"] = "张三"     # O(1) 插入
d.get("name")          # O(1) 查找
"name" in d            # O(1) 判断存在
del d["name"]          # O(1) 删除

# set: 去重
nums = [1, 2, 2, 3, 3, 3]
unique = set(nums)     # {1, 2, 3}

哈希冲突与解决

不同键哈希到同一位置称为冲突,常见解决方式:

方法思路
链地址法每个位置存链表,冲突元素挂后面
开放寻址冲突时找下一个空位
再哈希用另一个哈希函数重新计算

应用:两数之和

def two_sum(nums, target):
    """找到两个数下标使其和等于 target"""
    seen = {}                  # 值 → 下标
    for i, v in enumerate(nums):
        need = target - v
        if need in seen:       # O(1) 查找
            return [seen[need], i]
        seen[v] = i
    return []
# 时间复杂度 O(n), 空间 O(n)
⚠️ 注意
哈希表不保证顺序;键需可哈希(Python 中 list/dict 不可作键)。极端哈希冲突时退化为 O(n)。

树形结构

树(Tree)是层级化的非线性结构:一个根节点、若干子树,模拟「一对多」的关系。

基本概念

        根(1)
       /         (2)    (3)
    /  \        (4)  (5)    (6)

- 节点: 1~6
- 根: 1 (无父节点)
- 叶子: 4,5,6 (无子节点)
- 父/子: 1 是 2,3 的父; 2,3 是 1 的子
- 兄弟: 2,3
- 高度: 从根到最远叶子的边数
- 深度: 从根到该节点的边数

二叉树定义

class TreeNode:
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None

常见树类型

类型特点
二叉树每个节点最多 2 个子节点
二分搜索树 (BST)左小右大,查找 O(log n)
平衡树 (AVL/红黑树)自动保持平衡
堆 (Heap)父大于子(大顶堆)
B 树/B+树多路搜索树,数据库索引

树的遍历框架

def preorder(root):       # 前序: 根左右
    if not root: return
    print(root.val)
    preorder(root.left)
    preorder(root.right)

def inorder(root):        # 中序: 左根右
    if not root: return
    inorder(root.left)
    print(root.val)
    inorder(root.right)

def postorder(root):      # 后序: 左右根
    if not root: return
    postorder(root.left)
    postorder(root.right)
    print(root.val)
💡 应用
文件系统目录、HTML DOM、公司组织架构、数据库索引——树无处不在。

图论结构

图(Graph)由节点(Vertex)和边(Edge)组成,表达「多对多」的关系,是网络类问题的通用模型。

基本概念

概念说明
有向图边有方向 (A→B ≠ B→A)
无向图边无方向 (A—B)
带权图边上有权重(距离/成本)
连通分量互相可达的节点集合
度节点相连的边数(有向分入度/出度)

表示方式

# 邻接矩阵 (n×n, 适合稠密图)
#      A B C
#  A  [0 1 1]
#  B  [1 0 0]
#  C  [1 0 0]
matrix = [[0,1,1],[1,0,0],[1,0,0]]

# 邻接表 (适合稀疏图, 常用)
graph = {
    "A": ["B", "C"],
    "B": ["A"],
    "C": ["A"],
}

图遍历

def dfs(graph, node, visited=None):
    """深度优先遍历"""
    if visited is None: visited = set()
    visited.add(node)
    print(node, end=" ")
    for nxt in graph[node]:
        if nxt not in visited:
            dfs(graph, nxt, visited)

def bfs(graph, start):
    """广度优先遍历"""
    from collections import deque
    visited, q = {start}, deque([start])
    while q:
        node = q.popleft()
        print(node, end=" ")
        for nxt in graph[node]:
            if nxt not in visited:
                visited.add(nxt)
                q.append(nxt)

图的应用

  • 社交网络:好友关系(无向图)
  • 导航:最短路径(带权有向图)
  • 任务调度:依赖关系(有向无环图)
  • 网络拓扑:路由器连接
✅ 选型
边少节点多 → 邻接表(省内存);判断两点是否直接相连频繁 → 邻接矩阵(O(1))。

分治算法

分治(Divide and Conquer):把大问题拆成小问题,递归解决后合并结果。归并排序、快速排序都是分治思想。

三步骤

1. 分解(Divide): 把问题分成规模更小的子问题
2. 解决(Conquer): 递归解决子问题(到最小规模直接解决)
3. 合并(Combine): 合并子问题的解得到原问题的解

示例:归并排序

def merge_sort(arr):
    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(a, b):
    i = j = 0
    res = []
    while i < len(a) and j < len(b):
        if a[i] <= b[j]:
            res.append(a[i]); i += 1
        else:
            res.append(b[j]); j += 1
    res.extend(a[i:])
    res.extend(b[j:])
    return res

# 复杂度: 时间 O(n log n), 空间 O(n)

示例:最大子数组和

def max_subarray(arr):
    """分治法求最大连续子数组和"""
    def helper(lo, hi):
        if lo == hi:          # 单个元素
            return arr[lo]
        mid = (lo + hi) // 2
        # 三种情况取最大: 全左 / 全右 / 跨中点
        left = helper(lo, mid)
        right = helper(mid+1, hi)

        # 跨中点的最大和
        lmax, s = float("-inf"), 0
        for i in range(mid, lo-1, -1):
            s += arr[i]; lmax = max(lmax, s)
        rmax, s = float("-inf"), 0
        for i in range(mid+1, hi+1):
            s += arr[i]; rmax = max(rmax, s)
        return max(left, right, lmax + rmax)

    return helper(0, len(arr)-1)

适用特征

  • 问题可以递归分解为同构子问题
  • 子问题相互独立(无重复子问题,否则用动态规划)
  • 子问题的解可以高效合并
✅ 复杂度
分治递归式 T(n) = aT(n/b) + O(n^k),常用主定理求解;归并排序即 T(n) = 2T(n/2) + O(n) → O(n log n)。

贪心算法

贪心算法(Greedy):每一步都选择当前看起来最优的方案,希望通过局部最优得到全局最优。并非所有问题都适用,但适用时极其高效。

贪心策略步骤

1. 定义「贪心选择」: 每步选什么
2. 证明贪心选择是安全的(局部最优→全局最优)
3. 递归/迭代地做贪心选择, 缩减问题规模

经典示例:找零钱

def coin_change_greedy(amount, coins=(25, 10, 5, 1)):
    """美分制找零(面额满足贪心性质)"""
    result = {}
    for c in coins:
        result[c] = amount // c
        amount %= c
    return result

# 注意: 面额 {1,3,4} 时贪心对 amount=6 得 4+1+1 (3枚)
# 但最优是 3+3 (2枚) → 贪心不适用!

示例:活动选择(区间调度)

def max_activities(activities):
    """最多能参加的活动数(活动为(开始,结束))"""
    # 贪心: 每次选结束最早且与已选不冲突的活动
    activities.sort(key=lambda x: x[1])   # 按结束时间排序
    count, last_end = 0, 0
    for start, end in activities:
        if start >= last_end:
            count += 1
            last_end = end
    return count

print(max_activities([(1,3),(2,5),(3,6),(5,7),(6,8)]))  # 3

示例:跳跃游戏

def can_jump(nums):
    """能否从下标0跳到末尾(每步最多跳 nums[i])"""
    max_reach = 0
    for i, step in enumerate(nums):
        if i > max_reach:      # 到不了当前位置
            return False
        max_reach = max(max_reach, i + step)
    return True

print(can_jump([2,3,1,1,4]))  # True
print(can_jump([3,2,1,0,4]))  # False

何时用贪心

适用特征
✅ 适用活动选择、区间覆盖、最小生成树、哈夫曼编码、部分背包
❌ 不适用0-1 背包、硬币问题(非规范面额)、旅行商
⚠️ 谨慎
贪心必须证明贪心选择性质与最优子结构;无法证明时改用动态规划。考试中贪心题通常有明显「排序后选」的模式。

动态规划

动态规划(Dynamic Programming, DP):把问题拆成重叠子问题,存储子问题的解避免重复计算,常用于最优化问题。

核心要素

1. 状态定义: dp[i] 表示什么
2. 状态转移: dp[i] 由哪些子状态推出(递推式)
3. 初始化:   边界条件
4. 遍历顺序: 确保计算 dp[i] 时子状态已就绪

示例:斐波那契(记忆化 vs 迭代)

# 朴素递归: O(2^n) 大量重复计算
def fib_naive(n):
    if n <= 1: return n
    return fib_naive(n-1) + fib_naive(n-2)

# 迭代 DP: O(n)
def fib_dp(n):
    if n <= 1: return n
    dp = [0] * (n + 1)
    dp[1] = 1
    for i in range(2, n + 1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]

print(fib_dp(50))   # 12586269025 (瞬间)

示例:爬楼梯

def climb_stairs(n):
    """每次爬1或2阶, 到第n阶有多少种方法"""
    if n <= 2: return n
    a, b = 1, 2            # dp[1], dp[2]
    for _ in range(3, n+1):
        a, b = b, a + b    # 滚动数组: dp[i]=dp[i-1]+dp[i-2]
    return b

示例:0-1 背包

def knapsack(weights, values, capacity):
    """0-1背包: 选物品使总价值最大, 总重量≤capacity"""
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        w, v = weights[i-1], values[i-1]
        for c in range(capacity + 1):
            if w > c:
                dp[i][c] = dp[i-1][c]      # 不选
            else:
                dp[i][c] = max(dp[i-1][c],
                               dp[i-1][c-w] + v)   # 选
    return dp[n][capacity]

print(knapsack([2,3,4,5], [3,4,5,6], 8))  # 10

DP vs 分治 vs 贪心

算法子问题关系选择策略
分治独立递归拆分合并
贪心独立(每步最优)局部最优选择
DP重叠存储子解避免重复
✅ 识别信号
看到「最大/最小/方案数、不能重复使用」→ 大概率 DP。写 DP 前先把状态定义写清楚,这是最重要的一步。

高级树结构

高级树结构:AVL 树、红黑树、B 树/B+树、Trie——在基础树上加入平衡或特殊约束以优化特定操作。

AVL 树(严格平衡)

# 每个节点记录平衡因子(左右子树高度差 ∈ {-1,0,1})
# 插入/删除后, 若 |平衡因子| > 1 则旋转:
#   LL → 右旋, RR → 左旋
#   LR → 先左旋再右旋, RL → 先右旋再左旋
# 查找 O(log n), 插入/删除 O(log n) 但旋转次数多

红黑树(近似平衡)

规则:
1. 节点非红即黑
2. 根是黑
3. 红节点的子节点都是黑(不连续红)
4. 每个节点到叶子的路径黑节点数相同

特点: 插入/删除最多 O(1) 旋转,
     平衡略松但统计性能更优
应用: Java TreeMap/HashMap, Linux 进程调度, Python 无(用 dict)

B 树 / B+树(多路)

B树: 一个节点存多个键, 多子节点(不是二叉)
     → 减少磁盘 IO(一次读一页)
B+树: 数据只在叶子, 内部节点只存索引
     → 叶子链表连接, 适合范围查询
应用: MySQL InnoDB 索引, 文件系统

Trie(前缀树)

class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_end = False

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word):
        node = self.root
        for c in word:
            node = node.children.setdefault(c, TrieNode())
        node.is_end = True

    def search(self, word):
        node = self.root
        for c in word:
            if c not in node.children:
                return False
            node = node.children[c]
        return node.is_end

    def starts_with(self, prefix):
        node = self.root
        for c in prefix:
            if c not in node.children:
                return False
            node = node.children[c]
        return True

# 应用: 输入法联想、拼写检查、IP 路由
💡 选型
需要严格 O(log n) → AVL;读写均衡、插入频繁 → 红黑树;磁盘存储 → B+树;字符串前缀 → Trie。

高级图算法

高级图算法:最小生成树、最短路径、拓扑排序——解决工程中的网络优化与依赖问题。

最小生成树 (MST)

# Prim 算法: 从一点出发, 每次加入最近的未选点
import heapq

def prim(graph, start=0):
    """graph: 邻接表 [(邻居, 权重)], 返回总权重"""
    n = len(graph)
    visited = [False] * n
    pq = [(0, start)]        # (权重, 节点)
    total = 0
    while pq:
        w, u = heapq.heappop(pq)
        if visited[u]: continue
        visited[u] = True
        total += w
        for v, w2 in graph[u]:
            if not visited[v]:
                heapq.heappush(pq, (w2, v))
    return total

# 应用: 通信网络布线、电路设计

最短路径:Dijkstra

def dijkstra(graph, start):
    """单源最短路径(边权非负)"""
    n = len(graph)
    dist = [float("inf")] * n
    dist[start] = 0
    pq = [(0, start)]
    while pq:
        d, u = heapq.heappop(pq)
        if d > dist[u]: continue
        for v, w in graph[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                heapq.heappush(pq, (dist[v], v))
    return dist

# 应用: 地图导航、网络路由
# 边权含负 → 用 Bellman-Ford; 多源 → Floyd

拓扑排序(有向无环图)

def topo_sort(graph):
    """课程表问题: 依赖顺序。返回拓扑序或空(有环)"""
    from collections import deque
    n = len(graph)
    indeg = [0] * n
    for u in range(n):
        for v in graph[u]:
            indeg[v] += 1

    q = deque(i for i in range(n) if indeg[i] == 0)
    order = []
    while q:
        u = q.popleft()
        order.append(u)
        for v in graph[u]:
            indeg[v] -= 1
            if indeg[v] == 0:
                q.append(v)
    return order if len(order) == n else []   # 空=有环

# 应用: 编译依赖、任务调度、课程先修

其他高级算法

算法问题
Floyd-Warshall所有点对最短路径
Bellman-Ford含负权边的最短路径
KruskalMST(并查集实现)
Tarjan强连通分量
✅ 记忆
正权最短路 → Dijkstra(堆优化 O(E log V));检测环 → 拓扑排序;连通性 → 并查集;最小连接成本 → MST。

排序算法

排序算法总览:对比各算法的时间/空间复杂度与稳定性,为后续逐个深入学习建立框架。

排序算法对比

算法最好平均最坏空间稳定
插入排序O(n)O(n²)O(n²)O(1)✅
希尔排序O(n^1.3)~O(n^1.5)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)❌
堆排序O(n log n)O(n log n)O(n log n)O(1)❌

稳定性含义

稳定: 相等元素排序后保持原相对顺序
例如按价格排序后, 同价商品仍按原顺序排列
- 插入/归并: 稳定 (相等时不交换)
- 快排/堆排: 不稳定 (可能交换相等元素)

如何选择

场景推荐
基本有序的小数组插入排序(近乎 O(n))
大数据量、需稳定归并排序
大数据量、内存敏感堆排序(O(1) 空间)
通用默认快速排序(工程实现常用)

Python 内置

arr.sort()                    # 原地排序(稳定, Timsort)
sorted(arr)                   # 返回新列表
arr.sort(reverse=True)        # 降序
arr.sort(key=lambda x: x[1])  # 按第二个元素排序
# Python 默认的 Timsort = 归并+插入的混合, O(n log n)
✅ 学习顺序
本教程将依次深入:插入 → 希尔 → 归并 → 快速(及随机化/双路/三路变种)→ 堆排序。理解「为什么快/慢」比背代码重要。

插入排序

插入排序(Insertion Sort):像打扑克牌一样,把每个元素插入到前面已排序序列的正确位置。

算法思想

初始: [5, 2, 4, 6, 1]
 第1轮: 把2插到5前 → [2, 5, 4, 6, 1]
 第2轮: 把4插到5前 → [2, 4, 5, 6, 1]
 第3轮: 6已就位 → [2, 4, 5, 6, 1]
 第4轮: 把1插到最前 → [1, 2, 4, 5, 6]

实现

def insertion_sort(arr):
    n = len(arr)
    for i in range(1, n):
        key = arr[i]           # 待插入元素
        j = i - 1
        while j >= 0 and arr[j] > key:   # 向前找位置
            arr[j + 1] = arr[j]           # 后移
            j -= 1
        arr[j + 1] = key       # 插入
    return arr

print(insertion_sort([5, 2, 4, 6, 1]))  # [1,2,4,5,6]

复杂度分析

情况复杂度说明
最好(已有序)O(n)内层 while 不执行
平均O(n²)每个元素平均后移一半
最坏(逆序)O(n²)每个元素都移到最前

优化:减少交换

def insertion_sort_opt(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
    return arr

特点

  • 稳定:相等元素不交换
  • 原地:O(1) 额外空间
  • 自适应:数据越有序越快
  • 适合小数组或近乎有序的数组(如 Timsort 用它的优化版)
✅ 工程价值
插入排序虽然平均 O(n²),但在「近乎有序」场景几乎 O(n),常作为归并/快排对小规模子数组的兜底。

希尔排序

希尔排序(Shell Sort):插入排序的改进——先按大间隔分组插入,逐步缩小间隔,最后间隔为 1 时就是普通插入排序。

算法思想

间隔序列(常用): n/2, n/4, ..., 1

例: [9, 8, 7, 6, 5, 4, 3, 2, 1], n=9
间隔4: 分组 (9,5,1) (8,4) (7,3) (6,2) 组内插入排序
间隔2: 分组 (9,7,5,3,1) (8,6,4,2) 组内插入排序
间隔1: 全数组插入排序(此时已接近有序, 很快)

「先粗排后细排」→ 减少元素移动总距离

实现

def shell_sort(arr):
    n = len(arr)
    gap = n // 2
    while gap > 0:
        # 对每个间隔分组做插入排序
        for i in range(gap, n):
            key = arr[i]
            j = i
            while j >= gap and arr[j - gap] > key:
                arr[j] = arr[j - gap]
                j -= gap
            arr[j] = key
        gap //= 2
    return arr

print(shell_sort([9, 8, 7, 6, 5, 4, 3, 2, 1]))

间隔序列选择

间隔序列复杂度
n/2, n/4, ..., 1O(n²)
2^k - 1 (Hibbard)O(n^1.5)
3^k - 1 / 2 (Sedgewick)O(n^4/3)~O(n^7/6)

特点

  • 不稳定(间隔分组打乱相对顺序)
  • 原地排序,O(1) 空间
  • 比简单插入排序快,但弱于 O(n log n) 算法
  • 适合中等规模数据
✅ 为什么有效
大间隔让元素快速「跳跃」到大致位置,最后小间隔微调。元素移动总次数远少于纯插入排序。

归并排序

归并排序(Merge Sort):分治思想的经典应用——把数组对半拆分到最小,再两两有序合并。

算法流程

[5,2,4,6,1,3]
    /        [5,2,4]    [6,1,3]
  /   \      /   [5] [2,4]  [6] [1,3]
     / \        /    [2] [4]    [1] [3]
     \ /        \ /
   [2,4]      [1,3]
    \           /
 [2,4,5]    [1,3,6]
       \      /
   [1,2,3,4,5,6]

实现

def merge_sort(arr):
    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(a, b):
    i = j = 0
    res = []
    while i < len(a) and j < len(b):
        if a[i] <= b[j]:        # 等号保证稳定
            res.append(a[i]); i += 1
        else:
            res.append(b[j]); j += 1
    res.extend(a[i:])
    res.extend(b[j:])
    return res

原地归并(节省空间)

def merge_inplace(arr, lo, mid, hi):
    """把 arr[lo:mid] 与 arr[mid:hi] 合并"""
    left = arr[lo:mid]
    right = arr[mid:hi]
    i = j = 0
    k = lo
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            arr[k] = left[i]; i += 1
        else:
            arr[k] = right[j]; j += 1
        k += 1
    while i < len(left):
        arr[k] = left[i]; i += 1; k += 1
    while j < len(right):
        arr[k] = right[j]; j += 1; k += 1

复杂度与特点

  • 时间:O(n log n)(稳定,最好最坏都是)
  • 空间:O(n)(需辅助数组)
  • 稳定:合并时相等元素取左边
  • 适合链表排序(无需随机访问)

归并思想的应用

# 逆序对计数
def count_inversions(arr):
    """利用归并过程统计逆序对数量"""
    if len(arr) <= 1: return arr, 0
    mid = len(arr) // 2
    left, inv1 = count_inversions(arr[:mid])
    right, inv2 = count_inversions(arr[mid:])
    merged, inv3 = [], 0
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            merged.append(left[i]); i += 1
        else:
            merged.append(right[j])
            inv3 += len(left) - i     # 关键
            j += 1
    merged += left[i:] + right[j:]
    return merged, inv1 + inv2 + inv3
✅ 价值
归并排序是「分治 + 合并」模板的最佳教材;其合并过程的比较逻辑可迁移到求逆序对、求区间和等难题。

随机化快速排序

快速排序(Quick Sort):选一个基准(pivot),把小于它的放左边、大于的放右边,然后递归两边。**随机化**选取基准避免最坏情况。

基本思想

选基准 pivot(如最后一个元素)
分区: [< pivot] [pivot] [> pivot]
递归排序左右两区

例: [3,1,4,1,5,9,2], pivot=2
分区后: [1,1] [2] [3,4,5,9]
递归: [1,1] → [1,1]; [3,4,5,9] → 继续

随机化快排实现

import random

def quick_sort(arr, lo=0, hi=None):
    if hi is None: hi = len(arr) - 1
    if lo >= hi: return arr

    # 随机选基准, 与最后一个交换 → 期望 O(n log n)
    pi = random.randint(lo, hi)
    arr[pi], arr[hi] = arr[hi], arr[pi]

    pivot = arr[hi]
    i = lo                      # 已分区的小于基准部分的边界
    for j in range(lo, hi):
        if arr[j] < pivot:      # 严格小于 → 稳定?不, 快排本就不稳定
            arr[i], arr[j] = arr[j], arr[i]
            i += 1
    arr[i], arr[hi] = arr[hi], arr[i]   # 基准归位

    quick_sort(arr, lo, i - 1)  # 递归左
    quick_sort(arr, i + 1, hi)  # 递归右
    return arr

print(quick_sort([3, 1, 4, 1, 5, 9, 2]))

为什么随机化

固定选第一个/最后一个为基准时:
- 已有序数组 → 每次分区极度不均 → O(n²) ❌
随机选基准 → 期望平均 O(n log n) ✅
几乎不可能每次都选中极值

复杂度

情况复杂度
平均(随机化)O(n log n)
最好(每次正好对半)O(n log n)
最坏(每次都选中极值)O(n²)(概率极低)
空间O(log n)(递归栈)

应用:快速选择(第 k 大)

def quick_select(arr, k):
    """找第 k 小元素 (k 从 0 开始), 平均 O(n)"""
    lo, hi = 0, len(arr) - 1
    while lo <= hi:
        pi = random.randint(lo, hi)
        arr[pi], arr[hi] = arr[hi], arr[pi]
        pivot = arr[hi]
        i = lo
        for j in range(lo, hi):
            if arr[j] < pivot:
                arr[i], arr[j] = arr[j], arr[i]
                i += 1
        arr[i], arr[hi] = arr[hi], arr[i]
        if i == k: return arr[i]
        elif i < k: lo = i + 1
        else: hi = i - 1

print(quick_select([3,1,4,1,5,9,2], 2))   # 第3小=2
✅ 记忆
快排核心 = 「分区」:把 pivot 放对位置,两边递归。随机化是工程标配,避免有序输入退化。

双路快速排序

双路快速排序(Two-Way Quick Sort):针对大量重复元素优化的分区方式——左右两个指针同时扫描,把相等的元素均匀分到两边。

问题:大量重复元素

普通分区 [pivot]:
 数组 [1,1,1,1,1,2] 选 pivot=1 时:
 所有 1 都进"小于或等于"区 → 分区严重失衡 → O(n²)

双路分区: 把等于 pivot 的元素分到两边 → 平衡

双路分区实现

import random

def quick_sort_2way(arr, lo=0, hi=None):
    if hi is None: hi = len(arr) - 1
    if lo >= hi: return arr

    pi = random.randint(lo, hi)
    arr[pi], arr[lo] = arr[lo], arr[pi]   # pivot 放最左
    pivot = arr[lo]

    i, j = lo + 1, hi        # 左右指针
    while True:
        # 左指针右移, 找 >= pivot 的元素
        while i <= hi and arr[i] < pivot:
            i += 1
        # 右指针左移, 找 <= pivot 的元素
        while j >= lo + 1 and arr[j] > pivot:
            j -= 1
        if i > j: break       # 交错则停止
        arr[i], arr[j] = arr[j], arr[i]   # 交换
        i += 1; j -= 1

    arr[lo], arr[j] = arr[j], arr[lo]     # pivot 归位
    quick_sort_2way(arr, lo, j - 1)
    quick_sort_2way(arr, j + 1, hi)
    return arr

# 关键: 条件用 < 和 > (等于的元素两边都收) → 平衡

对比

分区方式重复元素性能
单路全部堆到一边退化 O(n²)
双路均匀分两边O(n log n)
三路(下章)单独成区O(n)(全相同时)

正确性要点

1. pivot 先放最左, 最后与 arr[j] 交换(j 是 <=pivot 区边界)
2. 双指针: 左找大、右找小, 相遇交换
3. 循环条件是 while True + break(i > j), 
   保证 j 最终指向 <=pivot 的位置
4. 等于 pivot 的元素被<左右两个方向>吸收 → 平衡
✅ 适用
数据含大量重复值(如评分、状态码、年龄)时,双路/三路快排显著优于普通快排。Python 的 sort 内置了类似优化。

三路排序算法

三路快速排序(Three-Way Quick Sort):把数组分成 小于 / 等于 / 大于 三个区,等于 pivot 的元素一次性归位,不再递归。

分区结果

[ < pivot ] [ == pivot ] [ > pivot ]
     ↑递归       无需递归       ↑递归

对全相同数组: 一次分区就完成, O(n) ✅

实现

import random

def quick_sort_3way(arr, lo=0, hi=None):
    if hi is None: hi = len(arr) - 1
    if lo >= hi: return arr

    pi = random.randint(lo, hi)
    arr[pi], arr[lo] = arr[lo], arr[pi]
    pivot = arr[lo]

    lt = lo        # arr[lo+1..lt] < pivot
    i = lo + 1     # 扫描指针
    gt = hi        # arr[gt..hi] > pivot

    while i <= gt:
        if arr[i] < pivot:
            arr[lt+1], arr[i] = arr[i], arr[lt+1]
            lt += 1; i += 1
        elif arr[i] > pivot:
            arr[gt], arr[i] = arr[i], arr[gt]
            gt -= 1          # i 不动(换来的元素未检查)
        else:
            i += 1

    arr[lo], arr[lt] = arr[lt], arr[lo]   # pivot 区归位

    quick_sort_3way(arr, lo, lt - 1)      # 递归 <区
    quick_sort_3way(arr, gt + 1, hi)      # 递归 >区
    return arr

print(quick_sort_3way([3,1,4,1,5,9,2,1,3,1]))

三指针的含义

指针维护区间
lt[lo..lt] 全部 < pivot
i[lt+1..i-1] 全部 == pivot(扫描区)
gt[gt..hi] 全部 > pivot

复杂度

情况复杂度
全相同O(n)(一次分区完成)
平均O(n log n)
空间O(log n) 递归栈

应用:荷兰国旗问题

def dutch_flag(arr):
    """只含 0/1/2, 排序(本质是三路分区思想)"""
    lo, i, hi = 0, 0, len(arr) - 1
    while i <= hi:
        if arr[i] == 0:
            arr[lo], arr[i] = arr[i], arr[lo]
            lo += 1; i += 1
        elif arr[i] == 2:
            arr[hi], arr[i] = arr[i], arr[hi]
            hi -= 1
        else:
            i += 1
    return arr

print(dutch_flag([2,0,1,2,1,0,0,2]))  # [0,0,0,1,1,2,2,2]
✅ 总结
三路快排 = 快排 + 「相等元素不递归」。对于重复多的真实数据(如数据库去重排序),这是最实用的快排变体。

排序算法衍生问题

排序不只是「排好序」——利用排序后的性质可解决大量衍生问题:第 k 大、找重复、区间合并、计数排序思想等。

1. 第 k 大 / 第 k 小

def kth_largest(nums, k):
    """基于快排分区, 平均 O(n)"""
    return quick_select(nums, len(nums) - k)  # 复用上章

# 或直接排序 O(n log n) (数据量小也够用)
def kth_largest_simple(nums, k):
    return sorted(nums, reverse=True)[k-1]

2. 找重复元素

def find_duplicates(nums):
    """排序后相邻相等即重复"""
    nums.sort()
    res = []
    for i in range(1, len(nums)):
        if nums[i] == nums[i-1] and (not res or res[-1] != nums[i]):
            res.append(nums[i])
    return res

# 哈希法 O(n) 空间 O(n)
def find_duplicates_hash(nums):
    seen, res = set(), set()
    for v in nums:
        if v in seen: res.add(v)
        seen.add(v)
    return list(res)

3. 合并区间

def merge_intervals(intervals):
    """合并重叠区间"""
    if not intervals: return []
    intervals.sort(key=lambda x: x[0])   # 按起点排序
    merged = [intervals[0]]
    for s, e in intervals[1:]:
        last_s, last_e = merged[-1]
        if s <= last_e:                  # 重叠
            merged[-1][1] = max(last_e, e)
        else:
            merged.append([s, e])
    return merged

print(merge_intervals([[1,3],[2,6],[8,10],[15,18]]))
# [[1,6],[8,10],[15,18]]

4. 计数排序(非比较排序)

def counting_sort(arr, max_val):
    """适合取值范围的整数, O(n+k)"""
    count = [0] * (max_val + 1)
    for v in arr:
        count[v] += 1
    res = []
    for v, c in enumerate(count):
        res.extend([v] * c)
    return res

print(counting_sort([3,1,2,3,1,0,2], 3))  # [0,1,1,2,2,3,3]

5. 找多数元素

def majority_element(nums):
    """出现次数 > n/2 的元素(排序后中间位置必是它)"""
    nums.sort()
    return nums[len(nums) // 2]
# 更优: Boyer-Moore 投票 O(n) O(1), 不用排序
✅ 思路
排序是「预处理」:排序后,相邻比较、二分、双指针等技巧就能施展。遇到区间类、前 k 类问题先想想排序。

堆的基本存储

堆(Heap)是一棵完全二叉树,用数组存储,满足堆序性质:大顶堆父 ≥ 子,小顶堆父 ≤ 子。堆是实现优先队列、堆排序的基础。

数组存储完全二叉树

        62
       /       41    30
    / \   /   28  16 22  13
  /  19  17

数组: [62, 41, 30, 28, 16, 22, 13, 19, 17]
下标:   0   1   2   3   4   5   6   7   8

规律(下标 i):
- 左孩子: 2*i + 1
- 右孩子: 2*i + 2
- 父节点: (i-1) // 2
- 最后一个非叶节点: n//2 - 1

堆的实现骨架

class MaxHeap:
    def __init__(self):
        self.data = []          # 数组存储

    def __len__(self):
        return len(self.data)

    def peek(self):
        """看堆顶(最大值), O(1)"""
        return self.data[0] if self.data else None

    @staticmethod
    def _parent(i): return (i - 1) // 2
    @staticmethod
    def _left(i):   return 2 * i + 1
    @staticmethod
    def _right(i):  return 2 * i + 2

性质

  • 堆不是完全有序的树,只保证「父 ≥ 子」
  • 堆顶是最大/最小元素
  • 插入、删除堆顶都是 O(log n)(树高)
  • 建堆(heapify)O(n)

Python 内置堆

import heapq

# 小顶堆
heap = []
heapq.heappush(heap, 3)   # 插入 O(log n)
heapq.heappush(heap, 1)
heapq.heappush(heap, 2)
heapq.heappop(heap)       # 弹出最小 1

# 大顶堆: 存负数
heapq.heappush(heap, -x)  # 取时再取负
# heapq.nlargest(k, arr) / nsmallest(k, arr)
💡 应用
优先队列(任务调度)、堆排序、Top-K 问题、合并有序流——堆在「动态取最值」场景不可替代。

堆的 shift up

shift up(上浮):向堆中插入新元素时,把它放到末尾后不断与父节点比较,上浮到正确位置,保持堆序。

插入过程

大顶堆插入 50:
[62, 41, 30, 28, 16, 22, 13, 19, 17] + 50(末尾)
[62, 41, 30, 28, 16, 22, 13, 19, 17, 50]
                 ↑ 50 与父 16 比, 50>16 → 交换
[62, 41, 30, 28, 50, 22, 13, 19, 17, 16]
                 ↑ 50 与父 41 比, 50>41 → 交换
[62, 50, 30, 28, 41, 22, 13, 19, 17, 16]
                 ↑ 50 与父 62 比, 50<62 → 停止 ✅

实现

class MaxHeap:
    # ... 省略父/子下标与 data

    def insert(self, value):
        """插入元素并上浮, O(log n)"""
        self.data.append(value)
        self._shift_up(len(self.data) - 1)

    def _shift_up(self, i):
        """元素从位置 i 上浮到合适位置"""
        while i > 0:
            parent = (i - 1) // 2
            if self.data[i] <= self.data[parent]:
                break                    # 已满足堆序
            self.data[i], self.data[parent] =                 self.data[parent], self.data[i]
            i = parent                   # 继续向上

测试

h = MaxHeap()
for v in [62, 41, 30, 28, 16, 22, 13]:
    h.insert(v)
h.insert(50)                # 插入大值
print(h.peek())             # 62 (堆顶仍是最大)
print(h.data)               # [62, 50, 30, 28, 41, 22, 13, 16, 17]?

复杂度

  • 插入:O(log n)(树高 = log n 次比较交换)
  • 空间:O(1) 额外(原地在数组末尾操作)
✅ 理解
shift up 的循环不变量:「i 以上的部分」始终保持堆序。每轮把「新元素与父比较」,直到父更大或到顶。与 shift down 互为镜像。

堆的 shift down

shift down(下沉):从堆中删除堆顶时,用最后一个元素填补堆顶,然后不断下沉到正确位置。

删除堆顶过程

[62, 41, 30, 28, 16, 22, 13, 19, 17]
 ① 保存堆顶 62(返回)
 ② 最后一个元素 17 移到堆顶
 [17, 41, 30, 28, 16, 22, 13, 19]
 ③ 下沉: 17 与较大子 41 比, 41>17 → 交换
 [41, 17, 30, 28, 16, 22, 13, 19]
 ④ 17 与较大子 28 比, 28>17 → 交换
 [41, 28, 30, 17, 16, 22, 13, 19]
 ⑤ 17 与较大子 22 比, 22>17 → 交换
 [41, 28, 30, 22, 16, 17, 13, 19]
 ⑥ 17 与较大子 19 比, 19>17 → 交换
 [41, 28, 30, 22, 16, 19, 13, 17] ✅

实现

class MaxHeap:
    def pop(self):
        """弹出并返回堆顶(最大值), O(log n)"""
        if not self.data:
            return None
        top = self.data[0]
        last = self.data.pop()          # 取最后一个
        if self.data:
            self.data[0] = last         # 放到堆顶
            self._shift_down(0)         # 下沉
        return top

    def _shift_down(self, i):
        """元素从位置 i 下沉到合适位置"""
        n = len(self.data)
        while True:
            left = 2 * i + 1
            right = 2 * i + 2
            largest = i
            # 找三个节点中最大者
            if left < n and self.data[left] > self.data[largest]:
                largest = left
            if right < n and self.data[right] > self.data[largest]:
                largest = right
            if largest == i:
                break                   # 已就位
            self.data[i], self.data[largest] =                 self.data[largest], self.data[i]
            i = largest                 # 继续下沉

测试

h = MaxHeap()
for v in [62, 41, 30, 28, 16, 22, 13, 19, 17]:
    h.insert(v)
print(h.pop())   # 62
print(h.pop())   # 41
print(h.peek())  # 30

关键点

  • 下沉时与较大的孩子交换(大顶堆)
  • 边界:left < n 判断孩子是否存在
  • while True + break 模式清晰
✅ 对称理解
shift up:插入用(从下往上);shift down:删除堆顶用(从上往下)。两者都 O(log n),是堆的全部核心操作。

基础堆排序

堆排序(Heap Sort):利用堆「堆顶是最值」的性质——反复弹出堆顶,得到有序序列。**基础版**需要额外 O(n) 空间。

算法流程

① 把所有元素插入堆
② 依次弹出堆顶 → 得到升序(小顶堆)或降序(大顶堆)
例(小顶堆):
heap = 建堆([5,3,8,1])
pop → 1, pop → 3, pop → 5, pop → 8
结果: [1,3,5,8] ✅

基础实现(O(n) 空间)

import heapq

def heap_sort(arr):
    """基础版: 借助辅助堆"""
    heap = []
    for v in arr:
        heapq.heappush(heap, v)    # 建堆 O(n log n)
    return [heapq.heappop(heap) for _ in range(len(arr))]

print(heap_sort([5, 3, 8, 1, 9]))  # [1,3,5,8,9]

空间优化方向

问题: 用了额外数组存放结果 → O(n) 空间
下章: 原地堆排序 → O(1) 额外空间
步骤:
① 把原数组 heapify 成大顶堆(原地)
② 反复: 堆顶(最大)与末尾交换, 堆大小-1, 下沉
③ 结束: 数组升序 ✅

复杂度

指标值
时间O(n log n)(建堆 + n 次 O(log n) 弹出)
空间(基础版)O(n)
稳定❌ 不稳定

Python 一行版

import heapq
heapq.heapify(arr)      # 原地建堆(小顶)
sorted_arr = [heapq.heappop(arr) for _ in range(len(arr))]
✅ 意义
基础堆排序虽然多花空间,但思路直观。掌握「建堆 + 反复弹出」后,原地版只是把「弹出」改成「与末尾交换」。

优化堆排序

原地堆排序:把「弹出堆顶」改为「堆顶与末尾交换」,堆的大小减 1,仅 O(1) 额外空间。

核心思想

大顶堆 [62, 41, 30, 28, 16, 22, 13, 19, 17]
① 堆顶与末尾交换: [17, 41, 30, ..., 62]
   62 已就位(末尾=最大)
② 堆大小-1, 对前 8 个元素下沉
③ 重复 ① ② → 数组尾部逐渐累积最大元素 → 升序

每次交换+下沉 O(log n), 共 n 次 → O(n log n)

实现

def heapify(arr, n, i):
    """以 i 为根的下沉(使其成为合法子堆)"""
    largest = i
    left, right = 2*i+1, 2*i+2
    if left < n and arr[left] > arr[largest]:
        largest = left
    if right < n and arr[right] > arr[largest]:
        largest = right
    if largest != i:
        arr[i], arr[largest] = arr[largest], arr[i]
        heapify(arr, n, largest)   # 递归下沉

def heap_sort_inplace(arr):
    n = len(arr)
    # ① 建堆: 从最后一个非叶节点开始下沉
    for i in range(n // 2 - 1, -1, -1):
        heapify(arr, n, i)

    # ② 反复把堆顶与末尾交换
    for i in range(n - 1, 0, -1):
        arr[0], arr[i] = arr[i], arr[0]   # 最大放末尾
        heapify(arr, i, 0)                # 缩小堆重新下沉
    return arr

print(heap_sort_inplace([5,3,8,1,9,2]))  # [1,2,3,5,8,9]

建堆为什么是 O(n)

从最后一个非叶节点(n//2-1)倒着下沉:
- 底层节点多但下沉距离短
- 高层节点下沉距离长但数量少
总工作量 ≈ n (不是 n log n)

复杂度

指标值
建堆O(n)
排序n × O(log n) = O(n log n)
空间O(1)(原地,仅递归栈)
稳定❌
✅ 特点
堆排序「最坏也是 O(n log n)」——这点比快排(最坏 O(n²))稳,比归并省空间。代价是常数大、不稳定,工程上不如 Timsort。

索引堆及其优化

索引堆(Index Heap):堆元素不是数据本身,而是数据的下标。可任意修改、删除元素,且不移动数据本体——适合动态优先队列(如 Dijkstra 优化)。

为什么需要索引堆

普通堆的问题:
- 修改某个元素的值 → 需先找到它(可能要遍历 O(n))
- 元素移动频繁 → 数据本身被搬来搬去(大对象代价高)

索引堆: 堆数组存"下标", 数据本体不动
→ 想改 data[i] → 直接改, 再按索引堆的 index 调整

结构

data:  [62, 41, 30, 28, 16, 22, 13]
index: [0,  1,  2,  3,  4,  5,  6]   # 堆里存的是这些下标
# 堆序比较的是 data[index[i]]

实现(索引堆)

class IndexMaxHeap:
    def __init__(self, data):
        self.data = data            # 原始数据(下标访问)
        self.index = list(range(len(data)))  # 堆数组存下标
        # 建堆(按 data[index[i]] 比较)
        for i in range(len(self.index)//2 - 1, -1, -1):
            self._shift_down(i)

    def _shift_up(self, i):
        while i > 0:
            p = (i - 1) // 2
            if self.data[self.index[i]] <= self.data[self.index[p]]:
                break
            self.index[i], self.index[p] = self.index[p], self.index[i]
            i = p

    def _shift_down(self, i):
        n = len(self.index)
        while True:
            l, r, largest = 2*i+1, 2*i+2, i
            if l < n and self.data[self.index[l]] > self.data[self.index[largest]]:
                largest = l
            if r < n and self.data[self.index[r]] > self.data[self.index[largest]]:
                largest = r
            if largest == i: break
            self.index[i], self.index[largest] = self.index[largest], self.index[i]
            i = largest

    def pop_max(self):
        """弹出最大值对应的原始下标"""
        if not self.index: return None
        top = self.index[0]
        last = self.index.pop()
        if self.index:
            self.index[0] = last
            self._shift_down(0)
        return top

应用:Dijkstra 的堆优化

# 经典做法: heapq 存 (dist, node) 即可,
# 索引堆的价值在大数据/频繁改值的自定义场景。
# 概念理解为主: 「堆里存下标, 比较用数据值」
✅ 核心
索引堆 = 「堆结构 + 数据隔离」。修改 data[i] 后只需根据 i 在 index 中的位置做上浮/下沉(借助 index 的反向映射可 O(1) 定位)。

二分搜索树

二分搜索树(Binary Search Tree, BST):二叉树 + 有序性质——左子树所有值 < 根 < 右子树所有值。支持 O(log n) 的查找/插入/删除。

BST 性质

        41
       /       22    65
    /  \   /   15   28 53  70

规则: 每个节点, 左子树所有值 < 节点值 < 右子树所有值
中序遍历 → 有序序列: 15,22,28,41,53,65,70

节点与树

class Node:
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None

class BST:
    def __init__(self):
        self.root = None
        self.size = 0

查找

    def contains(self, val):
        """是否包含 val, O(log n)"""
        cur = self.root
        while cur:
            if val == cur.val: return True
            elif val < cur.val: cur = cur.left
            else: cur = cur.right
        return False

插入

    def insert(self, val):
        self.root = self._insert(self.root, val)
        self.size += 1

    def _insert(self, node, val):
        if node is None:
            return Node(val)
        if val < node.val:
            node.left = self._insert(node.left, val)
        elif val > node.val:
            node.right = self._insert(node.right, val)
        # 相等: 忽略(或计数)
        return node

最小/最大

    def minimum(self):
        """最左节点"""
        cur = self.root
        while cur and cur.left:
            cur = cur.left
        return cur.val if cur else None

    def maximum(self):
        cur = self.root
        while cur and cur.right:
            cur = cur.right
        return cur.val if cur else None

复杂度与退化

情况查找/插入
平衡 BSTO(log n)
退化为链表(有序插入)O(n) ❌
✅ 提示
BST 的中序遍历=有序是其最大优点;但有序输入会退化 → 工程上用 AVL/红黑树自动平衡(本教程后文深入遍历与删除)。

二分搜索树节点的插入

深入 BST 插入:递归与非递归实现,理解「返回值接收新子树」的递归技巧。

递归插入

class Node:
    def __init__(self, val):
        self.val = val
        self.left = self.right = None

def insert(node, val):
    """返回插入后的(子)树根。若 val 已存在则不插入。"""
    if node is None:
        return Node(val)
    if val < node.val:
        node.left = insert(node.left, val)
    elif val > node.val:
        node.right = insert(node.right, val)
    # val == node.val: 已存在, 不操作
    return node

递归中的关键点

insert(node.left, val) 的返回值要赋给 node.left
→ 因为新节点是在子树里创建的, 必须接回父节点
→ 「递归函数返回子树根」是树类递归的通用模式

非递归插入

def insert_iter(root, val):
    new = Node(val)
    if root is None:
        return new
    cur = root
    while True:
        if val < cur.val:
            if cur.left is None:
                cur.left = new
                break
            cur = cur.left
        elif val > cur.val:
            if cur.right is None:
                cur.right = new
                break
            cur = cur.right
        else:
            break          # 已存在
    return root

构建示例树

root = None
for v in [41, 22, 65, 15, 28, 53, 70]:
    root = insert(root, v)

# 结构:
#        41
#       /  #     22    65
#    / \   / #  15  28 53  70

测试

def inorder(node, res):
    if node:
        inorder(node.left, res)
        res.append(node.val)
        inorder(node.right, res)

res = []
inorder(root, res)
print(res)   # [15, 22, 28, 41, 53, 65, 70] 有序 ✅
✅ 建议
先写递归版(简洁、体现结构),理解后再写迭代版(面试常考)。记住:插入值相等时不重复插入,保证 BST 元素唯一。

二分搜索树节点的查找

BST 查找:按值比较,一路向下,每次排除一半——与二分查找异曲同工。

递归查找

def find(node, val):
    """返回包含 val 的节点, 没有返回 None"""
    if node is None or node.val == val:
        return node
    if val < node.val:
        return find(node.left, val)
    return find(node.right, val)

node = find(root, 28)
print(node.val if node else None)   # 28

非递归查找

def find_iter(node, val):
    while node and node.val != val:
        if val < node.val:
            node = node.left
        else:
            node = node.right
    return node

查找 floor / ceil

def floor(node, val):
    """不大于 val 的最大节点值"""
    if node is None: return None
    if node.val == val: return node.val
    if val < node.val:
        return floor(node.left, val)
    # val > node.val: 答案在右子树或就是 node
    res = floor(node.right, val)
    return res if res is not None else node.val

查找前驱 / 后继

# 前驱: 中序遍历中 val 的前一个
# 后继: 中序遍历中 val 的后一个
# 技巧: 中序遍历一遍, 找相邻元素

def predecessor(node, val):
    """小于 val 的最大值(简化: 基于 floor+严格小于)"""
    res = floor(node, val)
    return res if res != val else _floor_less(node, val)

# 完整实现通常配合删除/旋转使用, 理解概念为主

复杂度

树形态查找
平衡O(log n)
退化为链表O(n)
✅ 为什么快
每走一步,比较后淘汰一半子树,所以步数 = 树高。树高越小越接近 O(log n)——这正是平衡树的动机。

二分搜索树深度优先遍历

深度优先遍历(DFS):前序、中序、后序三种顺序,核心都是「递归访问根与子树」,区别在于根何时被访问。

三种顺序

        41
       /       22    65
    / \   /   15  28 53  70

前序(根左右):   41, 22, 15, 28, 65, 53, 70
中序(左根右):   15, 22, 28, 41, 53, 65, 70  ← 有序!
后序(左右根):   15, 28, 22, 53, 70, 65, 41

递归实现

def preorder(node):
    if node is None: return
    print(node.val, end=" ")     # 根
    preorder(node.left)          # 左
    preorder(node.right)         # 右

def inorder(node):
    if node is None: return
    inorder(node.left)           # 左
    print(node.val, end=" ")     # 根
    inorder(node.right)          # 右

def postorder(node):
    if node is None: return
    postorder(node.left)         # 左
    postorder(node.right)        # 右
    print(node.val, end=" ")     # 根

非递归前序(栈)

def preorder_iter(root):
    if root is None: return
    stack = [root]
    while stack:
        node = stack.pop()
        print(node.val, end=" ")
        # 先压右再压左 → 左先出
        if node.right: stack.append(node.right)
        if node.left:  stack.append(node.left)

非递归中序(栈)

def inorder_iter(root):
    stack, cur = [], root
    while stack or cur:
        while cur:                    # 一路向左
            stack.append(cur)
            cur = cur.left
        cur = stack.pop()             # 弹出(根)
        print(cur.val, end=" ")
        cur = cur.right               # 转向右子树

应用

顺序典型用途
前序复制树、序列化、打印结构
中序有序输出(BST)、BST 验证
后序释放树、计算子树大小(先子后父)
✅ 记忆
前中后指的都是根的位置:前=根先,中=根中间,后=根最后。左右子树顺序始终不变。

二分搜索树层序遍历

层序遍历(BFS,广度优先):按层从上到下、从左到右访问节点,用队列实现。

遍历顺序

        41
       /       22    65
    / \   /   15  28 53  70

层序: 41, 22, 65, 15, 28, 53, 70

队列实现

from collections import deque

def level_order(root):
    """层序遍历, 返回列表"""
    if root is None: return []
    res = []
    q = deque([root])
    while q:
        node = q.popleft()
        res.append(node.val)
        if node.left:  q.append(node.left)
        if node.right: q.append(node.right)
    return res

逐层输出(每层一行)

def level_order_layers(root):
    """按层分组输出: [[41], [22,65], [15,28,53,70]]"""
    if root is None: return []
    res, q = [], deque([root])
    while q:
        layer = []
        for _ in range(len(q)):     # 本层全部处理完
            node = q.popleft()
            layer.append(node.val)
            if node.left:  q.append(node.left)
            if node.right: q.append(node.right)
        res.append(layer)
    return res

应用:树的最大深度

def max_depth(root):
    """BFS 统计层数 = 最大深度"""
    if root is None: return 0
    depth, q = 0, deque([root])
    while q:
        depth += 1
        for _ in range(len(q)):
            node = q.popleft()
            if node.left:  q.append(node.left)
            if node.right: q.append(node.right)
    return depth

# 递归版: return 1 + max(max_depth(l), max_depth(r))

DFS vs BFS

维度DFS(栈/递归)BFS(队列)
路径纵深探索逐层扩散
空间O(树高)O(最大层宽)
适合路径问题、回溯最短路径、按层处理
✅ 记忆
BFS = 队列 + 逐层。每轮先记录「当前队列长度」,处理完这一层再进入下一层,即可精确分层。

二分搜索树节点删除

BST 节点删除是最复杂的操作,按「被删节点有几个孩子」分三种情况处理。

三种情况

① 无孩子(叶子): 直接删除, 父指针置空
② 一个孩子: 用孩子顶替
③ 两个孩子: 用「后继」(右子树最小) 或「前驱」(左子树最大) 顶替
   → 保持 BST 性质

删 28(叶子)     删 22(一个孩子15)    删 41(两个孩子, 用后继53)
  41                41                  53
 /  \              /  \                /  22   65           15   65             22   65
 \   / \              / \            / \   /  28 53 70           53  70          15 28 53?  70

实现

def remove(node, val):
    """从子树删除 val, 返回新子树根"""
    if node is None:
        return None
    if val < node.val:
        node.left = remove(node.left, val)
    elif val > node.val:
        node.right = remove(node.right, val)
    else:                          # 找到要删的节点
        # 情况①: 无左孩子(含叶子)
        if node.left is None:
            return node.right      # 右孩子(或None)顶替
        # 情况①': 无右孩子
        if node.right is None:
            return node.left
        # 情况③: 两个孩子 → 用右子树最小(后继)顶替
        successor = node.right
        while successor.left:      # 找右子树最小值
            successor = successor.left
        node.val = successor.val   # 值替换
        node.right = remove(node.right, successor.val)  # 删掉后继
    return node

删除最小值辅助函数

def remove_min(node):
    """删除子树最小节点, 返回新根"""
    if node.left is None:
        return node.right
    node.left = remove_min(node.left)
    return node

def min_node(node):
    while node.left:
        node = node.left
    return node

验证

root = remove(root, 41)
res = []
inorder(root, res)
print(res)   # [15, 22, 28, 53, 65, 70] 仍有序 ✅
⚠️ 易错
两个孩子时用后继(右子树最小)顶替,因为后继没有左孩子,删除简单;删除后必须递归修正右子树,避免悬空引用。

二分搜索树的特性

总结 BST 的核心特性与局限性,理解它与其他树结构的取舍。

BST 核心特性

特性说明
有序性左 < 根 < 右,中序遍历升序
高效查找平衡时 O(log n)
动态插入删除O(log n),无需移动大量元素
支持 floor/ceil/rank有序结构天然支持范围查询

验证 BST 合法性

def is_valid_bst(root, lo=float("-inf"), hi=float("inf")):
    """判断是否合法 BST"""
    if root is None: return True
    if not (lo < root.val < hi):
        return False
    return (is_valid_bst(root.left, lo, root.val)
            and is_valid_bst(root.right, root.val, hi))

# 中序法: 中序遍历应严格递增
def is_valid_bst_inorder(root):
    res = []
    inorder(root, res)
    return all(res[i] < res[i+1] for i in range(len(res)-1))

局限性:退化

有序插入: 1,2,3,4,5,6,7
→ 每个节点只有右孩子 → 退化成链表!
        1
                   2
                       3
             ...
查找 O(n) ❌

解决: 平衡树(AVL/红黑树)自动调整高度

BST 的衍生应用

# 1. 验证二叉搜索树
# 2. 第 k 小元素: 中序遍历第 k 个
def kth_smallest(root, k):
    res = []
    inorder(root, res)
    return res[k-1]

# 3. 转化为有序数组 / 有序数组构建平衡BST
def sorted_to_bst(arr):
    if not arr: return None
    mid = len(arr) // 2
    node = Node(arr[mid])
    node.left = sorted_to_bst(arr[:mid])
    node.right = sorted_to_bst(arr[mid+1:])
    return node

选择指南

需求结构
纯查找/插入哈希表(O(1))
有序遍历/范围查询平衡 BST(AVL/红黑)
取最值堆
磁盘存储B+树
✅ 总结
BST 的价值在「有序 + 动态」;哈希表无序、数组有序但不动态。理解 BST 是一切平衡树/索引结构的地基。

并查集基础

并查集(Union-Find / Disjoint Set):高效处理「连通性」问题——判断两个元素是否属于同一集合、合并两个集合。

核心操作

find(x):  找到 x 所在集合的代表(根)
union(x,y): 把 x、y 所在集合合并
is_connected(x,y): find(x) == find(y)

应用:
- 网络连通性
- 社交网络「是否好友圈」
- 最小生成树 Kruskal
- 迷宫生成

最简实现(数组)

class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))   # parent[i] = i 的父节点
        self.count = n                 # 集合数

    def find(self, x):
        """找根(代表元素)"""
        while self.parent[x] != x:
            x = self.parent[x]
        return x

    def union(self, x, y):
        """合并 x、y 所在集合"""
        rx, ry = self.find(x), self.find(y)
        if rx == ry: return
        self.parent[rx] = ry           # rx 的父设为 ry
        self.count -= 1

    def is_connected(self, x, y):
        return self.find(x) == self.find(y)

uf = UnionFind(5)              # 0~4 各自独立
uf.union(0, 1)
uf.union(2, 3)
print(uf.is_connected(0, 1))   # True
print(uf.is_connected(0, 2))   # False
print(uf.count)                # 3

为什么是树

并查集用「树」表示集合:
- 根 = 集合代表
- parent[i] 指向父节点, 根的父是自己
- find = 沿 parent 向上走到根

优化空间(后续章节):
① 快速查找 → 基于根合并避免退化
② 按 size/rank 合并 → 树矮
③ 路径压缩 → 几乎 O(1)
✅ 思维
并查集就两个函数:find(找代表)和 union(合并)。所有复杂场景都是这两个操作的组合。

并查集快速查找

Quick Find:让 find 达到 O(1)——用「数组直接存所属集合代表」的方式,但 union 变慢为 O(n)。

思路

quick find: id[i] 直接存 i 所属集合的"代表"
find(x) → 直接返回 id[x] (O(1))
union(x,y) → 把 y 集合的所有元素 id 改成 x 的代表 (O(n))

例:
初始 id = [0,1,2,3,4]
union(0,1): id = [0,0,2,3,4]   # 1 并入 0 的集合
union(1,2): id = [0,0,0,3,4]   # 2 并入 0, 同时改1(已同组)
union(3,4): id = [0,0,0,3,3]

实现

class QuickFind:
    def __init__(self, n):
        self.id = list(range(n))
        self.count = n

    def find(self, x):
        return self.id[x]          # O(1)

    def is_connected(self, x, y):
        return self.id[x] == self.id[y]

    def union(self, x, y):
        """把 y 所在集合并入 x 所在集合, O(n)"""
        rx, ry = self.id[x], self.id[y]
        if rx == ry: return
        for i in range(len(self.id)):
            if self.id[i] == ry:
                self.id[i] = rx
        self.count -= 1

复杂度

操作Quick FindQuick Union(下章)
findO(1) ✅O(树高)
unionO(n) ❌O(树高)
n 次操作O(n²)最坏 O(n²)

何时用 Quick Find

✅ 查找极频繁、合并极少
❌ 大量合并 → union 太慢(要扫全数组)
❌ 不适用于动态大规模数据
✅ 对比意义
Quick Find 用「空间换查找速度」;Quick Union 用「树结构换合并速度」。理解两者的权衡,才能理解并查集为什么最终走向「树 + 优化」。

并查集快速合并

Quick Union:用父指针树表示集合,union 只需改一个指针(O(树高)),find 沿树向上找根。

思路

parent[i]: i 的父节点, 根的父是自己
find(x): 沿 parent 向上走到根
union(x,y): 把 x 的根 指向 y 的根 (改一个指针)

初始: [0,1,2,3,4]
union(0,1): parent[0]=1 → [1,1,2,3,4]
union(2,3): parent[2]=3 → [1,1,3,3,4]
union(1,3): parent[1]=3(根) → [1,3,3,3,4]
    结构: 3 → 1 → 0  和  3 → 2

实现

class QuickUnion:
    def __init__(self, n):
        self.parent = list(range(n))
        self.count = n

    def find(self, x):
        """沿父链找到根, O(树高)"""
        while self.parent[x] != x:
            x = self.parent[x]
        return x

    def is_connected(self, x, y):
        return self.find(x) == self.find(y)

    def union(self, x, y):
        """x 的根指向 y 的根, O(树高)"""
        rx, ry = self.find(x), self.find(y)
        if rx == ry: return
        self.parent[rx] = ry
        self.count -= 1

最坏退化

连续 union(0,1), union(1,2), union(2,3):
parent[0]=1, parent[1]=2, parent[2]=3
→ 树退化成链: 3→2→1→0
find(0) 要走 O(n) ❌

解决: 按 size / rank 合并(下两章) → 树保持矮

复杂度对比

操作Quick FindQuick Union
findO(1)O(树高) ≤ O(n)
unionO(n)O(树高) ≤ O(n)
✅ 关键
Quick Union 的 union 已经是「改一个指针」,但树可能退化。下一步(size/rank 优化 + 路径压缩)就是让树永远矮,把整体压到近似 O(1)。

并查集 size 的优化

合并时把元素少的树并入元素多的树(按 size 合并),避免树退化,保持 O(log n) 树高。

问题

Quick Union 无脑把 rx 指向 ry:
union(0,1), union(0,2), union(0,3)...  → 链状树
为什么: 每次 x 的根被挂到 y 下面, 让大树的根变成小树的孩子

size 优化: 谁的元素多谁当根 → 树保持矮胖

实现

class UnionFindSize:
    def __init__(self, n):
        self.parent = list(range(n))
        self.size = [1] * n       # 以 i 为根的集合大小
        self.count = n

    def find(self, x):
        while self.parent[x] != x:
            x = self.parent[x]
        return x

    def union(self, x, y):
        rx, ry = self.find(x), self.find(y)
        if rx == ry: return
        # 小的并入大的
        if self.size[rx] < self.size[ry]:
            self.parent[rx] = ry
            self.size[ry] += self.size[rx]
        else:
            self.parent[ry] = rx
            self.size[rx] += self.size[ry]
        self.count -= 1

为什么有效

按 size 合并后:
- 节点所在树的高度 ≤ log2(集合大小)
- 因为: 每当某节点深度+1, 所在集合至少翻倍
  (它所在的小树被并入 ≥ 自身大小的树)
→ find / union: O(log n) ✅

验证

uf = UnionFindSize(8)
for i in range(1, 8):
    uf.union(0, i)      # 0 一直并入/被并入
# 树高很小(约 log n), find(7) 很快

演进路线

Quick Union        → 最坏 O(n)      ❌
+ size 合并        → O(log n)       ✅
+ 路径压缩(下章)   → 几乎 O(1)      🚀
✅ 注意
size 优化保证的是「树高 O(log n)」。它解决退化,但还没达到最优——加上路径压缩后,均摊复杂度接近常数。

并查集 rank 的优化

按 rank(树高)合并:把高度低的树并入高度高的树。比 size 更贴合「树高」这个优化目标。

rank vs size

size:  按节点数量合并(元素多的当根)
rank:  按树的高度合并(高的当根)

场景对比:
大树浅(元素多但矮) vs 小树深(元素少但高)
→ rank 合并会把矮的并入高的 → 总高度不增加
→ size 合并可能把高树挂到矮树下 → 高度增加

实现

class UnionFindRank:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n       # 树的高度(大致)
        self.count = n

    def find(self, x):
        while self.parent[x] != x:
            x = self.parent[x]
        return x

    def union(self, x, y):
        rx, ry = self.find(x), self.find(y)
        if rx == ry: return
        if self.rank[rx] < self.rank[ry]:
            self.parent[rx] = ry      # 矮的挂高的
        elif self.rank[rx] > self.rank[ry]:
            self.parent[ry] = rx
        else:
            self.parent[ry] = rx      # 相等: 随便挂
            self.rank[rx] += 1        # 高度+1
        self.count -= 1

高度变化规则

合并时:
- 高度不同: 矮树挂高树下, 总高不变
- 高度相同: 任选一个当根, 其高度 +1

例:
  树A(高2)  树B(高1)
  A ∪ B →  B 挂到 A 下 → 高度仍 2
  树A(高2)  树B(高2)
  A ∪ B →  A 当根, 高度变 3

复杂度

按 rank 合并: find/union O(log n)
+ 路径压缩(下章): 均摊 ≈ O(1) (反阿克曼函数 α(n))

size vs rank 对比

维度sizerank
合并依据节点数树高
最坏树高O(log n)O(log n)
实现略简单略复杂(注意高度相等+1)
✅ 建议
面试/工程中 rank 与 size 任选其一即可达到 O(log n)。rank 更精准贴合高度目标,size 实现更直观。两者都可再叠加路径压缩。

并查集路径压缩

路径压缩(Path Compression):find 过程中把沿途节点直接挂到根下,让树变得更矮,均摊复杂度接近 O(1)。

思想

find(4) 沿 4→3→2→1→0 找到根 0:
路径压缩后: 4,3,2,1 全部直接指向 0

     0              0
    /|               /|   1 5       →      1 2 3 4 ...
  /  |              5 ...
 2   6
 /
3
/
4

下次 find(4): 一步直达 ✅

递归路径压缩(最简单)

class UnionFindPC:
    def __init__(self, n):
        self.parent = list(range(n))

    def find(self, x):
        """递归找根, 同时把所有祖先挂到根下"""
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        rx, ry = self.find(x), self.find(y)
        if rx != ry:
            self.parent[rx] = ry

迭代路径压缩(半压缩)

    def find_iter(self, x):
        """两遍法: 先找根, 再回程压缩"""
        root = x
        while self.parent[root] != root:
            root = self.parent[root]
        # 第二遍: 沿途全部指向根
        while self.parent[x] != x:
            nxt = self.parent[x]
            self.parent[x] = root
            x = nxt
        return root

完整优化版(rank + 路径压缩)

class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n
        self.count = n

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        rx, ry = self.find(x), self.find(y)
        if rx == ry: return
        if self.rank[rx] < self.rank[ry]:
            self.parent[rx] = ry
        elif self.rank[rx] > self.rank[ry]:
            self.parent[ry] = rx
        else:
            self.parent[ry] = rx
            self.rank[rx] += 1
        self.count -= 1

    def is_connected(self, x, y):
        return self.find(x) == self.find(y)

复杂度

n 次操作均摊: O(n · α(n))
α = 反阿克曼函数, 对任何实际规模都 ≤ 4
→ 可以认为「常数时间」
✅ 记忆
路径压缩 = find 时顺手把路压平。与 rank 合并配合是并查集的最终形态,也是面试的完整答案。

图论基础和表示

系统学习图的表示:邻接矩阵与邻接表,理解各自适用场景,为图算法打基础。

图的定义

图 G = (V, E)
V: 节点集合, E: 边集合
边可以带方向(有向图)、带权重(带权图)

    0 --- 1
    | \   |
    |  \  |
    3 --- 2
    无向图 4 节点 5 条边

邻接矩阵

# n×n 矩阵, matrix[i][j] = 1 表示 i→j 有边
matrix = [
    [0, 1, 1, 1],   # 0 连 1,2,3
    [1, 0, 1, 0],   # 1 连 0,2
    [1, 1, 0, 1],   # 2 连 0,1,3
    [1, 0, 1, 0],   # 3 连 0,2
]

# 判断 i,j 是否相连: O(1)
def is_adjacent(matrix, i, j):
    return matrix[i][j] != 0

# 获取所有邻居: O(n)(要扫一行)

邻接表

# 每个节点存一个邻居列表
adj = [
    [1, 2, 3],   # 0 的邻居
    [0, 2],      # 1 的邻居
    [0, 1, 3],   # 2 的邻居
    [0, 2],      # 3 的邻居
]

# 获取邻居: O(度数)
def neighbors(adj, i):
    return adj[i]

# 带权图: 存 (邻居, 权重)
weighted = [
    [(1, 5), (2, 3)],
    [(0, 5), (2, 2)],
    [(0, 3), (1, 2)],
]

对比

维度邻接矩阵邻接表
空间O(n²)O(n + e)
判断相连O(1) ✅O(度数)
遍历邻居O(n)O(度数) ✅
适合稠密图稀疏图(常见) ✅

图类的封装

class Graph:
    def __init__(self, n, directed=False):
        self.n = n
        self.directed = directed
        self.adj = [[] for _ in range(n)]

    def add_edge(self, u, v):
        self.adj[u].append(v)
        if not self.directed:
            self.adj[v].append(u)

    def degree(self, u):
        return len(self.adj[u])

g = Graph(4)
g.add_edge(0, 1); g.add_edge(0, 2); g.add_edge(1, 2)
g.add_edge(0, 3); g.add_edge(2, 3)
✅ 工程惯例
绝大多数场景用邻接表(边少省空间、遍历快);只有「频繁判断两点是否相连」且图稠密时才用邻接矩阵。

相邻节点迭代器

相邻节点迭代器(Adjacency Iterator):封装「遍历某节点的所有邻居」的逻辑,让图算法代码更清晰统一。

为什么需要迭代器

图算法(DFS/BFS/Dijkstra)都要"遍历邻居":
- 邻接表: 直接 for v in adj[u]
- 邻接矩阵: for v in range(n): if matrix[u][v]
→ 把"遍历邻居"封装成接口, 图结构变了算法不用改

Python 实现(生成器)

class Graph:
    def __init__(self, n, directed=False, use_matrix=False):
        self.n = n
        self.directed = directed
        self.use_matrix = use_matrix
        if use_matrix:
            self.matrix = [[0]*n for _ in range(n)]
        else:
            self.adj = [[] for _ in range(n)]

    def add_edge(self, u, v):
        if self.use_matrix:
            self.matrix[u][v] = 1
            if not self.directed:
                self.matrix[v][u] = 1
        else:
            self.adj[u].append(v)
            if not self.directed:
                self.adj[v].append(u)

    def neighbors(self, u):
        """相邻节点迭代器: 统一接口"""
        if self.use_matrix:
            for v in range(self.n):
                if self.matrix[u][v]:
                    yield v
        else:
            for v in self.adj[u]:
                yield v

使用迭代器写通用算法

def dfs(g, start, visited):
    """不关心图内部结构, 只用 neighbors()"""
    visited.add(start)
    print(start, end=" ")
    for v in g.neighbors(start):
        if v not in visited:
            dfs(g, v, visited)

# 邻接表版与邻接矩阵版都能跑
g1 = Graph(4, use_matrix=False)
g2 = Graph(4, use_matrix=True)
for e in [(0,1),(0,2),(1,2),(0,3),(2,3)]:
    g1.add_edge(*e); g2.add_edge(*e)

dfs(g1, 0, set())   # 0 1 2 3
print()
dfs(g2, 0, set())   # 0 1 2 3 (同样结果!)

带权图的邻居迭代器

    def weighted_neighbors(self, u):
        """返回 (邻居, 权重)"""
        for v, w in self.adj[u]:
            yield v, w
✅ 设计价值
「迭代器」把图结构与图算法解耦:DFS、BFS、Dijkstra 只依赖 neighbors(),换存储方式零改动。面试中主动用迭代器封装是加分项。

深度优先遍历与连通分量

DFS(深度优先遍历)在图上的应用:访问所有可达节点,并顺手求出连通分量。

DFS 遍历图

def dfs(g, start, visited):
    visited[start] = True
    print(start, end=" ")
    for v in g.neighbors(start):
        if not visited[v]:
            dfs(g, v, visited)

n = 4
g = Graph(n)
for e in [(0,1),(0,2),(1,2),(0,3),(2,3)]:
    g.add_edge(*e)

visited = [False] * n
dfs(g, 0, visited)   # 0 1 2 3

连通分量

连通分量 = 互相可达的最大节点集合
例: 图有两个不连通的子图:
  子图A: {0,1,2}, 子图B: {3,4}
  → 连通分量: [{0,1,2}, {3,4}], 共 2 个

应用: 判断网络是否连通、找孤立点、朋友圈划分

求连通分量

def connected_components(g):
    n = g.n
    visited = [False] * n
    components = []
    for i in range(n):               # 遍历所有节点
        if not visited[i]:
            comp = []
            stack = [i]
            visited[i] = True
            while stack:             # 迭代 DFS
                u = stack.pop()
                comp.append(u)
                for v in g.neighbors(u):
                    if not visited[v]:
                        visited[v] = True
                        stack.append(v)
            components.append(comp)
    return components

# 构造两个不连通部分
g2 = Graph(5)
for e in [(0,1),(1,2),(3,4)]:
    g2.add_edge(*e)
print(connected_components(g2))
# [[0, 2, 1], [4, 3]]  → 2 个连通分量

DFS 变体

用途做法
可达性判断从 start DFS,看 target 是否 visited
环检测DFS 时遇到「已在栈中」的边
拓扑排序后序 DFS 的反转
二分图判定DFS 染色,相邻不同色
✅ 记忆
DFS 找连通分量 = 对每个未访问节点发起 DFS,每次 DFS 恰好覆盖一个连通分量。复杂度 O(V + E)。

寻路算法

寻路(Path Finding):在图中找到从起点到终点的路径。DFS 可寻路但不保证最短,BFS 在无权图中保证最短路径。

DFS 寻路

def dfs_find_path(g, start, target):
    """用 DFS 找一条路径(不保证最短)"""
    n = g.n
    visited = [False] * n
    path = []

    def dfs(u):
        visited[u] = True
        path.append(u)
        if u == target:
            return True
        for v in g.neighbors(u):
            if not visited[v]:
                if dfs(v):
                    return True
        path.pop()           # 回溯
        return False

    dfs(start)
    return path if path and path[-1] == target else []

# 递归回溯是「找路径」的模板

记录父节点寻路(更通用)

def find_path(g, start, target):
    """DFS/BFS 记录 parent, 从 target 回溯路径"""
    n = g.n
    visited = [False] * n
    parent = [-1] * n
    stack = [start]
    visited[start] = True
    while stack and not visited[target]:
        u = stack.pop()
        for v in g.neighbors(u):
            if not visited[v]:
                visited[v] = True
                parent[v] = u
                stack.append(v)
    # 回溯构建路径
    path = []
    if visited[target]:
        cur = target
        while cur != -1:
            path.append(cur)
            cur = parent[cur]
        path.reverse()
    return path

无权图最短路径(BFS)

def bfs_shortest(g, start, target):
    """BFS 首次到达 = 最短路径(无权图)"""
    from collections import deque
    n = g.n
    visited = [False] * n
    parent = [-1] * n
    q = deque([start])
    visited[start] = True
    while q:
        u = q.popleft()
        if u == target:
            break
        for v in g.neighbors(u):
            if not visited[v]:
                visited[v] = True
                parent[v] = u
                q.append(v)
    # 回溯
    path, cur = [], target
    if visited[target]:
        while cur != -1:
            path.append(cur)
            cur = parent[cur]
        path.reverse()
    return path

# 为什么 BFS 最短: 逐层扩散, 首次到达即最浅层
✅ 区别
DFS 快但路径未必最优;无权图最短路径用 BFS;带权正边用 Dijkstra。寻路的通用框架是「visited + parent + 回溯」。

广度优先遍历与最短路径

BFS(广度优先遍历)的完整应用:层级遍历、无权图最短路径、多源 BFS。

BFS 遍历

from collections import deque

def bfs(g, start):
    """BFS 遍历整个连通部分"""
    n = g.n
    visited = [False] * n
    visited[start] = True
    q = deque([start])
    while q:
        u = q.popleft()
        print(u, end=" ")
        for v in g.neighbors(u):
            if not visited[v]:
                visited[v] = True
                q.append(v)

BFS 求最短路径(含距离)

def bfs_shortest_dist(g, start):
    """返回每个节点到 start 的最短距离(无权图)"""
    n = g.n
    dist = [-1] * n          # -1 = 不可达
    dist[start] = 0
    q = deque([start])
    while q:
        u = q.popleft()
        for v in g.neighbors(u):
            if dist[v] == -1:       # 未访问
                dist[v] = dist[u] + 1
                q.append(v)
    return dist

# 例: 0--1--2--3
#     dist = [0, 1, 2, 3]

网格地图 BFS(迷宫最短步数)

def maze_shortest(maze, start, end):
    """网格迷宫最短路径(0可走1是墙)"""
    rows, cols = len(maze), len(maze[0])
    dirs = [(0,1),(1,0),(0,-1),(-1,0)]
    dist = [[-1]*cols for _ in range(rows)]
    sr, sc = start
    dist[sr][sc] = 0
    q = deque([(sr, sc)])
    while q:
        r, c = q.popleft()
        if (r, c) == end:
            return dist[r][c]
        for dr, dc in dirs:
            nr, nc = r+dr, c+dc
            if (0 <= nr < rows and 0 <= nc < cols
                    and maze[nr][nc] == 0 and dist[nr][nc] == -1):
                dist[nr][nc] = dist[r][c] + 1
                q.append((nr, nc))
    return -1   # 不可达

多源 BFS(多个起点同时扩散)

def multi_source_bfs(g, sources):
    """从多个源同时 BFS(如多个城市同时扩张)"""
    n = g.n
    dist = [-1] * n
    q = deque()
    for s in sources:
        dist[s] = 0
        q.append(s)
    while q:
        u = q.popleft()
        for v in g.neighbors(u):
            if dist[v] == -1:
                dist[v] = dist[u] + 1
                q.append(v)
    return dist
✅ 总结
BFS 三板斧:队列 + visited/dist + 逐层。无权图最短路径、网格迷宫、多源扩散都用它。图算法入门至此完成,接下来可攻 Dijkstra 等带权算法。
第 1 / 44 节