第4章:树(Trees)
一、导读
1.1 本章学习目标
第4章介绍树这种重要的非线性数据结构。与前面章节的线性结构不同,树结构天然适合表示具有层次关系的数据。从文件系统到数据库索引,从编译器语法树到网络路由,树结构无处不在。
本章的核心学习目标包括:
- 理解树的基本术语和性质:根、叶、深度、高度、度、路径等概念
- 掌握二叉树的遍历方法:前序、中序、后序、层序遍历的递归和迭代实现
- 深入理解二叉搜索树(BST):查找、插入、删除操作的实现和复杂度分析
- 理解平衡树的重要性:为什么BST可能退化为链表?AVL树如何解决这个问题?
- 了解B树的基本概念:多路搜索树如何适用于磁盘存储?
- 掌握树的实际应用:表达式树、Huffman编码、文件系统的目录结构
1.2 为什么第四章如此重要
树是计算机科学中最重要的数据结构之一。它不仅是一种存储结构,更是一种思维方式——将复杂问题分解为层次化的子问题。
二叉搜索树是后续学习平衡树(红黑树、B树、B+树)的基础。理解BST的退化问题和平衡的必要性,是理解高级数据结构设计动机的关键。
此外,树的遍历算法(前序、中序、后序)是递归思想的经典应用,在编译器、操作系统、图形学等领域都有广泛应用。
1.3 核心问题引导
一棵有 n 个节点的二叉树,最多有多少个叶节点?
二叉搜索树在什么情况下会退化为链表?如何避免?
AVL树的四种旋转操作分别在什么情况下触发?
为什么数据库索引通常使用B树而不是二叉搜索树?
如何通过前序遍历和中序遍历唯一确定一棵二叉树?
Huffman编码为什么能实现数据压缩?它的前缀性质有什么作用?
二、核心概念详解
4.1 树的基本概念
树是由 n(n≥0)个节点组成的有限集合。当 n=0 时称为空树。在非空树中:
- 有一个特殊的节点称为根(root)
- 其余节点可分为 m(m≥0)个互不相交的子集 T₁, T₂, ..., Tₘ,每个子集本身又是一棵树,称为根的子树
关键术语:
- 节点的度:节点的子树个数
- 叶节点:度为0的节点
- 分支节点:度不为0的节点
- 深度/高度:从根到该节点的路径长度
- 树的深度:树中节点的最大深度
4.2 二叉树
二叉树是每个节点最多有两个子树(左子树和右子树)的树结构,且子树有左右之分。
typedef struct TreeNode {
int element;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
// 创建新节点
TreeNode *createNode(int val) {
TreeNode *node = (TreeNode *)malloc(sizeof(TreeNode));
node->element = val;
node->left = node->right = NULL;
return node;
}4.2.1 二叉树的遍历
// 前序遍历:根-左-右
void preorder(TreeNode *T) {
if (T == NULL) return;
printf("%d ", T->element);
preorder(T->left);
preorder(T->right);
}
// 中序遍历:左-根-右
void inorder(TreeNode *T) {
if (T == NULL) return;
inorder(T->left);
printf("%d ", T->element);
inorder(T->right);
}
// 后序遍历:左-右-根
void postorder(TreeNode *T) {
if (T == NULL) return;
postorder(T->left);
postorder(T->right);
printf("%d ", T->element);
}
// 层序遍历(BFS)
void levelorder(TreeNode *root) {
if (root == NULL) return;
// 使用队列
TreeNode *queue[1000];
int front = 0, rear = 0;
queue[rear++] = root;
while (front < rear) {
TreeNode *curr = queue[front++];
printf("%d ", curr->element);
if (curr->left) queue[rear++] = curr->left;
if (curr->right) queue[rear++] = curr->right;
}
}遍历的应用:
- 中序遍历BST得到有序序列
- 后序遍历用于计算目录大小、释放内存
- 前序遍历用于复制树、序列化
4.3 二叉搜索树(BST)
二叉搜索树满足以下性质:对于每个节点 X,左子树中所有节点的值小于 X 的值,右子树中所有节点的值大于 X 的值。
// 查找 - O(h),h为树高
TreeNode *find(TreeNode *T, int x) {
if (T == NULL) return NULL;
if (x < T->element) return find(T->left, x);
else if (x > T->element) return find(T->right, x);
else return T;
}
// 查找最小值
TreeNode *findMin(TreeNode *T) {
if (T == NULL) return NULL;
while (T->left != NULL)
T = T->left;
return T;
}
// 插入 - O(h)
TreeNode *insert(TreeNode *T, int x) {
if (T == NULL) {
T = createNode(x);
} else if (x < T->element) {
T->left = insert(T->left, x);
} else if (x > T->element) {
T->right = insert(T->right, x);
}
return T;
}
// 删除 - O(h)
TreeNode *delete(TreeNode *T, int x) {
if (T == NULL) return NULL;
if (x < T->element)
T->left = delete(T->left, x);
else if (x > T->element)
T->right = delete(T->right, x);
else {
// 找到要删除的节点
if (T->left == NULL && T->right == NULL) {
free(T);
return NULL;
} else if (T->left == NULL) {
TreeNode *tmp = T->right;
free(T);
return tmp;
} else if (T->right == NULL) {
TreeNode *tmp = T->left;
free(T);
return tmp;
} else {
// 有两个子节点:用右子树的最小值替代
TreeNode *minNode = findMin(T->right);
T->element = minNode->element;
T->right = delete(T->right, minNode->element);
}
}
return T;
}BST的复杂度分析:
- 平均情况:O(log n)(树近似平衡时)
- 最坏情况:O(n)(树退化为链表时,如按序插入)
4.4 AVL树
AVL树是最早的自平衡二叉搜索树。它要求每个节点的左右子树高度差不超过1。通过旋转操作维护平衡,保证树高始终为 O(log n)。
int height(TreeNode *T) {
if (T == NULL) return -1;
int lh = height(T->left);
int rh = height(T->right);
return (lh > rh ? lh : rh) + 1;
}
#define BF(T) (height(T->left) - height(T->right))
// 右旋(LL型)
TreeNode *rotateRight(TreeNode *k2) {
TreeNode *k1 = k2->left;
k2->left = k1->right;
k1->right = k2;
return k1;
}
// 左旋(RR型)
TreeNode *rotateLeft(TreeNode *k1) {
TreeNode *k2 = k1->right;
k1->right = k2->left;
k2->left = k1;
return k2;
}
// 平衡插入
TreeNode *avlInsert(TreeNode *T, int x) {
if (T == NULL) return createNode(x);
if (x < T->element)
T->left = avlInsert(T->left, x);
else if (x > T->element)
T->right = avlInsert(T->right, x);
else
return T;
int balance = BF(T);
// LL型
if (balance > 1 && x < T->left->element)
return rotateRight(T);
// RR型
if (balance < -1 && x > T->right->element)
return rotateLeft(T);
// LR型
if (balance > 1 && x > T->left->element) {
T->left = rotateLeft(T->left);
return rotateRight(T);
}
// RL型
if (balance < -1 && x < T->right->element) {
T->right = rotateRight(T->right);
return rotateLeft(T);
}
return T;
}AVL树的性质:
- 树高不超过 1.44·log(n+2)
- 查找、插入、删除均为 O(log n)
- 插入最多需要1次旋转,删除可能需要 O(log n) 次旋转
4.5 B树
B树是一种多路搜索树,专为磁盘等外存设备设计。一个 m 阶B树满足:
- 每个节点最多有 m 个子节点
- 除根节点外,每个节点至少有 ⌈m/2⌉ 个子节点
- 所有叶节点在同一层
#define ORDER 4
typedef struct BTreeNode {
int keys[ORDER - 1];
struct BTreeNode *children[ORDER];
int n; // 当前关键字数
int leaf; // 是否为叶节点
} BTreeNode;
// B树查找
BTreeNode *btreeSearch(BTreeNode *x, int k) {
int i = 0;
while (i < x->n && k > x->keys[i])
i++;
if (i < x->n && k == x->keys[i])
return x;
if (x->leaf)
return NULL;
return btreeSearch(x->children[i], k);
}
// B树查找复杂度:O(log_m(n)),其中m为阶数
// 由于m很大(如1000),树高很小(如3-4层)
// 每次查找只需少量磁盘I/O三、实战案例
3.1 表达式树
// 判断字符是否为运算符
int isOperator(char c) {
return c == '+' || c == '-' || c == '*' || c == '/';
}
// 计算表达式树的值
int evalTree(TreeNode *T) {
if (T == NULL) return 0;
if (T->left == NULL && T->right == NULL)
return T->element; // 叶节点是操作数
int leftVal = evalTree(T->left);
int rightVal = evalTree(T->right);
switch (T->element) {
case '+': return leftVal + rightVal;
case '-': return leftVal - rightVal;
case '*': return leftVal * rightVal;
case '/': return rightVal != 0 ? leftVal / rightVal : 0;
}
return 0;
}
// 表达式树 ((3+4)*2) 的值 = 143.2 Huffman编码
typedef struct HuffNode {
int freq;
char ch;
struct HuffNode *left;
struct HuffNode *right;
} HuffNode;
// 构建Huffman树(简化版)
HuffNode *buildHuffmanTree(int freq[], char chars[], int n) {
// 使用优先队列(最小堆)
// 每次取出两个最小频率的节点合并
// 重复直到只剩一个节点
// 此处省略优先队列的实现细节
return NULL; // 占位
}
// 生成Huffman编码表
void generateCodes(HuffNode *root, char *code, int depth) {
if (root == NULL) return;
if (root->left == NULL && root->right == NULL) {
code[depth] = '\0';
printf("'%c': %s\n", root->ch, code);
return;
}
code[depth] = '0';
generateCodes(root->left, code, depth + 1);
code[depth] = '1';
generateCodes(root->right, code, depth + 1);
}
// Huffman编码是最优前缀编码
// 频率高的字符编码短,频率低的编码长
// 总编码长度最小四、本章小结
树是表示层次关系的非线性结构。二叉树是最常用的树形结构。
二叉搜索树支持 O(log n) 的查找、插入和删除(平均情况),但可能退化为 O(n)。
AVL树通过旋转操作维护平衡,保证 O(log n) 的最坏情况性能。
B树是多路搜索树,适用于磁盘存储,通过增大节点度数减少树高和I/O次数。
树的遍历(前序、中序、后序、层序)是递归和BFS思想的经典应用。
五、思考与练习
思考题
证明:含有 n 个节点的二叉树有 n-1 条边。
给定前序遍历为 [1,2,4,5,3,6],中序遍历为 [4,2,5,1,6,3],画出这棵二叉树。
为什么B树比二叉搜索树更适合数据库索引?
编程练习
实现二叉树的非递归遍历(使用栈)。
实现AVL树的完整插入和删除操作。
用Huffman编码实现一个简单的文件压缩程序。