一、什么是树?
树(Tree)是一种一对多 的非线性数据结构,它通过节点 和边组成层次关系,就像一棵倒过来的真实树木:根在最顶端,叶子在最末端,每个节点可以有多个子节点。
简单来说:
- 数组、链表是一对一的线性结构;
- 树是一对多的层次结构,每个节点可以有多个子节点。
树在实际开发中应用非常广泛:
- 文件系统的目录结构;
- DOM 文档对象模型;
- 数据库的索引结构;
- 路由表的前缀树;
- 表达式的语法树等。
二、树的核心术语
1. 根节点(Root)
树的最顶层节点,没有父节点,一棵树只有一个根节点。
2. 叶子节点(Leaf)
没有子节点的节点,位于树的末端。
3. 父节点与子节点
- 父节点:有边直接相连的上层节点;
- 子节点:有边直接相连的下层节点;
- 一个父节点可以有多个子节点,但一个子节点只能有一个父节点。
4. 兄弟节点(Sibling)
共享同一个父节点的节点,它们之间没有直接相连的边。
5. 子树(Subtree)
树中任意一个节点及其所有后代组成的树,子树本身也是一棵完整的树,体现了树的递归特性。
6. 深度(Depth)
从根节点到该节点的路径长度,根节点的深度为 0。
7. 高度(Height)
从该节点到最远叶子节点的路径长度,叶子节点的高度为 0。
三、二叉树的节点定义
最常用的树是二叉树,每个节点最多有两个子节点:左子节点和右子节点。
对应的 C 语言代码定义如下:
// 二叉树节点结构体
typedef struct TreeNode {
int val; // 数据域:存储节点数据
struct TreeNode* left; // 左子节点指针
struct TreeNode* right; // 右子节点指针
} TreeNode;
- 叶子节点的
left和right都指向NULL; - 树就是由多个
TreeNode通过指针串联起来的层次结构。
四、二叉树的基本操作
1. 创建节点
创建一个新的二叉树节点,初始化数据和子节点指针:
// 创建新节点
TreeNode* createNode(int val) {
TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));
node->val = val;
node->left = NULL;
node->right = NULL;
return node;
}
2. 构建二叉树
通过手动指定节点的父子关系,构建一棵二叉树:
// 构建示例二叉树
TreeNode* buildTree() {
// 创建节点
TreeNode* root = createNode(1);
TreeNode* node2 = createNode(2);
TreeNode* node3 = createNode(3);
TreeNode* node4 = createNode(4);
TreeNode* node5 = createNode(5);
TreeNode* node6 = createNode(6);
// 建立父子关系
root->left = node2;
root->right = node3;
node2->left = node4;
node2->right = node5;
node3->right = node6;
return root;
}
3. 前序遍历
前序遍历的顺序是:根节点 → 左子树 → 右子树,代码实现:
// 前序遍历
void preorderTraversal(TreeNode* root) {
if (root == NULL) return;
printf("%d ", root->val); // 先访问根节点
preorderTraversal(root->left); // 再遍历左子树
preorderTraversal(root->right); // 最后遍历右子树
}
4. 中序遍历
中序遍历的顺序是:左子树 → 根节点 → 右子树,代码实现:
// 中序遍历
void inorderTraversal(TreeNode* root) {
if (root == NULL) return;
inorderTraversal(root->left); // 先遍历左子树
printf("%d ", root->val); // 再访问根节点
inorderTraversal(root->right); // 最后遍历右子树
}
5. 后序遍历
后序遍历的顺序是:左子树 → 右子树 → 根节点,代码实现:
// 后序遍历
void postorderTraversal(TreeNode* root) {
if (root == NULL) return;
postorderTraversal(root->left); // 先遍历左子树
postorderTraversal(root->right); // 再遍历右子树
printf("%d ", root->val); // 最后访问根节点
}
6. 计算树的高度
树的高度是从根节点到最远叶子节点的路径长度,代码实现:
// 计算树的高度
int getHeight(TreeNode* root) {
if (root == NULL) return 0;
int leftHeight = getHeight(root->left);
int rightHeight = getHeight(root->right);
return (leftHeight > rightHeight ? leftHeight : rightHeight) + 1;
}
7. 计算树的节点总数
递归计算树的节点总数,代码实现:
// 计算树的节点总数
int getNodeCount(TreeNode* root) {
if (root == NULL) return 0;
return getNodeCount(root->left) + getNodeCount(root->right) + 1;
}
8. 释放树的内存
递归释放树的所有节点内存,避免内存泄漏:
// 释放树的内存
void freeTree(TreeNode* root) {
if (root == NULL) return;
freeTree(root->left);
freeTree(root->right);
free(root);
}
五、完整代码示例
#include <stdio.h>
#include <stdlib.h>
// 二叉树节点结构体
typedef struct TreeNode {
int val;
struct TreeNode* left;
struct TreeNode* right;
} TreeNode;
// 创建新节点
TreeNode* createNode(int val) {
TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));
node->val = val;
node->left = NULL;
node->right = NULL;
return node;
}
// 构建示例二叉树
TreeNode* buildTree() {
TreeNode* root = createNode(1);
TreeNode* node2 = createNode(2);
TreeNode* node3 = createNode(3);
TreeNode* node4 = createNode(4);
TreeNode* node5 = createNode(5);
TreeNode* node6 = createNode(6);
root->left = node2;
root->right = node3;
node2->left = node4;
node2->right = node5;
node3->right = node6;
return root;
}
// 前序遍历
void preorderTraversal(TreeNode* root) {
if (root == NULL) return;
printf("%d ", root->val);
preorderTraversal(root->left);
preorderTraversal(root->right);
}
// 中序遍历
void inorderTraversal(TreeNode* root) {
if (root == NULL) return;
inorderTraversal(root->left);
printf("%d ", root->val);
inorderTraversal(root->right);
}
// 后序遍历
void postorderTraversal(TreeNode* root) {
if (root == NULL) return;
postorderTraversal(root->left);
postorderTraversal(root->right);
printf("%d ", root->val);
}
// 计算树的高度
int getHeight(TreeNode* root) {
if (root == NULL) return 0;
int leftHeight = getHeight(root->left);
int rightHeight = getHeight(root->right);
return (leftHeight > rightHeight ? leftHeight : rightHeight) + 1;
}
// 计算树的节点总数
int getNodeCount(TreeNode* root) {
if (root == NULL) return 0;
return getNodeCount(root->left) + getNodeCount(root->right) + 1;
}
// 释放树的内存
void freeTree(TreeNode* root) {
if (root == NULL) return;
freeTree(root->left);
freeTree(root->right);
free(root);
}
int main() {
// 构建二叉树
TreeNode* root = buildTree();
// 测试前序遍历
printf("前序遍历:");
preorderTraversal(root); // 输出:1 2 4 5 3 6
printf("\n");
// 测试中序遍历
printf("中序遍历:");
inorderTraversal(root); // 输出:4 2 5 1 3 6
printf("\n");
// 测试后序遍历
printf("后序遍历:");
postorderTraversal(root); // 输出:4 5 2 6 3 1
printf("\n");
// 测试树的高度
printf("树的高度:%d\n", getHeight(root)); // 输出:3
// 测试节点总数
printf("节点总数:%d\n", getNodeCount(root)); // 输出:6
// 释放内存
freeTree(root);
printf("内存已释放\n");
return 0;
}
六、树的实际应用场景
树在实际开发中应用非常广泛,常见场景包括:
- 文件系统:操作系统的目录结构就是一棵树,根目录是根节点,子目录和文件是子节点;
- DOM 树 :HTML 文档的结构就是一棵树,根节点是
<html>,子节点是各种标签; - 数据库索引:MySQL 的 InnoDB 引擎使用 B+ 树作为索引结构,提高查询效率;
- 路由表:路由器的路由表使用前缀树(Trie)存储路由信息;
- 表达式求值:算术表达式可以表示为一棵二叉树,通过后序遍历计算结果;
- 哈夫曼树:用于数据压缩,根据字符出现频率构建最优编码。
七、总结
树是一种一对多的层次数据结构,它通过节点和边组成层次关系,体现了递归特性。
最常用的树是二叉树,每个节点最多有两个子节点,支持前序、中序、后序三种遍历方式,以及计算高度、节点总数等基本操作。
在实际开发中,树的应用非常广泛,是算法和开发中不可或缺的基础数据结构。
希望这篇文章能帮助你深入理解树的原理和实现!