目录
一、树
树(Tree)是一种非线性的数据结构,由节点和边组成,节点之间的关系是一对多的关系。它类似于现实生活中的树,树的顶部被称为根(Root),而树的最底部节点被称为叶子节点(Leaf)。树的节点分为内部节点(Internal Node)和叶子节点两种类型。
树的节点可以包含任意数量的子节点,而每个子节点也可以有自己的子节点,从而形成了层级关系。一个节点的子节点被称为该节点的孩子(Children),而该节点是其每个子节点的父节点(Parent)。具有相同父节点的节点被称为兄弟(Siblings)。
1.1 树中的相关术语
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
节点的度:一个节点含有的子节点的个数称为该节点的度;
叶节点或终端节点:度为0的节点称为叶节点;
非终端节点或分支节点:度不为0的节点;
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
兄弟节点:具有相同父节点的节点互称为兄弟节点;
树的度:一棵树中,最大的节点的度称为树的度;
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次;
堂兄弟节点:双亲在同一层的节点互为堂兄弟;
节点的祖先:从根到该节点所经分支上的所有节点;
子孙:以某节点为根的子树中任一节点都称为该节点的子孙;
森林:由 m (m>=0) 棵互不相交的树的集合称为森林。
下面配合图例来进行说明:
1的孩子为2,8,反之2和8的父结点为1,9的父结点为8;
1结点的度为2,2结点的度为2,4结点的度为3,因为结点4的度最大,所以该树的度为3;
5、6、7、3、9的度为0,所以都是叶子结点;
3和4互为兄弟结点,同样的5、6、7也互为兄弟结点;4和9以及3和9都是堂兄弟关系;
结点5的祖先为4、2、1结点;
结点2的子孙为3、4、5、6、7;
从根开始定义,第一层,第二层..则图中树的高度为4;
结点2的高度为2。
二、二叉树
2.1 二叉树概念
二叉树是一种数据结构,由节点组成,每个节点最多有两个子节点:左子节点和右子节点。二叉树的节点通常包含一个值和指向其左右子节点的指针。这种结构可以用来表示层级关系,比如文件系统、组织结构等。
2.2 二叉树的特点
1. 每个节点最多有两个子节点:
在二叉树中,每个节点最多可以有两个子节点,分别称为左子节点和右子节点。这使得二叉树的结构相对简单,易于实现和理解。
2. 节点间的关系是有序的:
在二叉树中,左子节点通常小于或等于父节点,而右子节点通常大于父节点,这种关系对于二叉搜索树来说尤其重要。这种有序性质使得在二叉搜索树中进行搜索、插入和删除等操作更加高效。
3. 高度平衡性的重要性:
对于很多应用而言,保持二叉树的高度平衡是至关重要的。高度平衡的二叉树可以保证在最坏情况下的操作复杂度为 O(log n),其中 n 是树中节点的数量。这种性质对于提高算法效率至关重要。
4. 多种遍历方式:
二叉树的节点可以按照不同的顺序进行遍历,包括前序遍历、中序遍历、后序遍历和层序遍历等。每种遍历方式都有自己的特点和应用场景,可以方便地用于不同的问题求解。
5. 适用于多种应用场景:
二叉树在计算机科学中有着广泛的应用,比如在编程语言中的语法树、数据库中的索引结构、文件系统中的目录结构等。它的简单性和灵活性使得它成为了解决各种问题的重要工具。
总的来说,二叉树作为一种简单而强大的数据结构,在算法和数据结构领域扮演着重要角色,其特点包括简单的结构、有序的关系、高度平衡性和灵活的遍历方式,使得它适用于各种不同的应用场景。
2.3 二叉树定义与创建
#include <iostream>
// 定义二叉树节点结构体
struct TreeNode {
int val; // 节点值
TreeNode* left; // 左子节点指针
TreeNode* right; // 右子节点指针
// 构造函数
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
int main() {
// 创建二叉树节点示例
TreeNode* root = new TreeNode(1); // 创建根节点,节点值为 1
root->left = new TreeNode(2); // 创建左子节点,节点值为 2
root->right = new TreeNode(3); // 创建右子节点,节点值为 3
// 打印节点值
std::cout << "Root: " << root->val << std::endl;
std::cout << "Left Child: " << root->left->val << std::endl;
std::cout << "Right Child: " << root->right->val << std::endl;
// 释放内存,防止内存泄漏
delete root->left;
delete root->right;
delete root;
return 0;
}
创建的二叉树结构如下图所示:
2.4 二叉树的存储
二叉树的存储方式有多种,其中最常见的包括链式存储和顺序存储。
1. 链式存储:
- 链式存储是通过指针来表示树的结构,每个节点由一个结构体或类来表示,其中包含节点的值以及指向左右子节点的指针。
- 这种方式灵活性较高,可以方便地进行节点的插入、删除等操作。
- 链式存储适用于任意形状的树,包括不完全二叉树和非平衡树。
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面课程学到高阶数据结构如红黑树等会用到三叉链。
代码部分
// 二叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
}
// 三叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pParent; // 指向当前节点的双亲
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
};
2. **顺序存储**:
- 顺序存储是通过数组来表示树的结构,按照某种顺序将树的节点依次存储在数组中。
- 如果树是完全二叉树,可以利用数组的特性来表示节点之间的父子关系,比如父节点在数组中的下标 i,那么其左子节点在下标 2i+1,右子节点在下标 2i+2。
- 顺序存储节省了指针空间,但对于不完全二叉树需要使用一些特殊的方法来表示节点之间的关系。
选择合适的存储方式取决于具体的应用场景和需求。链式存储适用于树结构比较灵活的情况,而顺序存储适用于完全二叉树等形状规则的情况。在实际应用中,也可以根据情况将两种存储方式结合起来使用。
2.5 二叉树的类型
二叉树有多种类型,包括:
1. 二叉搜索树(Binary Search Tree, BST):一种特殊的二叉树,其中每个节点的左子树中的值都小于节点的值,而右子树中的值都大于节点的值。这种性质使得对BST进行搜索、插入和删除操作更加高效。
2. 平衡二叉树(Balanced Binary Tree):一种特殊的二叉树,其左右子树的高度差不超过1,这样可以确保在最坏情况下的操作复杂度为 O(log n),其中 n 是树中节点的数量。
3. 满二叉树(Full Binary Tree):每个节点要么没有子节点,要么有两个子节点的二叉树。满二叉树是一种特殊的完全二叉树。【表述2:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。】
4. 完全二叉树(Complete Binary Tree):除了最底层之外,其他层的节点都被完全填充,而且最底层的节点都尽量靠左排列。
5. 哈夫曼树(Huffman Tree):一种特殊的二叉树,用于数据压缩算法中。它的叶子节点包含数据,而非叶子节点包含权值,构建时会按照权值从小到大合并节点。
二叉树可以用递归或迭代方式进行遍历,包括前序遍历(Pre-order)、中序遍历(In-order)、后序遍历(Post-order)和层序遍历(Level-order)等。这些遍历方式在不同情况下都有自己的应用场景。
2.6 二叉树的性质
二叉树性质1:在二叉树的第i层上最多有2^(i-1)个结点(i≥1)。
第一层是根结点,只有一个,所以2^(1-1)=2^0=1。 第二层有两个,2^(2-1)=2^1=2。 第三层有四个,2^(3-1)=2^2=4。 第四层有八个,2^(4-1)=2^3=8。
二叉树性质2:深度为k的二叉树至多有(2^k)-1个结点(k≥1)。
注意这里一定要看清楚,是2^k后再减去1,而不是2^(k-1)。以前很多同学不能完全理解,这样去记忆,就容易把性质2与性质1给弄混淆了。 深度为k意思就是有k层的二叉树,我们先来看看简单的。 如果有一层,至多1=2^1-1个结点。 如果有二层,至多1+2=3=2^2-1个结点。 如果有三层,至多1+2+4=7=2^3-1个结点。 如果有四层,至多1+2+4+8=15=2^4-1个结点。
二叉树性质3:对任何一棵二叉树,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。
终端结点数其实就是叶子结点数,而一棵二叉树,除了叶子结点外,剩下的就是度为1或2的结点数了,我们设n1为度是1的结点数。则树T结点总数n=n0+n1+n2。
空链域指空指针数,每个叶子节点有两个空指针。
二叉树性质4:具有n个结点的完全二叉树的深度为(向下取整)。
二叉树性质5:如果对一棵有n个结点的完全二叉树(其深度为)的结点按层序编号(从第一层到第层,每层从左到右),对任一结点i(1<=i<=n),有
2.7 二叉树的遍历
二叉树的遍历方式是指按照一定顺序访问二叉树的所有节点,常见的遍历方式包括前序遍历、中序遍历、后序遍历和层序遍历。下面是这些遍历方式的详细解释:
1. 前序遍历(Pre-order traversal):
- 遍历顺序为:根节点 -> 左子树 -> 右子树。
- 具体操作顺序为:先访问当前节点,然后递归遍历左子树,最后递归遍历右子树。
2. 中序遍历(In-order traversal):
- 遍历顺序为:左子树 -> 根节点 -> 右子树。
- 具体操作顺序为:先递归遍历左子树,然后访问当前节点,最后递归遍历右子树。
- 对于二叉搜索树,中序遍历的结果是有序的。
3. 后序遍历(Post-order traversal):
- 遍历顺序为:左子树 -> 右子树 -> 根节点。
- 具体操作顺序为:先递归遍历左子树,然后递归遍历右子树,最后访问当前节点。
4. 层序遍历(Level-order traversal):
- 从根节点开始,按照从上到下、从左到右的顺序逐层遍历节点。
- 使用队列来实现,先将根节点入队,然后每次出队一个节点,将其左右子节点入队,直到队列为空。
这些遍历方式各有自己的特点和应用场景,可以根据具体的需求选择合适的遍历方式。例如,前序遍历常用于复制一棵树,中序遍历常用于搜索二叉树,后序遍历常用于计算表达式的值,而层序遍历常用于树的层级遍历等。
2.7.1 前序遍历
- 遍历顺序为:根节点 -> 左子树 -> 右子树。
前序遍历流程如下图所示:
前序遍历代码体现如下:
// 前序遍历
int Pre_traversal(TreeNode* T) {
if( T == NULL) // 如果二叉树为空,则返回0
return 0;
std::cout << T->val; // 遍历根节点
Pre_traversal(T->left); // 递归遍历左子树
Pre_traversal(T->right); // 递归遍历右子树
}
我们对一个整型二叉树进行前序遍历,二叉树如下图所示,该二叉树在2.3节构建。
分析遍历结果应为:12435
二叉树创建与前序遍历代码如下:
#include <iostream>
// 定义二叉树节点结构体
struct TreeNode {
int val; // 节点值
TreeNode* left; // 左子节点指针
TreeNode* right; // 右子节点指针
// 构造函数
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 前序遍历
int Pre_traversal(TreeNode* T) {
if( T == NULL) // 如果二叉树为空,则返回0
return 0;
std::cout << T->val; // 遍历根节点
Pre_traversal(T->left); // 递归遍历左子树
Pre_traversal(T->right); // 递归遍历右子树
}
int main() {
// 创建二叉树节点示例
TreeNode* root = new TreeNode(1); // 创建根节点,节点值为 1
root->left = new TreeNode(2); // 创建左子节点,节点值为 2
root->right = new TreeNode(3); // 创建右子节点,节点值为 3
root->left->left = new TreeNode(4);
root->right->left = new TreeNode(5);
// 前序遍历打印二叉树
Pre_traversal(root);
// 释放内存,防止内存泄漏
delete root->left->left;
delete root->left;
delete root->right;
delete root;
return 0;
}
结果如下:
结果与分析一致。
2.7.2 中序遍历
遍历顺序为:左子树 -> 根节点 -> 右子树。
第一次经过根节点时不访问,等遍历完左子树之后再访问,然后遍历右子树。
中序遍历的代码如下:
// 中序遍历
int In_traversal(TreeNode* T) {
if (T == NULL) // 如果二叉树为空,则返回0
return 0;
In_traversal(T->left); // 递归遍历左子树
std::cout << T->val; // 遍历根节点
In_traversal(T->right); // 递归遍历右子树
}
二叉树创建与中序遍历代码如下:
#include <iostream>
// 定义二叉树节点结构体
struct TreeNode {
int val; // 节点值
TreeNode* left; // 左子节点指针
TreeNode* right; // 右子节点指针
// 构造函数
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 中序遍历
int In_traversal(TreeNode* T) {
if (T == NULL) // 如果二叉树为空,则返回0
return 0;
In_traversal(T->left); // 递归遍历左子树
std::cout << T->val; // 遍历根节点
In_traversal(T->right); // 递归遍历右子树
}
int main() {
// 创建二叉树节点示例
TreeNode* root = new TreeNode(1); // 创建根节点,节点值为 1
root->left = new TreeNode(2); // 创建左子节点,节点值为 2
root->right = new TreeNode(3); // 创建右子节点,节点值为 3
root->left->left = new TreeNode(4);
root->right->left = new TreeNode(5);
// 中序遍历打印二叉树
In_traversal(root);
// 释放内存,防止内存泄漏
delete root->left->left;
delete root->left;
delete root->right;
delete root;
return 0;
}
结果如下:
2.7.3 后序遍历
- 遍历顺序为:左子树 -> 右子树 -> 根节点。
后序遍历代码如下:
// 后序遍历
int Post_traversal(TreeNode* T) {
if (T == NULL) // 如果二叉树为空,则返回0
return 0;
Post_traversal(T->left); // 递归遍历左子树
Post_traversal(T->right); // 递归遍历右子树
std::cout << T->val; // 遍历根节点
}
二叉树创建与后序遍历代码如下:
#include <iostream>
// 定义二叉树节点结构体
struct TreeNode {
int val; // 节点值
TreeNode* left; // 左子节点指针
TreeNode* right; // 右子节点指针
// 构造函数
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 后序遍历
int Post_traversal(TreeNode* T) {
if (T == NULL) // 如果二叉树为空,则返回0
return 0;
Post_traversal(T->left); // 递归遍历左子树
Post_traversal(T->right); // 递归遍历右子树
std::cout << T->val; // 遍历根节点
}
int main() {
// 创建二叉树节点示例
TreeNode* root = new TreeNode(1); // 创建根节点,节点值为 1
root->left = new TreeNode(2); // 创建左子节点,节点值为 2
root->right = new TreeNode(3); // 创建右子节点,节点值为 3
root->left->left = new TreeNode(4);
root->right->left = new TreeNode(5);
// 后序遍历打印二叉树
Post_traversal(root);
// 释放内存,防止内存泄漏
delete root->left->left;
delete root->left;
delete root->right;
delete root;
return 0;
}
2.7.4 层序遍历
从根节点开始,按照从上到下、从左到右的顺序逐层遍历节点。
图解如下:
层序遍历,需要借助队列数据结构,先把根节点入队列,依次出队列,每次出一个数据,就带节点的孩子入队列(先入左子节点,后入右子节点),直到全部节点出过队列,队列为空,循坏结束。
代码如下:
#include <iostream>
#include <queue>
// 定义二叉树节点结构体
struct TreeNode {
int val; // 节点值
TreeNode* left; // 左子节点指针
TreeNode* right; // 右子节点指针
// 构造函数
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 层序遍历
// 层序遍历函数
void levelOrderTraversal(TreeNode* root) {
if (root == nullptr) return;
// 创建队列,用于存放待访问的节点
std::queue<TreeNode*> q;
// 将根节点入队
q.push(root);
// 开始层序遍历
while (!q.empty()) {
// 计算当前层的节点个数
int levelSize = q.size();
// 遍历当前层的所有节点
for (int i = 0; i < levelSize; ++i) {
// 取出队首节点
TreeNode* node = q.front();
q.pop();
// 输出节点值
std::cout << node->val << " ";
// 将当前节点的左右子节点入队
if (node->left != nullptr) {
q.push(node->left);
}
if (node->right != nullptr) {
q.push(node->right);
}
}
}
}
int main() {
// 创建二叉树节点示例
TreeNode* root = new TreeNode(1); // 创建根节点,节点值为 1
root->left = new TreeNode(2); // 创建左子节点,节点值为 2
root->right = new TreeNode(3); // 创建右子节点,节点值为 3
root->left->left = new TreeNode(4);
root->right->left = new TreeNode(5);
// 层序遍历打印二叉树
levelOrderTraversal(root);
// 释放内存,防止内存泄漏
delete root->left->left;
delete root->left;
delete root->right;
delete root;
return 0;
}
结果如下:
2.8 根据遍历反推二叉树
2.8.1 根据前序+中序遍历反推二叉树
通过遍历结果来反推二叉树的形态,最重要的是理解每种遍历之间和二叉树的联系。例如:我们可以从前序遍历得到根节点,其第一个就是根结点,根结点在其中序遍历的序列中,把该位置左右分为了根结点的左右子树,然后我们就可以将整个中序序列划分为三大部分了。然后我们又对其每一个小部分进行再次的判断,又能得到左、右子树的根和对应的左右子树...一直细化三部分后就能得到最终结果了。
2.8.2 根据后序+中序遍历反推二叉树
2.8.3 根据层序+中序遍历反推二叉树
2.8.4 前序、后序、层序序列两两组合
引用自该博客:《数据结构C语言版》——二叉树详解(图文并茂)_c语言二叉树-CSDN博客
参考自: