5.1 树的基本概念

树 (Tree)
n (n≥0) 个结点的有限集合。空树记为 ∅。任一非空树 T 满足:① 有且仅有一个根;② 其余结点可分为 m (m>0) 棵互不相交的子树 T₁, T₂, …, Tₘ。
性质:① 树中结点数 = 所有结点度数之和 + 1;② 含 n 个结点的树有 n−1 条边。

5.2 树的存储结构

1. 双亲表示法

每个结点存储一个指向其双亲的指针(或下标)。适合从孩子找双亲的场景,但找孩子需遍历。

2. 孩子表示法

每个结点存储指向其所有孩子的指针链表。适合从双亲找孩子。

3. 孩子双亲表示法

结合以上两种,同时存储孩子链表和双亲指针。

4. 二叉链表表示法

每个结点含两个指针:firstchild(第一个孩子)和 nextsibling(下一个兄弟)。这种表示法自然地将树转换为二叉树。

5.3 二叉树

二叉树 (Binary Tree)
每个结点最多有两棵子树的有序树,子树有左右之分。

二叉树的性质

特殊二叉树

二叉树的存储

5.4 二叉树的遍历

按根结点访问顺序分为四种遍历:

void PreOrder(BTNode *t){
  if(t){ visit(t); PreOrder(t->lchild); PreOrder(t->rchild); }
}
void InOrder(BTNode *t){
  if(t){ InOrder(t->lchild); visit(t); InOrder(t->rchild); }
}
void PostOrder(BTNode *t){
  if(t){ PostOrder(t->lchild); PostOrder(t->rchild); visit(t); }
}

二叉树遍历动画

交互式
速度 1x

5.5 线索二叉树

n 个结点的二叉链表中有 n+1 个空指针域,利用这些空指针域指向遍历序列中的前驱和后继,称为 线索

5.6 树与二叉树的转换

森林→二叉树
采用"左孩子右兄弟"表示法:将每棵树的根作为兄弟,每个结点的第一个孩子作为 lchild,下一个兄弟作为 rchild。

转换步骤:

  1. 将每棵树转换为二叉树(左孩子右兄弟)。
  2. 将各棵二叉树的根结点相连作为兄弟。
  3. 连接后的整体即为森林对应的二叉树。
二叉树→森林
若二叉树根的右子树非空,则将右子树断开形成森林,再将森林中各二叉树还原为一般树。
关键:树的前序 = 对应二叉树的前序;树的后序 = 对应二叉树的中序。

5.7 哈夫曼树

哈夫曼树 (Huffman Tree)
带权路径长度 (WPL) 最小的二叉树。WPL = Σ(叶子权值 × 路径长度)。

哈夫曼树的构造步骤

  1. 将 n 个带权结点看作 n 棵只有一个结点的二叉树,构成森林 F。
  2. 在 F 中选取两棵权值最小的树作为左右子树构造新二叉树,新树根权值为两子树根权之和。
  3. 将新树加入 F,删除原两棵树。
  4. 重复步骤 2-3 直到 F 中只剩一棵树。

哈夫曼编码

从根到每个叶子的路径上的左"0"右"1"序列即为该字符的哈夫曼编码。哈夫曼编码是前缀编码,任一字符的编码不是其他字符编码的前缀。

哈夫曼树构建动画

交互式

5.8 并查集 (Disjoint Set Union)

并查集
维护一个元素集合,支持两个操作:Find(x)(查找 x 所在集合的代表元素)和 Union(x, y)(将 x 和 y 所在集合合并)。

实现方式

int Find(int x){
  if(parent[x] != x) parent[x] = Find(parent[x]);  // 路径压缩
  return parent[x];
}
void Union(int x, int y){
  x = Find(x); y = Find(y);
  if(rank[x] < rank[y]) parent[x] = y;
  else if(rank[x] > rank[y]) parent[y] = x;
  else { parent[y] = x; rank[x]++; }
}
时间复杂度:采用路径压缩和按秩合并优化后,Find 和 Union 的时间复杂度为 O(α(n))(反阿克曼函数,可视为常数)。

并查集 DSU(按秩合并 + 路径压缩)

交互式
速度1x

5.9 线索二叉树动画演示

利用 n+1 个空指针域存放中序前驱/后继。ltag=0指向左孩子, ltag=1指向前驱; rtag同理。

中序线索化

交互式动画
速度 1x
步骤 0/0

5.10 树/森林与二叉树转换

左孩子右兄弟表示法: 第一个孩子->左孩子, 下一个兄弟->右孩子。左侧为原树(A有3个孩子B,C,D; B有2个孩子E,F; D有1个孩子G), 右侧为转换后的二叉树。

