文章目录
1. 概念
树(Tree)是一种重要的数据结构,是一种非线性数据结构,用于表示具有层次关系的数据它由 n (n≥0) 个节点的有限集合 T 组成,并满足以下两个条件:
- 有且仅有一个特定的称为根(Root)的节点。
- 其余的节点可以分为 m (m≥0) 个互不相交的有限集合 T1, T2, ..., Tm,其中每一个集合又是一棵树,称为其根的子树(Subtree)。
树的基本术语
- 根节点(Root):树中唯一没有父节点的节点。
- 节点的度(Degree):一个节点的子树的个数称为该节点的度数,一棵树的度数是指该树中节点的最大度数。
- 叶节点或终端节点(Leaf or Terminal Node):度数为零的节点。
- 分支节点(Branch Node):度数不为零的节点。
- 内部节点(Internal Node):除根节点外的分支节点。
- 父节点(Parent Node):一个节点的子树之根节点称为该节点的子节点,该节点称为它们的父节点。
- 兄弟节点(Sibling Node):同一父节点的各个子节点之间称为兄弟节点。
- 子节点(Child Node):父节点的直接后裔。
- 路径(Path):从根节点到某一节点之间经过的节点序列。
- 树的层次(Level):根节点为第 1 层,其子节点为第 2 层,以此类推。
- 树的高度或深度(Height or Depth):树中节点的最大层次。
- 子树(Subtree):每个节点可以作为根节点的子树。
树的特性
- 有穷性:树结构是由有限个节点组成的。
- 层次关系:树中的节点有明确的层次关系。
- 递归特性:树的定义是递归的,每个子树也是一棵树。
树的应用场景
- 文件系统:操作系统中的文件系统通常使用树结构表示目录和文件之间的层次关系。
- 表达式解析:编译器使用树结构来解析和表示程序中的表达式。
- 数据库索引:许多数据库管理系统使用树结构来组织和快速查找数据。
2. 分类
路径和边数
- 路径:在树中,一个节点序列 k1,k2,…,ki,ki+1,…,kj,并满足 kik_iki 是 ki+1 的父节点,称为从 k1 到 kj 的路径。路径的长度为 j−1,即路径中的边数。路径中的前面的节点是后面节点的祖先,后面的节点是前面节点的子孙。
节点的层数和树的高度
- 层数:节点的层数等于其父节点的层数加一。根节点的层数定义为一。树中节点层数的最大值称为该树的高度或深度。
有序树和无序树
- 有序树:若树中每个节点的各个子树的排列次序从左到右,不能交换,即兄弟之间是有序的,则该树称为有序树。一般的树是有序树。
森林
- 森林:m (m≥0) 棵互不相交的树的集合称为森林。树去掉根节点就成为森林,森林加上一个新的根节点就成为树。
3. 逻辑结构
树的逻辑结构描述了树中节点之间的关系。以下是详细解释:
-
子节点和父节点:
- 树中任何节点都可以有零个或多个直接后继节点,称为子节点(Child)。
- 任何节点最多只有一个直接前趋节点,称为父节点(Parent)。
- 根节点没有父节点,叶节点没有后继节点。
-
根节点:
- 树中唯一没有父节点的节点称为根节点(Root)。
- 根节点是树的起始点,是从树中任何一个节点到达其他所有节点的唯一通路的起点。
-
叶节点:
- 树中没有子节点的节点称为叶节点或终端节点(Leaf)。
- 叶节点是树的终点,没有任何后继节点。
举例
A
/ | \
B C D
/ \ / \
E F G H
-
根节点:A
- 根节点A没有父节点,它是整个树的起点。
-
父节点与子节点:
- A的子节点是B、C、D。
- B的子节点是E、F。
- D的子节点是G、H。
-
叶节点:
- E、F、G、H都是叶节点,因为它们没有子节点。
逻辑结构特性
-
唯一性:
- 每个节点最多只有一个父节点,这保证了树的层次结构是唯一的,没有循环。
-
层次性:
- 节点的层次由根节点出发逐层增加,根节点是第一层,它的子节点是第二层,依次类推。
-
无环性:
- 树结构中没有环路(循环),从一个节点出发沿着子节点路径不能回到该节点。
示例
4. 二叉树
二叉树的概念
二叉树(Binary Tree) 是每个节点最多有两个子节点的树结构。这两个子节点分别称为左子节点和右子节点。二叉树与普通树不同,二叉树的每个节点的子树分为左子树和右子树,即使只有一个子节点,也需要区分是左子节点还是右子节点。
- 二叉树是由 n(n≥0)个节点组成的有限集合。
- 二叉树是由一个根节点和两个互不相交的、分别称为左子树和右子树的二叉树组成。
- 二叉树严格区分左右子树,即使只有一个子节点也要区分左右。
二叉树的性质
-
二叉树第 i 层上的节点最多为 2^{i-1} 个。
- 这意味着,随着树的层数增加,每层的节点数量以指数形式增长。
-
深度为 k(k≥1)的二叉树最多有 2^k - 1 个节点。
- 这表明二叉树的总节点数随着树的深度增加以指数形式增长。
-
在任意一棵二叉树中,树叶的数目比度数为 2 的节点的数目多一。
- 这是一个有趣的性质,体现了二叉树的结构特征。
-
总节点数为各类节点之和:
- n=n0+n1+n2,其中 n0 是叶节点的数目,n1 是只有一个子节点的节点数目,n2 是有两个子节点的节点数目。
-
总节点数为所有子节点数加一:
- n=n1+2⋅n2+1,因为每个度数为2的节点有2个子节点,而度数为1的节点有1个子节点,根节点本身也算一个节点。
- 从而可以得出 n0=n2+1。
5. 完全二叉树和满二叉树
满二叉树
- 定义:深度为 k(k≥1)时有 2^k - 1 个节点的二叉树。
- 特点:每一层的节点数都达到最大值。
- 每一层都是满的,即每一层的节点数都是最大可能数。
- 第 k 层有 2^{k-1} 个节点,深度为 kkk 的满二叉树总节点数为 2^k - 1。
完全二叉树
- 定义:只有最下面两层有度数小于 2 的节点,并且最下面一层的叶节点集中在最左边的若干位置上的二叉树。
- 特点:完全二叉树除了最下面一层之外,其他层的节点数都达到最大值。
- 除了最后一层外,每一层都是满的。
- 最后一层的叶节点都尽可能地向左排列。
- 这意味着完全二叉树的结构是紧凑的,具有较好的平衡性。
完全二叉树的深度计算
- 公式:对于具有 n 个节点的完全二叉树,其深度为 [log2n]+1 或 log2(n+1)。
- 解释:
- [log2n]+1:取底数 2 的对数,向下取整后再加 1。
- log2(n+1):取底数 2 的对数,向上取整。
- 例如,对于一个有 15 个节点的完全二叉树:
- 使用公式 [log2 15]+1:[log2 15] = 3,加 1 后深度为 4。
- 使用公式 [log2 (15+1)]:log216=4,所以深度为 4。
6. 顺序存储结构
二叉树可以通过数组来进行存储,这种存储方式称为顺序存储结构。具体来说,对于二叉树中的每一个节点,我们可以将其映射到数组中的一个位置,从而实现树的顺序存储。
核心思想
-
补齐为完全二叉树:
- 如果二叉树不是完全二叉树,需要通过补充虚节点的方式将其变成完全二叉树。
- 补充后,每个非空节点的子节点都能正确地映射到数组中的位置。
-
顺序编号:
- 按照从上到下、从左到右的顺序给每个节点编号。
- 根节点编号为1,其左子节点编号为2,右子节点编号为3,以此类推。
-
数组存储:
- 编号为i的节点,其左子节点的编号为2i,右子节点的编号为2i+1。
- 将所有节点按编号顺序依次存储在数组中,对于不存在的节点,用特殊符号(如0或null)表示。
示例:
假设我们有以下二叉树:
a
/ \
b c
/ \ / \
d e f g
将其转化为完全二叉树:
a
/ \
b c
/ \ / \
d e f g
/ \ / \ / \
0 0 0 0 0 0
映射到数组中,编号从1开始:
index: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
value: a b c d e f g 0 0 0 0 0 0 0 0
具体存储关系
- 根节点
a
位于数组的第1位。 b
和c
分别是a
的左、右子节点,分别位于数组的第2、3位。d
和e
分别是b
的左、右子节点,分别位于数组的第4、5位。f
和g
分别是c
的左、右子节点,分别位于数组的第6、7位。
数组存储规则
- 任意节点i:
- 左子节点编号为2i。
- 右子节点编号为2i+1。
- 父节点编号为i/2(取整)。
如图所示:
- 左侧是一个二叉树。
- 右侧是该二叉树的顺序存储表示。
这种存储方式的优势在于,可以直接通过数组索引快速访问树的节点,适用于需要频繁随机访问节点的场景。通过顺序存储结构,可以有效地利用数组的连续存储特性,减少存储空间,同时便于节点的快速访问。但对于需要频繁插入、删除操作的二叉树,这种存储方式可能会不太灵活。
7. 链式存储结构
二叉树的链式存储结构是通过指针来表示节点之间的关系,每个节点包含其值以及指向其左子节点和右子节点的指针。这种存储结构适用于动态操作较多的二叉树,因为它可以方便地插入和删除节点。
核心思想
节点结构:
- 每个节点包含三个部分:
- 数据域:存储节点的值。
- 左指针域:存储指向左子节点的指针。
- 右指针域:存储指向右子节点的指针。
二叉树结构:
- 整个二叉树通过根节点的指针进行访问,从根节点开始可以遍历整个树。
示例:
假设我们有以下二叉树:
A
/ \
B C
/ \ / \
D E F G
链式存储结构表示为:
A
/ \
B C
/ \ / \
D E F G
对应的链式存储结构:
- 根节点A:
- 左指针指向节点B,右指针指向节点C。
- 节点B:
- 左指针指向节点D,右指针指向节点E。
- 节点C:
- 左指针指向节点F,右指针指向节点G。
- 节点D、E、F、G:
- 左右指针均为空,表示无子节点。
如图所示:
- 左侧是一个二叉树。
- 右侧是该二叉树的链式存储表示。
8. 二叉树的遍历
遍历的定义
遍历指的是沿某条搜索路径周游二叉树,对树中的每一个节点访问一次且仅访问一次。“遍历”是任何类型均有的操作,对于线性结构而言,由于每个节点均只有一个后继,因此不需要另加讨论。而二叉树是非线性结构,每个节点有两个后继,则存在在如何遍历即按什么样的搜索路径进行遍历的问题。
遍历的性质
由于二叉树的递归性质,遍历算法也是递归的。三种基本的遍历算法如下:
- 先上后下的按层次遍历;
- 先左(子树)后右(子树)的遍历;
- 先右(子树)后左(子树)的遍历。
这三种遍历方式分别对应不同的算法实现。
按层次遍历
按层次遍历是指从根节点开始,一层一层地从左到右访问每一个节点,直到遍历完整棵树。常见的实现方法是使用队列来辅助进行层次遍历。
先左后右遍历(前序、中序、后序)
先左后右的遍历分为前序遍历、中序遍历和后序遍历三种方式:
- 前序遍历(Preorder Traversal):先访问根节点,然后遍历左子树,最后遍历右子树。
- 中序遍历(Inorder Traversal):先遍历左子树,然后访问根节点,最后遍历右子树。
- 后序遍历(Postorder Traversal):先遍历左子树,然后遍历右子树,最后访问根节点。
先右后左遍历
先右后左的遍历类似于先左后右的遍历,只是访问的顺序相反。
9. 遍历分类
1. 前序遍历(Preorder Traversal)
在前序遍历中,遍历的顺序是:
- 访问根节点;
- 前序遍历左子树;
- 前序遍历右子树。
在图中的例子中,前序遍历的结果是:A B C D E F G H K。
- 先访问根节点A,然后遍历左子树B,访问B后,继续遍历B的左子树C,访问C后,遍历C的左子树D,访问D后,C没有右子树,返回B,B的右子树为空,返回A。
- 接下来,遍历A的右子树E,访问E后,遍历E的左子树,E没有左子树,接着遍历E的右子树F,访问F后,遍历F的左子树G,访问G后,遍历G的左子树H,访问H后,G的右子树K,访问K后,返回G,G的右子树为空,返回F,F的右子树为空,返回E。
void pre_order(btee_pnode *t) {
if (t != NULL) {
printf("%c ", t->data); // 访问根节点
pre_order(t->left); // 前序遍历左子树
pre_order(t->right); // 前序遍历右子树
}
}
2. 中序遍历(Inorder Traversal)
在中序遍历中,遍历的顺序是:
- 中序遍历左子树;
- 访问根节点;
- 中序遍历右子树。
在图中的例子中,中序遍历的结果是:B D C A E H G K F。
- 先遍历左子树B,B的左子树C,C的左子树D,访问D后返回C,C没有右子树,返回B,访问B后,B的右子树为空,返回A,访问A后,遍历A的右子树E。
- E的左子树为空,访问E后,遍历E的右子树F,F的左子树G,G的左子树H,访问H后返回G,G的右子树K,访问K后返回G,返回F,访问F后,F的右子树为空,返回E。
void mid_order(btee_pnode *t) {
if (t != NULL) {
mid_order(t->left); // 中序遍历左子树
printf("%c ", t->data); // 访问根节点
mid_order(t->right); // 中序遍历右子树
}
}
3. 后序遍历(Postorder Traversal)
在后序遍历中,遍历的顺序是:
- 后序遍历左子树;
- 后序遍历右子树;
- 访问根节点。
在图中的例子中,后序遍历的结果是:D C B H K G F E A。
- 先遍历左子树B,B的左子树C,C的左子树D,访问D后返回C,C没有右子树,返回B,B的右子树为空,返回A,A的左子树遍历完毕,接着遍历A的右子树E。
- E的左子树为空,遍历E的右子树F,F的左子树G,G的左子树H,访问H后返回G,G的右子树K,访问K后返回G,返回F,访问F后,F的右子树为空,返回E,访问E后,返回A,访问A。
void post_order(btee_pnode *t) {
if (t != NULL) {
post_order(t->left); // 后序遍历左子树
post_order(t->right); // 后序遍历右子树
printf("%c ", t->data); // 访问根节点
}
}
4. 层次遍历算法
层次遍历(广度优先遍历)的步骤是:
- 从根节点开始,按照层次依次访问每一个节点;
- 使用队列来辅助实现,根节点入队;
- 队列非空时,出队一个节点,访问该节点,然后将该节点的左子树和右子树分别入队。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 二叉树节点定义
typedef struct btee_node {
char data; // 节点数据
struct btee_node *left; // 左子节点
struct btee_node *right; // 右子节点
} btee_pnode;
// 队列节点定义
typedef struct queue_node {
btee_pnode *tree_node; // 队列中的二叉树节点
struct queue_node *next; // 指向下一个队列节点
} queue_node;
// 队列结构定义
typedef struct {
queue_node *front; // 队列头指针
queue_node *rear; // 队列尾指针
} queue;
// 初始化队列
void init_queue(queue *q) {
q->front = q->rear = NULL; // 初始化队列头和尾指针为空
}
// 判断队列是否为空
bool is_empty(queue *q) {
return q->front == NULL; // 如果队列头指针为空,队列为空
}
// 入队操作
void enqueue(queue *q, btee_pnode *t) {
queue_node *new_node = (queue_node *)malloc(sizeof(queue_node)); // 分配新队列节点内存
new_node->tree_node = t; // 将二叉树节点赋值给新队列节点
new_node->next = NULL; // 新队列节点的下一个节点设为NULL
if (is_empty(q)) { // 如果队列为空
q->front = q->rear = new_node; // 新节点同时是队列头和队列尾
} else {
q->rear->next = new_node; // 将新节点链接到队列尾
q->rear = new_node; // 更新队列尾指针
}
}
// 出队操作
btee_pnode *dequeue(queue *q) {
if (is_empty(q)) { // 如果队列为空
return NULL; // 返回NULL
} else {
queue_node *temp = q->front; // 暂存队列头节点
btee_pnode *result = temp->tree_node; // 取出队列头节点中的二叉树节点
q->front = q->front->next; // 更新队列头指针
if (q->front == NULL) { // 如果出队后队列为空
q->rear = NULL; // 更新队列尾指针为空
}
free(temp); // 释放暂存的队列头节点内存
return result; // 返回取出的二叉树节点
}
}
// 层次遍历二叉树
void level_order(btee_pnode *t) {
queue q; // 定义一个队列
init_queue(&q); // 初始化队列
enqueue(&q, t); // 将根节点入队
while (!is_empty(&q)) { // 当队列不为空时
btee_pnode *current = dequeue(&q); // 出队一个节点
printf("%c ", current->data); // 访问节点数据
if (current->left != NULL) { // 如果左子节点不为空
enqueue(&q, current->left); // 将左子节点入队
}
if (current->right != NULL) { // 如果右子节点不为空
enqueue(&q, current->right); // 将右子节点入队
}
}
}
10. 题目示例
已知一棵二叉树的前序遍历的结果是ABECDFGHIJ, 中序遍历的结果是EBCDAFHIGJ, 画出这棵二叉树,并给出这棵二叉树的后序遍历序列。
-
理解前序和中序遍历:
- 前序遍历序列:根 -> 左子树 -> 右子树
- 中序遍历序列:左子树 -> 根 -> 右子树
-
构建二叉树:
- 使用前序遍历的第一个节点作为根节点。
- 在中序遍历中找到根节点位置,其左边为左子树,其右边为右子树。
- 递归构建左子树和右子树。
-
进行后序遍历:
- 左子树 -> 右子树 -> 根
实现
- 前序遍历:
ABECDFGHIJ
- 中序遍历:
EBCDAFHIGJ
1. 构建二叉树
- 根节点是前序遍历的第一个元素
A
- 在中序遍历中,
A
的位置在第 4 位,将中序遍历分成两部分:EBCD
和FHIGJ
左子树
- 前序遍历:
BECD
- 中序遍历:
EBCD
右子树
- 前序遍历:
FGHIJ
- 中序遍历:
FHIGJ
重复上述步骤,递归构建左右子树:
- 左子树根节点是
B
,在中序遍历中的位置是第 2 位,分成E
和CD
:- 左子树:前序遍历:
E
,中序遍历:E
- 右子树:前序遍历:
CD
,中序遍历:CD
- 左子树:前序遍历:
- 右子树根节点是
F
,在中序遍历中的位置是第 1 位,分成空和HIGJ
:- 右子树:前序遍历:
GHIJ
,中序遍历:HIGJ
- 右子树:前序遍历:
依次类推,构建整个二叉树。
A
/ \
B F
/ \ \
E C G
/ \
D H
\
I
\
J
#include <stdio.h>
#include <stdlib.h>
// 定义二叉树节点
typedef struct TreeNode {
char data;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
// 根据前序和中序遍历构建二叉树
TreeNode* buildTree(char* preorder, int preStart, int preEnd, char* inorder, int inStart, int inEnd) {
if (preStart > preEnd) return NULL;
char rootValue = preorder[preStart];
TreeNode* root = (TreeNode*)malloc(sizeof(TreeNode));
root->data = rootValue;
root->left = root->right = NULL;
int rootIndex = inStart;
while (inorder[rootIndex] != rootValue) rootIndex++;
int leftSize = rootIndex - inStart;
root->left = buildTree(preorder, preStart + 1, preStart + leftSize, inorder, inStart, rootIndex - 1);
root->right = buildTree(preorder, preStart + leftSize + 1, preEnd, inorder, rootIndex + 1, inEnd);
return root;
}
// 后序遍历
void postOrder(TreeNode* root) {
if (root == NULL) return;
postOrder(root->left);
postOrder(root->right);
printf("%c", root->data);
}
int main() {
char preorder[] = "ABECDFGHIJ";
char inorder[] = "EBCDAFHIGJ";
int length = sizeof(preorder) / sizeof(preorder[0]) - 1; // 减去一个 '\0' 的长度
TreeNode* root = buildTree(preorder, 0, length - 1, inorder, 0, length - 1);
printf("后序遍历序列:");
postOrder(root);
printf("\n");
return 0;
}
运行以上代码,构建二叉树并进行后序遍历,可以得到二叉树的后序遍历序列:EDCBIHJGFA
。
销毁二叉树
#include <stdio.h>
#include <stdlib.h>
// 二叉树节点定义
typedef struct btree_node {
char data;
struct btree_node *left;
struct btree_node *right;
} btree_node;
// 创建新节点
btree_node* create_node(char data) {
btree_node *new_node = (btree_node*)malloc(sizeof(btree_node));
new_node->data = data;
new_node->left = new_node->right = NULL;
return new_node;
}
// 销毁二叉树
void destroy_tree(btree_node *root) {
if (root != NULL) {
destroy_tree(root->left); // 递归销毁左子树
destroy_tree(root->right); // 递归销毁右子树
printf("Deleting node with data: %c\n", root->data);
free(root); // 释放当前节点的内存
}
}
int main() {
// 创建一个简单的二叉树
btree_node *root = create_node('A');
root->left = create_node('B');
root->right = create_node('C');
root->left->left = create_node('D');
root->left->right = create_node('E');
root->right->left = create_node('F');
root->right->right = create_node('G');
// 销毁二叉树
destroy_tree(root);
root = NULL; // 避免悬空指针
return 0;
}
- 节点定义:定义了一个
btree_node
结构体表示二叉树的节点。 - 创建节点:
create_node
函数用于创建一个新节点并初始化其数据。 - 销毁树:
destroy_tree
函数通过后序遍历递归地销毁二叉树的所有节点。对于每个节点,先销毁其左子树,再销毁其右子树,最后释放节点本身的内存。 - 主函数:
main
函数中创建了一个简单的二叉树,并在使用完成后调用destroy_tree
函数销毁它。
通过这种方式可以确保所有节点的内存都被正确释放,防止内存泄漏。