2The Art of Computer Programming, Volume 1

信息结构

数据结构

线性列表栈队列树链表

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

第2章:信息结构(Information Structures)

一、导读

1.1 本章学习目标

第2章是 TAOCP 第一卷中最长也最核心的一章,Knuth 在此系统地讲解了基本数据结构:线性列表、栈、队列、树和链表。不同于现代教材直接给出高级语言实现,Knuth 从 MIX 汇编层面展示这些数据结构的内存布局和操作方法。

本章核心学习目标:

  • 掌握线性列表的多种表示方法:连续存储(数组)和链接存储(链表)
  • 理解栈和队列的抽象特性:LIFO 和 FIFO 的数学性质
  • 掌握树的递归定义和遍历方法:前序、中序、后序遍历
  • 理解链表操作的底层实现:插入、删除、合并的 MIX 汇编代码
  • 学会分析数据结构操作的时间复杂度

1.2 为什么本章如此重要

数据结构是算法的基础。Knuth 在本章中不仅介绍了数据结构"是什么",更深入分析了"为什么"——为什么选择链表而非数组?为什么树的遍历有这三种顺序?每种选择的代价和收益是什么?

1.3 核心问题引导

数组和链表在插入/删除/访问操作上的时间复杂度差异的根本原因是什么?

如何用数组模拟链表?这种表示方法有什么优势?

树的三种遍历顺序分别适合什么应用场景?

双向链表相比单向链表有什么优势和代价?


二、核心概念详解

2.1 线性列表

2.1.1 连续存储(数组)

数组的内存布局:
地址:  1000  1001  1002  1003  1004
内容:  [a₀]  [a₁]  [a₂]  [a₃]  [a₄]

访问 a[i]: address = base + i × word_size
时间复杂度: O(1)

插入 a[k] 之前:
1. 将 a[k], a[k+1], ..., a[n-1] 向后移动一位
2. a[k] = new_element
时间复杂度: O(n-k) = O(n)

删除 a[k]:
1. 将 a[k+1], ..., a[n-1] 向前移动一位
时间复杂度: O(n-k) = O(n)

2.1.2 链接存储(链表)

单向链表:
┌────┬────┐    ┌────┬────┐    ┌────┬────┐
│INFO│LINK│──→│INFO│LINK│──→│INFO│LINK│──→ Λ (空)
└────┴────┘    └────┴────┘    └────┴────┘
   node1          node2          node3

每个节点: INFO 字段(数据)+ LINK 字段(指向下一节点的地址)

插入到 node1 之后:
1. P ← AVAIL (从可用空间分配新节点)
2. INFO(P) ← new_element
3. LINK(P) ← LINK(node1)
4. LINK(node1) ← P
时间复杂度: O(1) (已知插入位置)

删除 node1 之后的节点:
1. P ← LINK(node1)
2. LINK(node1) ← LINK(P)
3. AVAIL ← P (回收到可用空间)
时间复杂度: O(1)

查找第 k 个元素:
从头部开始遍历 k 步
时间复杂度: O(k)

2.2 栈和队列

2.2.1 栈(Stack)

栈: LIFO (Last In, First Out)

操作:
PUSH(S, x): 将 x 压入栈 S 的顶部
POP(S): 删除并返回栈顶元素
TOP(S): 返回栈顶元素(不删除)

数组实现:
TOP 指针指向栈顶
PUSH: S[++TOP] = x
POP: return S[TOP--]

应用:
- 函数调用栈
- 表达式求值(中缀转后缀)
- 括号匹配
- 深度优先搜索

2.2.2 队列(Queue)

队列: FIFO (First In, First Out)

操作:
ENQUEUE(Q, x): 将 x 加入队尾
DEQUEUE(Q): 删除并返回队首元素

循环数组实现:
FRONT 和 REAR 指针
ENQUEUE: Q[REAR] = x; REAR = (REAR + 1) mod M
DEQUEUE: x = Q[FRONT]; FRONT = (FRONT + 1) mod M

应用:
- 广度优先搜索
- 任务调度
- 缓冲区

2.3 树

2.3.1 树的定义

