树与二叉树 (Tree & Binary Tree)

title: ""

章节概述

树(Tree)是一种层次化的非线性数据结构,每个节点可以有零个或多个子节点。二叉树是
最常见的树结构变体,每个节点最多有两个子节点。树在计算机科学中无处不在——文件系统、
数据库索引、编译器的抽象语法树、网络路由表等。

本节侧重底层实现与内存理解,与 CPP教程对应章节 形成互补——CPP教程侧重 std::set/std::map(基于红黑树)的STL使用、迭代器语义和算法,本教程侧重手动实现二叉搜索树和AVL树的插入/删除/旋转,深刻理解树节点间的指针关系。

在CPP教程中对应章节侧重 std::set/std::map 的 STL 用法与红黑树特性,本节侧重手动实现 BST 和 AVL 树——亲手操作每个节点的 left/right 指针与旋转。



第一节: 二叉树的基本概念与结构


1.1 二叉树的节点定义

#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
 
typedef struct TreeNode {
 int val;
 struct TreeNode *left;
 struct TreeNode *right;
} TreeNode;

内存布局: 每个节点在堆上独立分配。节点间通过 leftright 指针连接——
本质上是一个有向无环图(DAG)的特殊形式。

graph TD
 root["root 1"] --> left["2"]
 root --> right["3"]
 left --> ll["4"]
 left --> lr["5"]
 right --> rl["6"]
 right --> rr["7"]

3 个节点间的关系:root->left->val == 2root->right->right->val == 7


1.2 二叉树的创建与基本操作

TreeNode* treenode_create(int val) {
 TreeNode *node = (TreeNode*)malloc(sizeof(TreeNode));
 if (!node) return NULL;
 node->val = val;
 node->left = NULL;
 node->right = NULL;
 return node;
}
 
// 手动构建一棵树(用于测试)
TreeNode* build_sample_tree() {
 TreeNode *root = treenode_create(1);
 root->left = treenode_create(2);
 root->right = treenode_create(3);
 root->left->left = treenode_create(4);
 root->left->right = treenode_create(5);
 root->right->left = treenode_create(6);
 root->right->right = treenode_create(7);
 return root;
}
 
// 计算节点数 —— 递归
int tree_size(const TreeNode *root) {
 if (!root) return 0;
 return 1 + tree_size(root->left) + tree_size(root->right);
}
 
// 计算高度 —— 递归
int tree_height(const TreeNode *root) {
 if (!root) return 0;
 int left_h = tree_height(root->left);
 int right_h = tree_height(root->right);
 return 1 + (left_h > right_h ? left_h : right_h);
}
 
// 释放所有节点(后序遍历)
void tree_destroy(TreeNode *root) {
 if (!root) return;
 tree_destroy(root->left);
 tree_destroy(root->right);
 free(root);
}

练习 1.2.1: 为什么树的销毁使用后序遍历?如果用前序遍历(先 free 根节点),会出现什么问题?


1.3 树遍历:递归版

三种标准遍历方式,区别在于访问根节点的时机:

// 前序遍历: 根 → 左 → 右
void preorder(const TreeNode *root) {
 if (!root) return;
 printf("%d ", root->val);
 preorder(root->left);
 preorder(root->right);
}
 
// 中序遍历: 左 → 根 → 右
void inorder(const TreeNode *root) {
 if (!root) return;
 inorder(root->left);
 printf("%d ", root->val);
 inorder(root->right);
}
 
// 后序遍历: 左 → 右 → 根
void postorder(const TreeNode *root) {
 if (!root) return;
 postorder(root->left);
 postorder(root->right);
 printf("%d ", root->val);
}

对上面 sample tree 的结果:

  • 前序: 1, 2, 4, 5, 3, 6, 7
  • 中序: 4, 2, 5, 1, 6, 3, 7
  • 后序: 4, 5, 2, 6, 7, 3, 1
graph TD
 subgraph "前序: 根→左→右"
 p1["1"] --> p2["2"] --> p4["4"]
 p2 --> p5["5"]
 p1 --> p3["3"] --> p6["6"]
 p3 --> p7["7"]
 end


