CS 教程 · 第 09 章

数据结构与算法

数组 / 链表 / 栈 / 队列 / 排序

程序 = 数据结构 + 算法。用合适的结构组织数据,用高效的算法处理问题。

数据结构和算法是程序员的"内功心法"。面试必考、工作常用,更是写出高性能代码的基础。本章从最基础的数组讲到动态规划,让你对算法世界建立系统认知。


9.1 时间复杂度:Big-O 表示法

在讨论算法之前,我们必须先学会评判算法的"好坏"。时间复杂度描述算法执行时间随输入规模增长的变化趋势。

常见复杂度排序

O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ) < O(n!)
复杂度 名称 典型算法 n=100 时
O(1) 常数 数组索引 `arr[5]` 1
O(log n) 对数 二分查找 ~7
O(n) 线性 遍历数组 100
O(n log n) 线性对数 快速排序 ~664
O(n²) 平方 冒泡排序 10000
O(2ⁿ) 指数 暴力穷举子集 天文数字

如何分析复杂度

def find_max(arr):
    max_val = arr[0]           # O(1)
    for num in arr:            # 循环 n 次
        if num > max_val:      # O(1)
            max_val = num      # O(1)
    return max_val
# 总复杂度:O(n)

def has_duplicate(arr):
    for i in range(len(arr)):          # O(n)
        for j in range(i + 1, len(arr)): # O(n)
            if arr[i] == arr[j]:
                return True
    return False
# 总复杂度:O(n²)

def binary_search(arr, target):
    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
# 总复杂度:O(log n)

分析原则:


9.2 数组与链表

数组(Array)

内存中连续的存储空间,通过索引 O(1) 时间访问任意元素。

内存地址: 1000  1004  1008  1012  1016
         ┌────┬────┬────┬────┬────┐
数组元素:│ 10 │ 20 │ 30 │ 40 │ 50 │
         └────┴────┴────┴────┴────┘
索引:      0    1    2    3    4
# Python 的 list 是动态数组
arr = [10, 20, 30, 40, 50]

arr[2]        # O(1)  — 随机访问
arr.append(60) # O(1)* — 末尾追加(均摊)
arr.insert(0, 5)  # O(n) — 插入到开头,所有元素后移
arr.pop(0)        # O(n) — 删除开头元素
*Python 的 list 是动态数组:当容量不足时自动扩容(通常是 1.125~2 倍),append 的均摊时间复杂度为 O(1)。

链表(Linked List)

通过指针连接的节点序列,内存不一定连续。

         ┌──────────┐    ┌──────────┐    ┌──────────┐
头部 ──> │ data: 10 │──> │ data: 20 │──> │ data: 30 │──> None
         │ next ──────  │ next ──────  │ next      │
         └──────────┘    └──────────┘    └──────────┘
# 链表节点定义
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

# 创建链表:10 -> 20 -> 30
head = ListNode(10)
head.next = ListNode(20)
head.next.next = ListNode(30)

# 遍历链表
def print_list(head):
    current = head
    while current:
        print(current.val, end=" -> ")
        current = current.next
    print("None")

print_list(head)  # 10 -> 20 -> 30 -> None

# 在头部插入(O(1))
new_head = ListNode(5)
new_head.next = head
head = new_head

# 在尾部插入(O(n),因为需要遍历到末尾)
def append(head, val):
    if not head:
        return ListNode(val)
    current = head
    while current.next:
        current = current.next
    current.next = ListNode(val)
    return head

# 反转链表(经典面试题!)
def reverse_list(head):
    prev = None
    current = head
    while current:
        next_temp = current.next  # 暂存下一个
        current.next = prev       # 反转指针
        prev = current            # 前移 prev
        current = next_temp       # 前移 current
    return prev

数组 vs 链表

操作 数组 链表
随机访问 O(1) ✅ O(n) ❌
头部插入/删除 O(n) O(1) ✅
尾部插入/删除 O(1) O(1)(有尾指针)/ O(n)
内存使用 连续(可能浪费) 不连续(指针开销)
缓存友好 是 ✅ 否 ❌
💡 实践建议:绝大多数场景用数组(Python list)即可。链表主要用于实现更高级的数据结构(如 LRU 缓存、图的邻接表)。

9.3 栈与队列

栈(Stack)

后进先出(LIFO)——像一叠盘子,最后放的先取。

