1. 树与二叉树基础概念精讲
树结构是计算机科学中最重要的非线性数据结构之一,广泛应用于文件系统、数据库索引等领域。在考研数据结构中,树与二叉树章节占据约15%的分值比重,是必须掌握的核心内容。
树的定义可以类比现实中的家族树:有一个唯一的根节点(祖先),每个节点可以有多个子节点(后代),但只能有一个父节点(直接上级)。这种"一对多"的关系使得树特别适合表示层级关系。例如操作系统中的目录结构就是典型的树形应用。
二叉树是每个节点最多有两个子节点的特殊树结构。与普通树不同,二叉树严格区分左子树和右子树,即使只有一个子节点也必须明确其左右位置。这个特性使得二叉树在算法实现上更加规范。
常见易错点:
- 混淆"度为m的树"与"m叉树":前者至少有一个节点度为m,后者所有节点度≤m
- 忽略二叉树是有序树:交换左右子树会得到不同的二叉树
- 计算结点数时漏算根节点:总节点数=总度数+1(根节点没有父节点)
2. 二叉树核心性质与计算技巧
2.1 特殊二叉树特征对比
| 类型 | 节点特征 | 层序编号规则 | 应用场景 |
|---|---|---|---|
| 满二叉树 | 所有非叶节点度为2,叶子全在最底层 | i的左孩子2i,右孩子2i+1 | 堆结构基础 |
| 完全二叉树 | 仅最后两层有叶子,且左对齐 | 同满二叉树 | 优先队列实现 |
| 二叉排序树 | 左<根<右的有序关系 | 无固定规则 | 动态查找系统 |
| 平衡二叉树 | 任意节点左右子树高度差≤1 | 通过平衡因子调整 | 数据库索引 |
完全二叉树的性质在考研中频繁出现:
- 当n为偶数时,有1个度为1的节点(且必为左孩子)
- 分支节点与叶节点的分界点是⌊n/2⌋
- 高度为h的完全二叉树至少2^(h-1)个节点,至多2^h-1个节点
真题示例: 已知完全二叉树第6层有8个叶子,求最少节点数。 解:前5层满节点=31,第6层8叶说明第5层有⌈8/2⌉=4个非叶节点,总节点=31+4+8=43
2.2 遍历序列还原二叉树
二叉树的遍历是考试重点,三种遍历方式的非递归实现需要熟练掌握。更关键的是根据遍历序列还原二叉树:
必须包含中序序列才能唯一确定二叉树形态。解题步骤:
- 用前序/后序的第一个/最后一个元素确定根节点
- 在中序序列中找到根的位置,划分左右子树
- 递归处理左右子树
高频错题: 已知前序ABDECFG,中序DBEAFCG,求后序。 解:A为根,左子树(DBE)右子树(FCG)→左子树前序BDE→B为根→D左E右→右子树前序CFG→C为根→F左G右→后序DEBFGCA
3. 线索二叉树实战解析
3.1 线索化原理
普通二叉树存在大量空指针域(n个节点有n+1个空链域),线索化利用这些空域存储遍历顺序中的前驱后继信息。通过增加两个标志位:
- ltag=0时lchild指向左孩子,1时指向前驱
- rtag=0时rchild指向右孩子,1时指向后继
中序线索化代码实现:
void InThread(ThreadTree p, ThreadTree &pre) { if(p) { InThread(p->lchild, pre); // 递归左子树 if(!p->lchild) { // 左子树为空建立前驱线索 p->lchild = pre; p->ltag = 1; } if(pre && !pre->rchild) { // 前驱节点右子树为空建立后继线索 pre->rchild = p; pre->rtag = 1; } pre = p; // 更新pre指针 InThread(p->rchild, pre); // 递归右子树 } }3.2 线索二叉树的遍历
线索二叉树的最大优势是可以直接找到前驱后继,无需递归栈:
找中序后继:
ThreadNode *NextNode(ThreadNode *p) { if(p->rtag == 1) return p->rchild; // 直接返回线索 else { p = p->rchild; // 进入右子树 while(p->ltag == 0) // 找最左下节点 p = p->lchild; return p; } }易错警示:
- 先序线索二叉树中找前驱需要知道父节点指针(普通线索无法实现)
- 后序线索二叉树中找后继同样需要父节点信息
- 中序线索是唯一能同时方便找前驱后继的情况
4. 树与森林的转换技巧
4.1 存储结构对比
| 存储方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 双亲表示法 | 找父节点快O(1) | 找孩子需要遍历 | 并查集实现 |
| 孩子表示法 | 找孩子方便 | 找父节点困难 | 需要频繁查询子节点 |
| 孩子兄弟表示法 | 树与二叉树转换的桥梁 | 找父节点需额外处理 | 森林与二叉树转换 |
孩子兄弟表示法(二叉树表示法)是最重要的存储方式:
- firstchild相当于二叉树的左孩子
- nextsibling相当于二叉树的右孩子
- 这种表示法使得树和二叉树的算法可以相互转换
4.2 森林与二叉树转换
转换规则:
- 每棵树的根节点作为二叉树的根
- 树中节点的第一个孩子作为二叉树的左孩子
- 树中节点的右兄弟作为二叉树的右孩子
重要性质:
- 森林的先序遍历序列 = 对应二叉树的先序序列
- 森林的后序遍历序列 = 对应二叉树的中序序列(注意不是后序!)
5. 高频考点:平衡二叉树调整
平衡二叉树(AVL树)的调整是每年必考大题,需要掌握四种旋转场景:
5.1 失衡类型与调整
LL型:在A的左孩子的左子树插入导致失衡
- 右旋操作:将B作为根,A作为B的右孩子,BR作为A的左孩子
RR型:在A的右孩子的右子树插入导致失衡
- 左旋操作:将B作为根,A作为B的左孩子,BL作为A的右孩子
LR型:在A的左孩子的右子树插入导致失衡
- 先左旋后右旋:先对C左旋变成LL型,再整体右旋
RL型:在A的右孩子的左子树插入导致失衡
- 先右旋后左旋:先对C右旋变成RR型,再整体左旋
记忆口诀: "LL右旋,RR左旋,LR先左后右,RL先右后左"
5.2 平衡二叉树插入示例
插入序列:{15, 3, 7, 10, 9, 8}
- 插入15和3后正常
- 插入7时节点15失衡(LR型):
- 先对3左旋:3作为7的左孩子
- 再对15右旋:7作为根,3左15右
- 插入10后无失衡
- 插入9时节点15失衡(RL型):
- 先对10右旋:10作为9的右孩子
- 再对15左旋:9作为根,7左15右
- 插入8时节点9失衡(LR型):
- 先对7左旋:7作为8的左孩子
- 再对9右旋:8作为根,7左9右
6. 哈夫曼树与编码实战
哈夫曼树是带权路径长度(WPL)最小的二叉树,用于数据压缩:
构建步骤:
- 将所有权值作为单独的树
- 每次选取权值最小的两棵树合并,新树权值为两者之和
- 重复直到只剩一棵树
真题技巧:
- 哈夫曼树没有度为1的节点(n个叶子共有2n-1个节点)
- 权值小的节点路径更长
- 同一组权值可能生成不同形态的哈夫曼树,但WPL相同
编码实现:
typedef struct { int weight; int parent, lch, rch; } HTNode; void CreateHuffmanTree(HTNode ht[], int n) { for(int i=1; i<=2*n-1; ++i) ht[i] = {0,0,0,0}; for(int i=1; i<=n; ++i) cin >> ht[i].weight; for(int i=n+1; i<=2*n-1; ++i) { int s1, s2; Select(ht, i-1, s1, s2); // 选择两个最小权值 ht[i].weight = ht[s1].weight + ht[s2].weight; ht[s1].parent = ht[s2].parent = i; ht[i].lch = s1; ht[i].rch = s2; } }在最后的冲刺阶段,建议每天手写一遍二叉树的非递归遍历代码,并完成2-3道平衡二叉树调整的练习题。对于线索二叉树,要重点理解不同遍历顺序下前驱后继的查找逻辑差异。实际编程时,树的深度优先搜索与二叉树遍历有诸多相似之处,这种触类旁通的能力正是考研考察的重点。