第二节: 迭代遍历 —— 显式用栈


2.1 迭代前序遍历

递归遍历用系统栈,迭代版本显式用自定义栈:

// 需要包含之前实现的栈(这里用简单数组模拟)
#define MAX_STACK 1000
 
void preorder_iterative(TreeNode *root) {
 if (!root) return;
 
 TreeNode *stack[MAX_STACK];
 int top = -1;
 stack[++top] = root;
 
 while (top >= 0) {
 TreeNode *node = stack[top--];
 printf("%d ", node->val);
 
 // 先压右再压左(因为栈是LIFO)
 if (node->right) stack[++top] = node->right;
 if (node->left) stack[++top] = node->left;
 }
}

为什么先压右后压左? 栈是后进先出——后压入的左子节点先弹出,实现”先左后右”的访问顺序。


2.2 迭代中序遍历 —— 经典的”左链入栈”法

void inorder_iterative(TreeNode *root) {
 TreeNode *stack[MAX_STACK];
 int top = -1;
 TreeNode *cur = root;
 
 while (cur || top >= 0) {
 // 沿左链一路到底
 while (cur) {
 stack[++top] = cur;
 cur = cur->left;
 }
 // 弹出栈顶并访问
 cur = stack[top--];
 printf("%d ", cur->val);
 // 转向右子树
 cur = cur->right;
 }
}

算法精髓: 先沿左子深入到底(将经过的节点压栈),然后弹出最后入栈的那个(最左深的节点),
访问后转向其右子。

sequenceDiagram
 participant traversal as 迭代中序
 participant stack as 栈

 Note over traversal: root = 1
 traversal->>stack: 沿左链: push 1, push 2, push 4
 Note over stack: [1,2,4] ← 栈顶
 traversal->>stack: pop 4 → 访问4
 Note over traversal: cur = 4->right = NULL
 traversal->>stack: pop 2 → 访问2
 Note over traversal: cur = 2->right = 5
 traversal->>stack: push 5
 traversal->>stack: pop 5 → 访问5
 Note over traversal: ...继续...

2.3 迭代后序遍历 —— 双栈法

后序遍历最复杂,经典解法使用两个栈:

void postorder_iterative(TreeNode *root) {
 if (!root) return;
 
 TreeNode *stack1[MAX_STACK], *stack2[MAX_STACK];
 int top1 = -1, top2 = -1;
 
 stack1[++top1] = root;
 while (top1 >= 0) {
 TreeNode *node = stack1[top1--];
 stack2[++top2] = node;
 
 if (node->left) stack1[++top1] = node->left;
 if (node->right) stack1[++top1] = node->right;
 }
 
 // stack2 中自顶向下恰好是后序遍历顺序
 while (top2 >= 0) {
 printf("%d ", stack2[top2--]->val);
 }
}

双栈法原理: Stack1 实现”根→右→左”的顺序(调整了压栈顺序),Stack2 将其反转得到”左→右→根”。

练习 2.3.1: 实现单栈后序遍历(更节省空间但更复杂)。

关于系统栈与自定义栈的对比——递归 vs 迭代,参见 。



第三节: 二叉搜索树(BST)


3.1 BST 定义与搜索

二叉搜索树(Binary Search Tree)满足:对任意节点,左子树所有值 ≤ 节点值 ≤ 右子树所有值。

graph TD
 bst8["8"] --> bst3["3"]
 bst8 --> bst10["10"]
 bst3 --> bst1["1"]
 bst3 --> bst6["6"]
 bst6 --> bst4["4"]
 bst6 --> bst7["7"]
 bst10 --> bst14["14"]
 bst14 --> bst13["13"]

BST 的查找等价于二分查找:每次比较决定走左子树还是右子树。

TreeNode* bst_search(TreeNode *root, int val) {
 TreeNode *cur = root;
 while (cur) {
 if (val == cur->val) return cur;
 if (val < cur->val)
 cur = cur->left;
 else
 cur = cur->right;
 }
 return NULL; // 未找到
}
 