入栈 push(3):    出栈 pop():
┌───┐           ┌───┐
│ 3 │ <─ 栈顶   │   │
├───┤           ├───┤
│ 2 │           │ 2 │ <─ 栈顶
├───┤           ├───┤
│ 1 │           │ 1 │
└───┘           └───┘
# Python 用 list 模拟栈
stack = []

stack.append("a")    # push
stack.append("b")
stack.append("c")

print(stack.pop())   # "c"(后进先出)
print(stack.pop())   # "b"
print(stack[-1])     # "a"(peek:查看栈顶但不弹出)
print(len(stack))    # 1

# 栈的经典应用:括号匹配
def is_valid_parentheses(s):
    """检查括号是否匹配"""
    stack = []
    pairs = {")": "(", "]": "[", "}": "{"}

    for char in s:
        if char in "([{":
            stack.append(char)
        elif char in ")]}":
            if not stack or stack.pop() != pairs[char]:
                return False

    return len(stack) == 0  # 栈空了才说明全部匹配

print(is_valid_parentheses("()[]{}"))  # True
print(is_valid_parentheses("([)]"))    # False

队列(Queue)

先进先出(FIFO)——像排队买票,先来的先服务。

入队 enqueue(1):        出队 dequeue():
队尾                 队首    队尾           队首
 ↓                    ↓      ↓              ↓
[3, 2, 1]     →     [3, 2]   →     [3]
from collections import deque

# deque(双端队列)——两端操作都是 O(1)
queue = deque()

queue.append("a")      # 入队(右侧)
queue.append("b")
queue.append("c")

print(queue.popleft()) # "a"(左侧出队,FIFO)
print(queue.popleft()) # "b"

queue.appendleft("x")  # 左侧入队
print(queue)           # deque(['x', 'c'])

# 队列的经典应用:BFS(广度优先搜索,见 9.7 节)

9.4 哈希表(Hash Table)

通过哈希函数将键映射到存储位置,实现接近 O(1) 的查找。

# Python 的 dict 和 set 都是基于哈希表
phone_book = {
    "张三": "138-0000-0001",
    "李四": "138-0000-0002",
}
print(phone_book["张三"])  # O(1)

# set:元素不重复的集合
visited = set()
visited.add("A")
visited.add("B")
visited.add("A")         # 重复,被忽略
print(visited)           # {'A', 'B'}
print("A" in visited)    # True,O(1) 查询

哈希冲突与解决

哈希函数:hash(key) % 10

键 "abc" → hash("abc") % 10 = 5 → 存入位置 5
键 "xyz" → hash("xyz") % 10 = 5 → 冲突!

解决方法:
1. 拉链法:位置 5 存一个链表 [("abc", val1), ("xyz", val2)]
2. 开放寻址法:位置 5 被占,尝试 6、7、8...

Python 的 dict 使用开放寻址法,通过巧妙的探测序列实现高性能。

实际应用

# 1. 两数之和(LeetCode 经典题)
def two_sum(nums, target):
    """找到两个和为 target 的数的索引"""
    seen = {}  # {值: 索引}
    for i, num in enumerate(nums):
        complement = target - num
        if complement in seen:
            return [seen[complement], i]
        seen[num] = i
    return []

print(two_sum([2, 7, 11, 15], 9))  # [0, 1]

# 2. 字符频率统计
from collections import Counter
text = "abracadabra"
freq = Counter(text)
print(freq)  # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
print(freq.most_common(2))  # [('a', 5), ('b', 2)]

9.5 树与二叉树

基本概念

        1          ← 根节点(root)
       / \
      2   3        ← 中间节点
     / \   \
    4   5   6      ← 叶子节点

节点 2 是节点 4 和 5 的父节点
节点 4 和 5 是兄弟节点
树的高度(深度):3

二叉树的遍历

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

# 创建树:
#     1
#    / \
#   2   3
#  / \
# 4   5
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)

# 前序遍历:根 → 左 → 右
def preorder(node):
    if not node:
        return
    print(node.val, end=" ")  # 先访问根
    preorder(node.left)
    preorder(node.right)

# 中序遍历:左 → 根 → 右  (BST 排序输出)
def inorder(node):
    if not node:
        return
    inorder(node.left)
    print(node.val, end=" ")  # 中间访问根
    inorder(node.right)

# 后序遍历:左 → 右 → 根  (先处理子树)
def postorder(node):
    if not node:
        return
    postorder(node.left)
    postorder(node.right)
    print(node.val, end=" ")  # 最后访问根

# 层序遍历(BFS)
from collections import deque