树 -> 二叉树转换

交互式动画
速度 1x
步骤 0/0

5.11 红黑树插入动画演示

红黑树是自平衡二叉搜索树,通过5条颜色性质保证O(log n)操作。插入时新节点默认为红色,若违反性质则需调整:Case1叔父红→颜色翻转,Case2/3叔父黑→旋转+重着色。

红黑树插入演示 (30,20,40,10,25,5,15)

交互式动画
速度 1x
步骤 0/0

5.12 堆与优先队列 [25新增]

堆 (Heap)
一种特殊的完全二叉树,每个结点的值都满足特定的偏序关系。分为两种:大根堆(每个结点的值 ≥ 其所有孩子结点的值)和小根堆(每个结点的值 ≤ 其所有孩子结点的值)。

堆的核心特性:根结点是整个堆中的最大值(大根堆)或最小值(小根堆)。堆本身是一棵完全二叉树,但不要求左右子树有序(与二叉搜索树不同)。

堆的存储结构

由于堆是一棵完全二叉树,自然使用数组进行顺序存储:

注意:有些教材使用 1 下标(根在 A[1]),此时左孩子 2i,右孩子 2i+1,父结点 ⌊i/2⌋。本处统一采用 0 下标。

堆的基本操作

1. 上滤 (Percolate Up / Sift Up)

当新元素插入到堆的末尾时,需要向上调整以维护堆的性质:将新结点与其父结点比较,若违反堆序则交换,重复此过程直到堆序恢复。

void PercolateUp(int heap[], int i){
  while(i > 0 && heap[i] > heap[(i-1)/2]){  // 大根堆
    swap(heap[i], heap[(i-1)/2]);
    i = (i-1)/2;
  }
}

2. 下滤 (Percolate Down / Sift Down)

当堆顶元素被替换(或删除)后,新的堆顶需要向下调整:将当前结点与其较大(大根堆)或较小(小根堆)的孩子比较,若违反堆序则交换,重复此过程。

void PercolateDown(int heap[], int n, int i){
  int largest = i;
  int l = 2*i + 1, r = 2*i + 2;  // 左右孩子
  if(l < n && heap[l] > heap[largest]) largest = l;
  if(r < n && heap[r] > heap[largest]) largest = r;
  if(largest != i){
    swap(heap[i], heap[largest]);
    PercolateDown(heap, n, largest);
  }
}

3. 插入 (Insert) — O(log n)

将新元素放在堆的末尾,然后执行上滤操作使其恢复到正确位置。最坏情况下需要上滤到根,比较次数为树高 O(log n)。

4. 删除堆顶 (Delete) — O(log n)

用堆的最后一个元素替换堆顶,堆大小减 1,然后对新的堆顶执行下滤操作。最坏情况下需要下滤到叶子,比较次数为 O(log n)。

建堆 (Build Heap)

给定一个无序数组,可以通过自底向上的方法在 O(n) 时间内建成堆:

  1. 从最后一个非叶子结点 ⌊n/2⌋ − 1(0 下标)开始,向前遍历每个结点。
  2. 对每个结点执行下滤操作,使其成为以该结点为根的合法堆。
void BuildHeap(int heap[], int n){
  for(int i = n/2 - 1; i >= 0; i--)
    PercolateDown(heap, n, i);
}

时间复杂度推导:为什么建堆是 O(n)?

初看每个结点都执行下滤,似乎是 O(n log n),但经过更精确的分析可以得到 O(n):

关键考点:建堆 O(n),堆排序 O(n log n)。建堆不能通过反复插入来实现(那样是 O(n log n)),必须使用自底向上的下滤方法。

堆的应用

堆 vs 二叉搜索树 (BST)

特性二叉搜索树
结构完全二叉树(形状确定)任意二叉树(形状取决于插入顺序)
偏序关系父 ≥ 子(大根堆)/ 父 ≤ 子(小根堆)左子树 < 根 < 右子树
存储方式数组(顺序存储)链式存储为主
查找最值O(1) — 堆顶即最值O(log n) ~ O(n)
查找任意值O(n) — 不支持高效查找O(log n) ~ O(n)
插入 / 删除O(log n)O(log n) ~ O(n)
建树O(n)(自底向上下滤)O(n log n)(逐个插入)
有序遍历不支持中序遍历即有序
典型应用优先队列、堆排序、Top-K动态集合查找、范围查询
总结:堆侧重"快速获取最值",BST 侧重"有序查找"。堆适合只需要最值的场景(如优先队列),BST 适合需要有序遍历或范围查询的场景。