// 递归版
TreeNode* bst_search_recursive(TreeNode *root, int val) {
 if (!root || root->val == val) return root;
 if (val < root->val)
 return bst_search_recursive(root->left, val);
 else
 return bst_search_recursive(root->right, val);
}

练习 3.1.1: 为什么 BST 的有序遍历(中序)输出的是升序序列?写出证明思路。


3.2 BST 插入

TreeNode* bst_insert(TreeNode *root, int val) {
 if (!root) return treenode_create(val);
 
 if (val < root->val) {
 root->left = bst_insert(root->left, val);
 } else if (val > root->val) {
 root->right = bst_insert(root->right, val);
 }
 // val == root->val: 不插入重复值(也可根据需求处理)
 
 return root;
}
 
// 迭代版
TreeNode* bst_insert_iterative(TreeNode *root, int val) {
 TreeNode *new_node = treenode_create(val);
 if (!new_node) return root;
 
 if (!root) return new_node;
 
 TreeNode *cur = root;
 while (1) {
 if (val < cur->val) {
 if (!cur->left) {
 cur->left = new_node;
 break;
 }
 cur = cur->left;
 } else if (val > cur->val) {
 if (!cur->right) {
 cur->right = new_node;
 break;
 }
 cur = cur->right;
 } else {
 free(new_node); // 重复,释放新节点
 break;
 }
 }
 return root;
}

练习 3.2.1: 如果 BST 插入的元素是递增序列(1, 2, 3, 4, 5…),得到的 BST 是什么形状?这会导致什么问题?


3.3 BST 删除 —— 三种情况

// 找右子树的最小节点(后继)
TreeNode* bst_min_node(TreeNode *node) {
 while (node && node->left) {
 node = node->left;
 }
 return node;
}
 
TreeNode* bst_delete(TreeNode *root, int val) {
 if (!root) return NULL;
 
 if (val < root->val) {
 root->left = bst_delete(root->left, val);
 } else if (val > root->val) {
 root->right = bst_delete(root->right, val);
 } else {
 // 找到了删除目标
 
 // 情况1: 叶子节点
 if (!root->left && !root->right) {
 free(root);
 return NULL;
 }
 // 情况2: 只有一个子节点
 else if (!root->left) {
 TreeNode *temp = root->right;
 free(root);
 return temp;
 }
 else if (!root->right) {
 TreeNode *temp = root->left;
 free(root);
 return temp;
 }
 // 情况3: 有两个子节点
 else {
 // 找后继(右子树最小节点)
 TreeNode *successor = bst_min_node(root->right);
 root->val = successor->val; // 用后继的值替换
 // 递归删除后继节点
 root->right = bst_delete(root->right, successor->val);
 }
 }
 return root;
}

三种删除情况图解:

graph TD
 subgraph "情况1: 叶子节点——直接删除"
 leaf["删除 4"] -.-> leaf_result["父节点的 left/right 置 NULL"]
 end
 subgraph "情况2: 一个子节点——子承父业"
 one["删除 3"] -.-> one_result["父节点指向 3 的唯一子节点"]
 end
 subgraph "情况3: 两个子节点——找后继替换"
 two["删除 8"] -.-> two_result["用 8 的后继(右子树最小值)替换 8 的值\n然后删除后继节点"]
 end

练习 3.3.1: 实现 BST 删除的前驱版本(用左子树的最大节点替代)。分析前驱和后继两种策略在树退化为链表时的性能差异。



第四节: AVL 树 —— 自平衡二叉搜索树


4.1 为什么需要平衡?

BST 在最坏情况下(插入有序序列)退化为链表,操作复杂度从 O(log n) 退化为 O(n)。
AVL 树通过维护”平衡因子”(左子树高度 - 右子树高度)来保证树的高度始终为 O(log n)。

// AVL 节点
typedef struct AVLNode {
 int val;
 int height; // 以本节点为根的子树高度
 struct AVLNode *left;
 struct AVLNode *right;
} AVLNode;
 
int avl_height(const AVLNode *node) {
 return node ? node->height : 0;
}
 