def level_order(root):
    if not root:
        return
    queue = deque([root])
    while queue:
        node = queue.popleft()
        print(node.val, end=" ")
        if node.left:
            queue.append(node.left)
        if node.right:
            queue.append(node.right)

preorder(root)     # 1 2 4 5 3
inorder(root)      # 4 2 5 1 3
postorder(root)    # 4 5 2 3 1
level_order(root)  # 1 2 3 4 5

二叉搜索树(BST)

左子树所有节点 < 根节点 < 右子树所有节点,查找/插入/删除平均 O(log n)。

        8
       / \
      3   10
     / \    \
    1   6    14
       / \
      4   7
def search_bst(root, target):
    """在 BST 中查找目标值"""
    if not root or root.val == target:
        return root
    if target < root.val:
        return search_bst(root.left, target)
    return search_bst(root.right, target)

9.6 排序算法

冒泡排序 — O(n²)

最简单的排序,像气泡一样将最大值"浮"到末尾。

def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        swapped = False
        # 每一轮将最大的元素"冒泡"到最后
        for j in range(0, n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                swapped = True
        if not swapped:  # 优化:本轮无交换,说明已有序
            break
    return arr

print(bubble_sort([64, 34, 25, 12, 22, 11, 90]))
# [11, 12, 22, 25, 34, 64, 90]

快速排序 — O(n log n) 平均

分治法的核心:选一个基准(pivot),小的放左边,大的放右边,递归处理。

def quick_sort(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(left) + middle + quick_sort(right)

print(quick_sort([3, 6, 8, 10, 1, 2, 1]))
# [1, 1, 2, 3, 6, 8, 10]

# 原地快排版本(节省内存,面试常考!)
def partition(arr, low, high):
    pivot = arr[high]
    i = low - 1
    for j in range(low, high):
        if arr[j] <= pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]
    arr[i + 1], arr[high] = arr[high], arr[i + 1]
    return i + 1

def quick_sort_inplace(arr, low=0, high=None):
    if high is None:
        high = len(arr) - 1
    if low < high:
        pi = partition(arr, low, high)
        quick_sort_inplace(arr, low, pi - 1)
        quick_sort_inplace(arr, pi + 1, high)
    return arr

归并排序 — O(n log n) 稳定

分治 + 合并:先递归拆分,再两两合并有序子数组。

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(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

排序算法对比

算法 最好 平均 最坏 空间 稳定
冒泡排序 O(n) O(n²) O(n²) O(1)
快速排序 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(n)
Python sort O(n log n) O(n log n) O(n log n) O(n)
💡 Python 的内置 `sorted()` 和 `list.sort()` 使用 Timsort(归并+插入排序的混合算法),在绝大多数场景下是最优选择。

9.7 搜索算法

二分查找 — O(log n)

前提:数组必须有序。

def binary_search(arr, target):
    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

arr = [1, 3, 5, 7, 9, 11, 13]
print(binary_search(arr, 7))   # 3
print(binary_search(arr, 6))   # -1(未找到)

深度优先搜索 DFS

像走迷宫:沿一条路走到底,无路可走再回头。

# 图的邻接表表示
graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F'],
    'D': ['B'],
    'E': ['B', 'F'],
    'F': ['C', 'E']
}

def dfs_iterative(graph, start):
    """DFS 迭代版本(使用栈)"""
    visited = set()
    stack = [start]

    while stack:
        node = stack.pop()
        if node not in visited:
            print(node, end=" ")
            visited.add(node)
            # 将邻居加入栈(可以反转以控制顺序)
            for neighbor in reversed(graph[node]):
                if neighbor not in visited:
                    stack.append(neighbor)

def dfs_recursive(graph, node, visited=None):
    """DFS 递归版本(更简洁)"""
    if visited is None:
        visited = set()
    visited.add(node)
    print(node, end=" ")
    for neighbor in graph[node]:
        if neighbor not in visited:
            dfs_recursive(graph, neighbor, visited)

print("迭代 DFS:", end="")
dfs_iterative(graph, 'A')   # A C F E B D
print("\n递归 DFS:", end="")
dfs_recursive(graph, 'A')   # A B D E F C

广度优先搜索 BFS

像水波扩散:一层一层往外探索。天然适合找最短路径

from collections import deque

def bfs(graph, start):
    visited = set()
    queue = deque([start])
    visited.add(start)

    while queue:
        node = queue.popleft()
        print(node, end=" ")

        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)