树(Tree)的递归定义:
- 空集是树
- 如果 T₁, T₂, ..., Tₖ 是互不相交的树,则
  T = {root} ∪ T₁ ∪ T₂ ∪ ... ∪ Tₖ 也是树
  root 称为根,T₁, ..., Tₖ 称为子树

术语:
- 根(Root): 没有父节点的节点
- 叶(Leaf): 没有子节点的节点
- 度(Degree): 子节点的数量
- 深度(Depth): 从根到该节点的路径长度
- 高度(Height): 从该节点到最深叶的路径长度
- 有序树: 子节点有顺序(长子、次子...)
- 二叉树: 每个节点最多有两个子节点(左、右)

2.3.2 树的遍历

前序遍历(Preorder): 根 → 左子树 → 右子树
中序遍历(Inorder): 左子树 → 根 → 右子树
后序遍历(Postorder): 左子树 → 右子树 → 根

示例树:
        A
       / \
      B   C
     / \
    D   E

前序: A B D E C
中序: D B E A C
后序: D E B C A

应用:
- 前序: 复制树、序列化
- 中序: 二叉搜索树的有序遍历
- 后序: 删除树、表达式树求值

2.4 树的存储表示

1. 子节点链表表示(最通用):
   每个节点: INFO, FIRST_CHILD, NEXT_SIBLING
   
   A → B → C
        ↓    ↓
        D → E

2. 左右子女表示(二叉树):
   每个节点: INFO, LEFT, RIGHT
   
   A: LEFT=B, RIGHT=C
   B: LEFT=D, RIGHT=E

3. 数组表示(完全二叉树):
   节点 i 的左子女: 2i
   节点 i 的右子女: 2i+1
   节点 i 的父节点: ⌊i/2⌋

三、实战案例

3.1 案例一:MIX 实现链表插入

算法: 在有序链表中插入一个元素

输入: 链表头指针 HEAD,待插入值 X
输出: 插入后的链表

MIX 实现:
       ENTA   0
       STX    INFO_NEW   新节点的 INFO = X
       
SEARCH LDA    HEAD       P ← HEAD
       JAZ    INSERT_FIRST  如果链表为空,插入到头部
LOOP   LD1    0,1(0:2)   取 P 的 LINK
       J1Z    INSERT_END  如果 P 是最后一个节点
       CMPA   0,1(3:5)   比较 X 和 INFO(P)
       JG     INSERT_HERE 如果 X > INFO(P),在 P 之后插入
       LD1    0,1(0:2)   P ← LINK(P)
       JMP    LOOP

INSERT_HERE:
       ENTA   0
       ST1    LINK_NEW   LINK(NEW) ← LINK(P)
       STX    0,1(0:2)   LINK(P) ← NEW (需要调整)
       ...

3.2 案例二:二叉树遍历的 MIX 实现

前序遍历的 MIX 实现(递归):

PREORDER STJ   RET_ADDR   保存返回地址
         LDA   0,1(3:5)   访问节点 INFO
         ...              处理节点
         LD1   0,1(0:2)   P ← LEFT
         J1Z   NO_LEFT
         JSJ   PREORDER   递归遍历左子树
NO_LEFT  LD1   RIGHT_PTR  P ← RIGHT
         J1Z   NO_RIGHT
         JSJ   PREORDER   递归遍历右子树
NO_RIGHT LDJ   RET_ADDR
         JMP   0,4        返回

四、本章小结

线性列表有两种基本表示:连续存储(数组,O(1) 随机访问)和链接存储(链表,O(1) 插入/删除)

栈(LIFO)和队列(FIFO)是两种受限的线性结构,各自有广泛的应用

树是递归定义的非线性结构,前序/中序/后序遍历是基本操作

存储表示的选择取决于具体操作的需求——频繁随机访问选数组,频繁插入删除选链表


五、思考与练习

思考题

如何用两个栈实现一个队列?如何用一个队列实现一个栈?

给定一棵二叉树的前序遍历和中序遍历序列,能否唯一确定这棵树?如何构造?

循环链表相比普通链表有什么优势?

编程练习

用 MIX 汇编实现双向链表的插入和删除操作。

实现二叉树的层序遍历(使用队列)。

实现用数组表示的完全二叉树,支持插入和删除(堆操作)。


2/5信息结构
0%