int avl_balance_factor(const AVLNode *node) {
 return node ? avl_height(node->left) - avl_height(node->right) : 0;
}
 
void avl_update_height(AVLNode *node) {
 if (node) {
 int hl = avl_height(node->left);
 int hr = avl_height(node->right);
 node->height = 1 + (hl > hr ? hl : hr);
 }
}

4.2 四种旋转

不平衡分为四种情况,对应四种旋转:

情况条件旋转
LL(左-左)左子树的左边插入右旋
RR(右-右)右子树的右边插入左旋
LR(左-右)左子树的右边插入先左旋左子,再右旋根
RL(右-左)右子树的左边插入先右旋右子,再左旋根

4.3 右旋(LL 情况)

// 右旋:左子节点"升上来"成为新的根
AVLNode* avl_rotate_right(AVLNode *y) {
 AVLNode *x = y->left;
 AVLNode *T2 = x->right; // x 的右子树(需要挂到 y 的左子)
 
 // 执行旋转
 x->right = y;
 y->left = T2;
 
 // 更新高度(顺序重要:先子后父)
 avl_update_height(y);
 avl_update_height(x);
 
 return x; // 新的根
}

右旋过程:

graph TD
 subgraph "旋转前(LL不平衡)"
 pre_y["y (bf=2)"] --> pre_x["x (bf=1)"]
 pre_x --> pre_T1["T1"]
 pre_x --> pre_T2["T2"]
 pre_y --> pre_T3["T3"]
 end
 subgraph "旋转后(平衡)"
 post_x["x (bf=0)"] --> post_T1["T1"]
 post_x --> post_y["y (bf=0)"]
 post_y --> post_T2["T2"]
 post_y --> post_T3["T3"]
 end
 pre_y -.->|右旋| post_x

4.4 左旋(RR 情况)

AVLNode* avl_rotate_left(AVLNode *x) {
 AVLNode *y = x->right;
 AVLNode *T2 = y->left;
 
 y->left = x;
 x->right = T2;
 
 avl_update_height(x);
 avl_update_height(y);
 
 return y;
}
graph TD
 subgraph "旋转前(RR不平衡)"
 pre_x["x (bf=-2)"] --> pre_T1["T1"]
 pre_x --> pre_y["y (bf=-1)"]
 pre_y --> pre_T2["T2"]
 pre_y --> pre_T3["T3"]
 end
 subgraph "旋转后(平衡)"
 post_y["y (bf=0)"] --> post_x["x (bf=0)"]
 post_y --> post_T3["T3"]
 post_x --> post_T1["T1"]
 post_x --> post_T2["T2"]
 end
 pre_x -.->|左旋| post_y

4.5 LR 和 RL 旋转(双旋转)

// LR: 先在左子做左旋,再在根做右旋
AVLNode* avl_rotate_left_right(AVLNode *node) {
 node->left = avl_rotate_left(node->left);
 return avl_rotate_right(node);
}
 
// RL: 先在右子做右旋,再在根做左旋
AVLNode* avl_rotate_right_left(AVLNode *node) {
 node->right = avl_rotate_right(node->right);
 return avl_rotate_left(node);
}
graph TD
 subgraph "LR 双旋转过程"
 lr1["y bf=2"] --> lr2["x bf=-1"]
 lr2 --> lr3["T1"]
 lr2 --> lr4["z bf=1"]
 lr4 --> lr5["T2"]
 lr4 --> lr6["T3"]
 lr1 --> lr7["T4"]
 end

 subgraph "步骤1: 左旋x"
 step1["对x执行左旋"]
 end

 subgraph "步骤2: 右旋y"
 step2["对y执行右旋 → 平衡!"]
 end

 lr1 -.-> step1 -.-> step2

4.6 完整的 AVL 插入