print("BFS:", end="")
bfs(graph, 'A')  # A B C D E F

DFS vs BFS

特性 DFS BFS
数据结构 栈(递归/显式栈) 队列
空间复杂度 O(h) h=深度 O(w) w=宽度
最短路径 不一定 保证 ✅
适用场景 迷宫、回溯、拓扑排序 最短路径、层序遍历

9.8 动态规划入门

动态规划(DP)将复杂问题分解为子问题,记忆已解决的结果避免重复计算。

斐波那契数列:从递归到 DP

# 版本1:朴素递归 — O(2ⁿ) 指数级,大量重复计算
def fib_naive(n):
    if n <= 1:
        return n
    return fib_naive(n - 1) + fib_naive(n - 2)
# fib_naive(50) → 等到天荒地老...

# 版本2:记忆化递归 — O(n),自上而下
def fib_memo(n, memo={}):
    if n <= 1:
        return n
    if n not in memo:
        memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
    return memo[n]
# fib_memo(50) → 瞬间完成

# 版本3:动态规划 — 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]

# 版本4:空间优化 DP — O(n) 时间,O(1) 空间
def fib_optimized(n):
    if n <= 1:
        return n
    prev2, prev1 = 0, 1
    for _ in range(2, n + 1):
        prev2, prev1 = prev1, prev2 + prev1
    return prev1

print(fib_optimized(50))  # 12586269025

经典问题:0/1 背包

def knapsack(weights, values, capacity):
    """
    0/1 背包问题
    weights: 物品重量列表
    values:  物品价值列表
    capacity: 背包容量
    返回:最大价值
    """
    n = len(weights)
    # dp[i][w]:考虑前 i 个物品,容量 w 时的最大价值
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        for w in range(capacity + 1):
            if weights[i - 1] <= w:
                # 选或不选当前物品,取最大值
                dp[i][w] = max(
                    dp[i - 1][w],                          # 不选
                    dp[i - 1][w - weights[i - 1]] + values[i - 1]  # 选
                )
            else:
                dp[i][w] = dp[i - 1][w]  # 装不下,只能不选

    return dp[n][capacity]

weights = [2, 3, 4, 5]
values  = [3, 4, 5, 6]
capacity = 8
print(f"最大价值: {knapsack(weights, values, capacity)}")  # 10
# 选择物品1(2,3) + 物品4(5,6) = 重量7, 价值9
# 或 物品2(3,4) + 物品3(4,5) = 重量7, 价值9
# 或 物品2(3,4) + 物品4(5,6) = 重量8, 价值10 ← 最优

9.9 图的基本概念

     A ──── B
     │      │ \
     │      │  E
     │      │ /
     C ──── D

顶点(Vertex):A, B, C, D, E
边(Edge):A-B, A-C, B-D, B-E, C-D, D-E
度(Degree):A 的度为 2(连接 B 和 C)
路径:A → C → D → E(长度为 3,经过 4 个顶点)

图的分类:

# 图的三种常用表示法
V = ['A', 'B', 'C', 'D', 'E']

# 1. 邻接矩阵(适合稠密图)
matrix = [
    [0, 1, 1, 0, 0],
    [1, 0, 0, 1, 1],
    [1, 0, 0, 1, 0],
    [0, 1, 1, 0, 1],
    [0, 1, 0, 1, 0],
]

# 2. 邻接表(适合稀疏图,最常用)
adj_list = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'D'],
    'D': ['B', 'C', 'E'],
    'E': ['B', 'D'],
}

# 3. 边列表(适合只需要遍历所有边的场景)
edges = [
    ('A', 'B'), ('A', 'C'),
    ('B', 'D'), ('B', 'E'),
    ('C', 'D'), ('D', 'E'),
]

9.10 算法学习路线建议

入门      →   基础      →    进阶       →    高级
排序       栈/队列     贪心算法        高级图算法
二分查找   哈希表      分治法          线段树/树状数组
递归       BFS/DFS    动态规划         并查集
           二叉树      回溯法          KMP/字符串算法

刷题平台推荐: LeetCode、牛客网、Codeforces

刷题策略:


本章小结


[⬅️ 上一章:08-编程入门-Python](./08-编程入门-Python.html) · [🏠 目录](./README.html) · [➡️ 下一章:10-数据库基础](./10-数据库基础.html)

Collaplex · 克拉普莱克斯 collaplex.me · 2026 浙ICP备2026080865号