AVLNode* avl_insert(AVLNode *node, int val) {
 // 1. 标准 BST 插入
 if (!node) {
 AVLNode *new_node = (AVLNode*)malloc(sizeof(AVLNode));
 new_node->val = val;
 new_node->height = 1;
 new_node->left = new_node->right = NULL;
 return new_node;
 }
 
 if (val < node->val) {
 node->left = avl_insert(node->left, val);
 } else if (val > node->val) {
 node->right = avl_insert(node->right, val);
 } else {
 return node; // 不插入重复值
 }
 
 // 2. 更新高度
 avl_update_height(node);
 
 // 3. 检查平衡因子并旋转
 int bf = avl_balance_factor(node);
 
 // LL 情况
 if (bf > 1 && val < node->left->val) {
 return avl_rotate_right(node);
 }
 // RR 情况
 if (bf < -1 && val > node->right->val) {
 return avl_rotate_left(node);
 }
 // LR 情况
 if (bf > 1 && val > node->left->val) {
 return avl_rotate_left_right(node);
 }
 // RL 情况
 if (bf < -1 && val < node->right->val) {
 return avl_rotate_right_left(node);
 }
 
 return node; // 已平衡
}

关键理解: 旋转不改变 BST 的性质(左小右大),只改变树的形状(高度)。
旋转操作只修改少量指针,时间复杂度 O(1)。

练习 4.6.1: 实现 AVL 树的删除操作。AVL 删除比插入更复杂——可能删除后某个节点需要多次旋转才能恢复平衡。



第五节: 树的实际应用


5.1 在 C 中模拟文件系统树

typedef struct FSNode {
 char *name;
 int is_directory; // 0=文件, 1=目录
 struct FSNode *parent;
 struct FSNode *first_child;
 struct FSNode *next_sibling; // 左儿子右兄弟表示法
} FSNode;
 
// 左儿子右兄弟: 将任意树表示为二叉树
// first_child 指向第一个孩子
// next_sibling 指向下一个兄弟
graph TD
 root["/"] --> var["var/"]
 root --> home["home/"]
 root --> etc["etc/"]
 var --> log["log/"]
 home --> user["user/"]
 home --> admin["admin/"]

 subgraph "左儿子右兄弟二叉树表示"
 b_root["/"] --> b_var["var/"]
 b_root --> b_home["home/"]
 b_home --> b_etc["etc/"]
 b_var --> b_log["log/"]
 b_home --> b_user["user/"]
 b_user --> b_admin["admin/"]
 end

练习 5.1.1: 实现 fs_find() —— 在文件系统树中按路径查找节点(如 /home/user/docs)。


5.2 表达式树

// 表达式树的节点
typedef struct ExprNode {
 char op; // 运算符: '+', '-', '*', '/' 或 '\0' 表示数字
 int value; // 数字值(op='\0'时有效)
 struct ExprNode *left;
 struct ExprNode *right;
} ExprNode;
 
// 后序遍历计算表达式树的值
int expr_eval(const ExprNode *root) {
 if (!root->left && !root->right) { // 叶子 = 数字
 return root->value;
 }
 int left_val = expr_eval(root->left);
 int right_val = expr_eval(root->right);
 switch (root->op) {
 case '+': return left_val + right_val;
 case '-': return left_val - right_val;
 case '*': return left_val * right_val;
 case '/': return left_val / right_val;
 default: return 0;
 }
}

表达式 (3 + 4) * 5 的表达式树:

graph TD
 mul["*"] --> add["+"]
 mul --> five["5"]
 add --> three["3"]
 add --> four["4"]

练习 5.2.1: 实现将后缀表达式转换为表达式树(非递归,用栈)。



章节测试


判断题(10题)

判断题 1

二叉树中每个节点最多有两个子节点。

判断题 2

BST 的中序遍历结果一定是升序的。

判断题 3

AVL 树的平衡因子必须在 [-1, 0, 1] 范围内。

判断题 4

树的高度和节点数 n 的关系是 O(n)。

判断题 5

BST 的查找操作平均时间复杂度是 O(log n),最坏是 O(n)。

判断题 6

先序(preorder)遍历等价于 DFS,也等价于 BFS。

判断题 7

删除 AVL 树节点后,可能需要进行多次旋转才能恢复平衡。

判断题 8

迭代中序遍历中,栈的最大深度等于树的高度。

判断题 9

后序遍历释放树是唯一安全的方式——因为释放父节点前必须先释放子节点。

判断题 10

表达式树的后序遍历结果恰好是中缀表达式。


选择题(10题)

选择题 1

一棵包含 15 个节点的完全二叉树,其高度是多少?

  • A. 3

  • B. 4

  • C. 5

  • D. 15

选择题 2

向空的 BST 依次插入 5, 3, 7, 2, 4, 6, 8 后,树的高度是?

  • A. 2

  • B. 3

  • C. 4

  • D. 7

选择题 3

AVL 树中,对于 LL 不平衡(bf=2, 左子的 bf=1),应执行什么操作?

  • A. 左旋

  • B. 右旋

  • C. 先左旋再右旋

  • D. 先右旋再左旋

选择题 4

在 64 位系统上,一个 TreeNode(含 val/left/right+填充)占用多少字节?

  • A. 16

  • B. 20

  • C. 24

  • D. 32

选择题 5

寻找 BST 中某个节点的”前驱”(preorder predecessor),正确的方法是?

  • A. 右子树的最小值

  • B. 左子树的最大值

  • C. 父节点

  • D. 右子树的最大值

选择题 6

以下哪种说法是正确的?

  • A. 所有二叉树都是 BST

  • B. 所有 BST 都是 AVL 树

  • C. AVL 树是一种自平衡 BST

  • D. BST 总是 O(log n) 查找

选择题 7

后序遍历”4 5 2 6 7 3 1”中,1 是什么节点?

  • A. 某个叶子节点

  • B. 根节点

  • C. 某个左子节点

  • D. 无法确定

选择题 8

一棵高度为 h 的满二叉树有多少个节点?

  • A. h

  • B. 2

  • C. 2^h - 1

  • D. h

选择题 9

AVL 旋转后需要更新高度的顺序是?

  • A. 先父后子

  • B. 先子后父

  • C. 无所谓

  • D. 同时更新

选择题 10

二叉搜索树退化为链表时,删除一个节点的复杂度是多少?

  • A. O(1)

  • B. O(log n)

  • C. O(n)

  • D. O(n²)


编程大题

编程题 1:实现完整的 AVL 树库

要求

  1. 实现 AVL 树的 insert、delete、search、min、max
  2. 实现四种旋转(LL, RR, LR, RL)
  3. 实现增量式高度更新
  4. 实现 is_balanced() 函数——遍历整棵树验证平衡条件
  5. 实现 print_tree() —— 递归打印树结构(带缩进,显示每个节点的值和高度)

提示: 删除操作比插入复杂——删除后每个回溯的祖先节点都可能需要旋转。

编程题 2:基于 BST 实现简单的 key-value 数据库

要求

  1. 节点存储 char *keyint value
  2. 支持插入、查找、删除、范围查询(给定 [low, high] 返回所有在此范围内的 key-value)
  3. 范围查询利用 BST 中序遍历的升序特性
  4. 讨论为什么不直接用 AVL 树?(提示:AVL 删除的多次旋转比 BST 费时,对于读多写少的场景 BST 可能更快?)

提示: 范围查询是 BST 相对于哈希表的绝对优势所在。

编程题 3:实现并对比三种自平衡树

要求

  1. 分别实现 AVL 树和最简单的”替罪羊树”(Scapegoat Tree,不平衡时暴力重建子树)
  2. 两种树接收相同的 100 万随机插入序列
  3. 对比:总旋转次数(AVL)、总重建次数(替罪羊树)、最终树高度、插入总耗时
  4. 分析结果并解释为什么实际工程中常用红黑树而不是 AVL 树

提示: 红黑树比 AVL 树旋转更少(但平衡度稍差),更适合写密集型应用。

推荐练习题(力扣)

知识点题目建议
二叉树、递归力扣二叉树
二叉树遍历力扣二叉树遍历
中序+后序求先序力扣二叉树重构
前序+中序求后序力扣二叉树重构
BST、平衡树力扣平衡树
对顶堆+BST力扣堆


知识网络