这一版为初稿,将大体二叉树写出来,后续会配图以及补充一些代码和细节
从零开始学数据结构与算法(四) :二叉树
一、 树的逻辑结构
1.树的定义和基本术语
1.1 树中的一些专业术语
- 节点(Node):树结构中的==基本单元==。每个节点可以包含一个值或数据,并且可以连接到其他节点。
- 根节点(Root Node):==树的顶部节点,它是树的起点,没有父节点与之相连。==
- 叶节点(Leaf Node):位于树的末端的节点,它==没有子节点==。
- 父节点(Parent Node)[^1]:==一个节点连接到它的直接下级节点的节点。==
- 子节点(Child Node):==一个节点连接到它的直接上级节点的节点。==
- 兄弟节点(Sibling Node):具有==相同父节点的节点==被称为兄弟节点。
- 祖先(Ancestor):对于树中的一个节点,它的祖先是指==从根节点到该节点的路径上的所有节点==,不包括该节点本身。
- 子孙(Descendant):对于树中的一个节点,它的子孙是==指以该节点为根的子树中的所有节点==。
- 度(Degree):节点的度是指==该节点拥有的子节点数量==,也称为子树的个数。节点的度为0表示它是叶节点,度大于0表示它是一个分支节点。
- 树的度(Degree of a Tree):树的度定义为==树中节点的最大度数==。例如,二叉树的度为2,每个节点最多有两个子节点。
- 路径(Path):路径是指==从一个节点到另一个节点的节点序列==。路径由边连接起来,并且沿着这些边可以从一个节点到达另一个节点。
- 路径长度(Path Length):路径的长度是指==路径上边的数量==。例如,如果一条路径由3条边连接,则路径长度为3。
- 深度(Depth):==一个节点与根节点之间的边的数量==。根节点的深度为0。
- 高度(Height):==树中最深节点的深度,也就是树的最长路径的长度。==
- 子树(Subtree):==树中的一个节点及其所有后代节点组成的树==。
- 二叉树(Binary Tree):每个节点最多有两个子节点的树结构。
1.2 树的定义
在计算机科学中,树(Tree)是一种非线性的数据结构,它是n(n>=0)个结点的有限集合。当 n=0 时,称为空树。任意一棵非空树满足以下条件:
1. 有且只有一个根节点。
2. 当n > 1 时,除根结点外的其余节点被分成m (m > 0) 个互不相交的有限集合,其中每个集合又是一棵树。
2.树的抽象数据类型定义
树的应用很广泛,在不同的场景当中,树的基本操作不尽相同。下面给出一个树的抽象数据类型定义:
ADT Tree
DataMode1
树是由一个根节点和若干棵子树构成,树中结点具有层次关系
Operation
InitTree
输入:无
功能:初始化一棵树
输出:一棵空树
DestroyTree
输入:无
功能:销毁一棵树
输出:无
PreOrder
输入:无
功能:前序遍历树
输出:树的前序遍历序列
PostOrder
输入:无
功能:后序遍历树
输出:树的后序遍历
LeverOrder
输入:无
功能:层序遍历树
输出:树的层序遍历序列
endADT
3.树的遍历操作
二、树的存储结构
1.双亲表示法
双亲表示法(Parent Representation)是一种用于表示树结构的方法,它通过在每个节点中存储其父节点的索引或位置来表示树中节点之间的关系。
在双亲表示法中,每个节点包含两个信息:
-
数据:节点所存储的值或数据。
-
双亲指针(Parent Pointer):指向父节点的指针或索引。该指针指示该节点的父节点在存储结构中的位置信息。
使用双亲表示法,可以通过节点之间的父子关系轻松导航树结构。根节点的父指针通常设置为特殊值(例如-1或空),表示根节点没有父节点。
以下是一个示例,展示了一个使用双亲表示法表示的树结构:
节点索引: 0 1 2 3 4 5 6 7 8 9
节点数据: A B C D E F G H I J
父节点索引: -1 0 0 1 1 2 2 6 6 6
在上述示例中,节点的索引从0到9,每个节点存储了相应的数据。父节点索引列显示了每个节点的父节点的索引。根节点的父节点索引为-1,表示它是树的根节点。
使用双亲表示法,可以方便地根据父节点索引找到节点的父节点,并以此遍历树结构。然而,需要注意的是,双亲表示法在进行插入和删除操作时可能需要重新调整节点的父指针,以保持正确的关系。
下面给出双亲表示法的存储结构定义:
#define MaxSize 100
typedef char DataType;
typedef struct
{
DataType data; //节点的数据信息
int parent; //双亲在数组中的下标
} PNode;
typedef struct
{
PNode tree[MaxSize];
int treeNum; //数的结点个数
} PTree;
2.孩子表示法
孩子表示法(Child Representation)是一种用于表示树结构的方法,它通过在每个节点中存储其子节点的引用或指针来表示节点之间的关系。
在孩子表示法中,每个节点包含两个信息:
-
数据:节点所存储的值或数据。
-
子节点指针(Child Pointers):指向子节点的指针或引用。这些指针指示该节点的子节点在存储结构中的位置信息。
使用孩子表示法,可以轻松地遍历树的节点和子节点之间的关系。每个节点可以有一个或多个子节点,通过子节点指针,可以直接访问节点的子节点。
以下是一个示例,展示了一个使用孩子表示法表示的树结构:
节点数据: A B C D E F G H I J
子节点指针: B C D F G H I J - -
在上述示例中,节点数据列显示了每个节点存储的数据。子节点指针列显示了每个节点的子节点的位置信息。例如,节点A的子节点是节点B和节点C。
使用孩子表示法,可以轻松地遍历树结构,从根节点开始,通过子节点指针访问每个节点的子节点。然而,需要注意的是,孩子表示法可能需要处理节点的变长子节点指针,或者使用额外的数据结构(如数组)来存储子节点指针,以便有效地表示树结构。
下面给出孩子表示法的存储结构定义:
#define MaxSize 100
typedef char DataType;
typedef struct ChildNode
{
int child;
struct ChildNode * next;
} ChildNode;
typedef struct
{
DataType data;
ChildNode * first;
} TreeNode;
typedef struct
{
TreeNode tree[Maxsize];
int treeNum; //定义孩子表示法存储结构
} CTree;
3.孩子兄弟表示法
孩子兄弟表示法(Child-Sibling Representation),也称为二叉树表示法或二叉链表表示法,是一种用于表示树结构的方法,它利用节点的孩子指针和兄弟指针来表示节点之间的关系。
在孩子兄弟表示法中,每个节点包含三个信息:
-
数据:节点所存储的值或数据。
-
第一个孩子指针(First Child Pointer):指向该节点的第一个孩子节点。
-
下一个兄弟指针(Next Sibling Pointer):指向该节点的下一个兄弟节点。
使用孩子兄弟表示法,可以用二叉树的结构来表示一般的树结构。每个节点的第一个孩子指针指向其第一个子节点,而下一个兄弟指针指向其下一个兄弟节点。
以下是一个示例,展示了一个使用孩子兄弟表示法表示的树结构:
节点数据: A B C D E F G H I J
第一个孩子指针: B - D - G - - - - -
下一个兄弟指针: C - E F - - - - - -
在上述示例中,节点数据列显示了每个节点存储的数据。第一个孩子指针列显示了每个节点的第一个孩子节点的位置信息。下一个兄弟指针列显示了每个节点的下一个兄弟节点的位置信息。
使用孩子兄弟表示法,可以轻松地遍历树结构,从根节点开始,通过第一个孩子指针和下一个兄弟指针访问每个节点的子节点和兄弟节点。这种表示方法节省了存储空间,并且在某些操作(如树的遍历和查找)上具有较好的性能。
孩子兄弟表示法常用于表示二叉树和森林(由多个不相交的树组成),尤其适用于树的动态操作和算法实现。
需要注意的是,孩子兄弟表示法在进行插入和删除操作时可能需要调整节点的指针,以维持正确的关系,并确保树的结构仍然有效。
孩子兄弟表示法是树结构的一种常用表示方法,但根据具体的应用需求和操作需求,选择适合的树表示方法是很重要的。
下面给出孩子兄弟表示法的存储结构定义:
typedef char DataType;
typedef struct CSNode
{
DataType data;
struct CSNode * firstchild, * rightsib;
} CSNode;
CsNode *root; // 定义根指针
三、二叉树的逻辑结构
1.二叉树的定义
1.1 二叉树
二叉树(Binary Tree)是一种特殊的树结构,其中每个节点最多有两个子节点,分别称为左子节点和右子节点。
二叉树的定义如下:
-
空树(Empty Tree):一个==没有节点==的二叉树被称为空树。
-
根节点(Root Node):==二叉树中的一个特殊节点被称为根节点,它是二叉树的顶部节点,没有父节点与之相连。==
-
节点(Node):二叉树中的每个节点包含一个值或数据,并且==可以连接到最多两个子节点==。每个节点可以有零个、一个或两个子节点。
-
左子节点(Left Child Node):一个节点==连接到它的左侧子节点==的节点被称为左子节点。
-
右子节点(Right Child Node):一个节点==连接到它的右侧子节点==的节点被称为右子节点。
-
叶节点(Leaf Node):叶节点是没有子节点的节点,它们位于二叉树的末端。
需要注意的是,二叉树中的节点可以有一个子节点为空(缺失),但不能有两个子节点都为空的情况。
二叉树的特点是每个节点最多有两个子节点,其中左子节点和右子节点的顺序是有意义的,不可以随意的颠倒。二叉树的结构可用于模拟层次关系、排序和搜索等应用。
1.2 斜树
斜树(Skewed Tree)是一种特殊类型的二叉树,==它的所有节点都只有一个子节点,要么都是左子节点,要么都是右子节点。==
根据子节点的位置,斜树可以分为两种类型:
- 左斜树(Left Skewed Tree):所有节点都只有左子节点,没有右子节点。每个节点的右子节点为空。
1
/
2
/
3
/
4
- 右斜树(Right Skewed Tree):所有节点都只有右子节点,没有左子节点。每个节点的左子节点为空。
1
\
2
\
3
\
4
斜树是二叉树的一种特殊情况,因为它的所有节点只有一个子节点,所以斜树的高度等于节点的数量。斜树的形状类似于一个单向链表,只有一个方向上的连接。
斜树在某些特定场景下可能会出现,但在一般情况下并不常见。它的特殊性使得斜树的插入和删除操作相对简单,但访问和搜索效率较低,因为树的平衡性被破坏了。
需要注意的是,斜树是二叉树的一种特殊情况,与其他类型的二叉树(如平衡二叉树、完全二叉树等)相比,斜树的应用范围较为有限。在大多数情况下,我们更倾向于使用平衡性更好的二叉树结构。
1.3 满二叉树
==满二叉树(Full Binary Tree)是一种特殊类型的二叉树,其中除了叶节点外,每个节点都有两个子节点,并且所有叶节点都在同一层级上。==
满二叉树的定义如下:
-
空树(Empty Tree):一个没有节点的二叉树被称为空树。
-
根节点(Root Node):二叉树中的一个特殊节点被称为根节点,它是二叉树的顶部节点,没有父节点与之相连。
-
节点(Node):二叉树中的每个节点包含一个值或数据,并且可以连接到最多两个子节点。每个节点可以有零个、一个或两个子节点。
-
叶节点(Leaf Node):叶节点是没有子节点的节点,它们位于二叉树的末端。
-
每个非叶节点都有两个子节点:除了叶节点外,满二叉树的每个非叶节点都有两个子节点。
-
所有叶节点在同一层级上:满二叉树的所有叶节点都位于同一层级上,也就是说从根节点到叶节点的路径长度是相同的。
下图是一个示例,展示了一个满二叉树的结构:
A
/ \
B C
/ \ / \
D E F G
在上述示例中,每个非叶节点都有两个子节点,而且所有叶节点(D、E、F、G)都在同一层级上。
满二叉树具有良好的平衡性和规律性,它的高度(从根节点到最深层的叶节点的路径长度)为log2(n+1),其中n是满二叉树中的节点数量。满二叉树的节点数量为2^h - 1,其中h是满二叉树的高度。
满二叉树在一些算法和数据结构中具有重要的应用,例如堆排序、完全二叉树的表示等。由于其特定的性质,满二叉树的插入和删除操作相对复杂,但在某些场景下,满二叉树能够提供更高效的存储和访问方式。
1.4 完全二叉树
完全二叉树(Complete Binary Tree)是一种特殊类型的二叉树,==除了最后一层外,所有层的节点都被填满,且最后一层的节点都尽可能地靠左排列。==
完全二叉树的定义如下:
-
空树(Empty Tree):一个没有节点的二叉树被称为空树。
-
根节点(Root Node):二叉树中的一个特殊节点被称为根节点,它是二叉树的顶部节点,没有父节点与之相连。
-
节点(Node):二叉树中的每个节点包含一个值或数据,并且可以连接到最多两个子节点。每个节点可以有零个、一个或两个子节点。
-
最后一层节点靠左排列:在最后一层的节点中,从左到右依次排列,不留有空缺。
-
最后一层之前的层都是满的:除了最后一层外,所有的层都被填满,每一层的节点数都达到最大。
下图是一个示例,展示了一个完全二叉树的结构:
A
/ \
B C
/ \ /
D E F
在上述示例中,除了最后一层的节点(F)没有右子节点外,其他层的节点都被填满,且最后一层的节点都尽可能地靠左排列。
完全二叉树具有一些特性:由于它的结构相对规整,因此可以使用数组或顺序存储来表示,同时也便于使用索引进行节点的访问和操作。完全二叉树在一些算法和数据结构中具有重要的应用,例如优先队列(使用堆实现)和哈夫曼树等。
需要注意的是,完全二叉树不一定是满二叉树,因为最后一层可以不是满的。而满二叉树的定义是除了叶节点外,每个节点都有两个子节点,并且所有叶节点在同一层级上。
2.二叉树的基本性质
2.1 性质1
==在一棵二叉树中,如果叶子节点的个数为n0,度为2的结点个数为n2,则n0=n2+1。==
2.2 性质2
==二叉树的第 i 层最多有 2i−1个结点(i>=1)。==
2.3 性质3
==在一棵深度为 k 的二叉树中,最多有2k−1 个结点。==
2.4 性质4
==具有 n 个结点的完全二叉树的深度为 ⌊log2n⌋+1。==
2.5 性质5
对一棵具有n个结点的完全二叉树从1开始按层序编号,则对于编号为i(1<=i<=n)的结点i,有:
==1. 如果 i>1,则结点i的父节点编号为 ⌊i/2⌋ ;否则结点 i 是根结点, 无双亲。
2. 如果 2i<=n,则结点的左孩子编号为 2i ;否则结点无左孩子。
== 3. 如果 2i<=n+1,则结点的右孩子编号为 2i+1 ;否则结点无右孩子。==
3.二叉树的抽象数据类型定义
和树一样,在不同的情况下,二叉树的基本操作不尽相同。下面给出一些二叉树抽象数据类型定义的例子,包含了部分基本操作:
ADT BiTree
DataModel
二叉树由一个根节点和两棵互不相交的左右子树构成,二叉树中的结点具有层次关系
Operation
InitBiTree
输入:无
功能:初始化一棵二叉树
输出:一个空的二叉树
CreatBiTree
输入:n 个结点的数据信息
功能:建立一棵二叉树
输出:含有n个结点的二叉树
DestoryBiTree
输入:无
功能:销毁一棵二叉树
输出:释放二叉树所占的内存空间
PreOrder
输入:无
功能:前序遍历二叉树
输出:二叉树的前序遍历序列
InOrder
输入:无
功能:中序遍历二叉树
输出:二叉树的中序遍历序列
PostOrder
输入:无
功能:后序遍历二叉树
输出:二叉树的后序遍历序列
LeverOrder
输入:无
功能:层序遍历二叉树
输出:二叉树的层序遍历序列
endADT
需要注意的是,二叉树的实现方式可以有多种,如链式存储(使用节点对象和引用)或数组存储(使用数组来表示节点及其关系),具体的实现方式可以根据需求和语言特性进行选择。以上定义给出了二叉树的操作接口,在具体实现时可以根据需要进行适当的调整。
4.二叉树的遍历操作
二叉树的遍历是指按照一定的顺序访问二叉树的所有节点。常见的二叉树遍历方式有三种:先序遍历、中序遍历和后序遍历。这些遍历方式可以通过递归或迭代的方式实现。
以下是关于二叉树遍历的一些常见操作:
先序遍历(Preorder Traversal):
==先序遍历的顺序是:根节点 -> 左子树 -> 右子树。==
递归实现:
1. 如果树为空,则返回。
2. 访问当前节点的值。
3. 递归地对左子树进行先序遍历。
4. 递归地对右子树进行先序遍历。
迭代实现:
1. 创建一个空栈,并将根节点压入栈。
2. 当栈不为空时,执行以下步骤:
- 弹出栈顶节点,访问该节点的值。
- 如果存在右子节点,则将右子节点压入栈。
- 如果存在左子节点,则将左子节点压入栈。
中序遍历(Inorder Traversal):
==中序遍历的顺序是:左子树 -> 根节点 -> 右子树。==
递归实现:
1. 如果树为空,则返回。
2. 递归地对左子树进行中序遍历。
3. 访问当前节点的值。
4. 递归地对右子树进行中序遍历。
迭代实现:
1. 创建一个空栈。
2. 初始化当前节点为根节点。
3. 当栈不为空或当前节点不为空时,执行以下步骤:
- 将当前节点及其所有左子节点依次压入栈,直到当前节点为空。
- 弹出栈顶节点,访问该节点的值。
- 将当前节点更新为弹出节点的右子节点。
后序遍历(Postorder Traversal):
==后序遍历的顺序是:左子树 -> 右子树 -> 根节点。==
递归实现:
1. 如果树为空,则返回。
2. 递归地对左子树进行后序遍历。
3. 递归地对右子树进行后序遍历。
4. 访问当前节点的值。
迭代实现:
1. 创建一个空栈。
2. 初始化当前节点为根节点。
3. 当栈不为空或当前节点不为空时,执行以下步骤:
- 将当前节点及其所有左子节点依次压入栈,直到当前节点为空。
- 如果栈顶节点的右子节点存在且未被访问过,则将当前节点更新为右子节点。
- 否则,弹出栈顶节点,访问该节点的值,并将当前节点置为空。
层序遍历(Level Order Traversal):
==按照层级顺序逐层访问二叉树节点的遍历方式。==
层序遍历的步骤如下:
- 创建一个空队列(可以使用数组或链表实现)。
- 将根节点入队。
- 当队列不为空时,执行以下步骤:
- 弹出队头节点,并访问该节点的值。
- 如果弹出的节点有左子节点,将左子节点入队。
- 如果弹出的节点有右子节点,将右子节点入队。
层序遍历会按照从上到下、从左到右的顺序逐层访问二叉树的节点。这意味着在同一层级上,左边的节点会在右边的节点之前被访问到。
下面是一个层序遍历的示例:
A
/ \
B C
/ \ /
D E F
层序遍历的结果为:A, B, C, D, E, F。
在实际实现中,可以使用队列来辅助层序遍历。首先将根节点入队,然后每次从队列中取出一个节点,访问该节点的值,并将其左右子节点(如果存在)依次入队。重复这个过程,直到队列为空,即可完成层序遍历。
层序遍历常用于广度优先搜索(BFS)等算法中,可以用于按层级遍历或搜索树的结构。
四、二叉树的存储结构
1.顺序存储结构
二叉树的顺序存储结构是一种使用数组来表示二叉树的方法。在顺序存储结构中,树的节点按照从上到下、从左到右的顺序依次存储在数组中,通过数组的索引来表示节点之间的关系。
假设二叉树的顺序存储结构使用一个一维数组 arr
来表示,其中根节点存储在索引为 0 的位置,对于任意节点存储在索引 i
的位置上,它的左子节点存储在索引 2i+1
的位置上,右子节点存储在索引 2i+2
的位置上。
下图是一个示例二叉树的顺序存储结构表示:
A
/ \
B C
/ \ / \
D E F G
对应的顺序存储结构数组为:['A', 'B', 'C', 'D', 'E', 'F', 'G']
。
在顺序存储结构中,可以通过数组的索引计算出节点之间的关系,例如,索引为 i
的节点的父节点索引为 (i-1)//2
,左子节点索引为 2i+1
,右子节点索引为 2i+2
。
顺序存储结构的优点是简单、易于实现和访问,不需要额外的指针和动态内存分配。然而,它对于非完全二叉树会浪费一定的空间,因为可能存在一些数组元素未被使用。
需要注意的是,顺序存储结构适用于静态的二叉树,即在构建树后不会改变其结构。如果需要频繁地进行插入、删除节点操作,顺序存储结构可能不是一个高效的选择,因为需要频繁地调整数组大小和移动元素位置。在这种情况下,链式存储结构(使用节点对象和引用)更为常用和灵活。
下面给出二叉树顺序存储结构的定义:
#define MaxSize 100 //假设二叉树的最大编号
typedef char DataType; //定义二叉树的数据类型,假设为char
typedef struct
{
DataType data[MaxSize];
int biTreeNum; //结点个数
} SeqBiTree;
2.二叉链表
2.1 二叉链表的存储结构定义
二叉链表是一种使用节点对象和引用来表示二叉树的存储结构。在二叉链表中,每个节点包含一个数据元素和两个指针,分别指向其左子节点和右子节点(如果存在)。
下面给出二叉链表的存储结构定义:
typedef char DataType;
typedef struct BiNode
{
DataType data;
struct BiNode * lchild, * rchild;
} BiNode;
2.2 二叉链表的实现
遍历操作是用递归写的,非递归的版本将放在七、拓展与提高当中
2.2.1 前序遍历
由二叉树前序遍历的操作定义,容易写出前序遍历的递归算法,代码如下:
void PreOrder(BiNode* root)
{
if (root == NULL) return; //递归结束条件
printf("%c", root->data); //访问根节点的数据域
PreOrder(root->lchild); //前序递归遍历root左子树
PreOrder(root->rchild); //前序递归遍历root右子树
}
2.2.2 中序遍历
由二叉树中序遍历的操作定义,容易写出中序遍历的递归算法,代码如下:
void InOrder(BiNode* root)
{
if (root == NULL) return; //递归结束条件
InOrder(root->lchild); //中序递归遍历root左子树
printf("%c", root->data); //访问根节点的数据域
InOrder(root->rchild); //中序递归遍历root右子树
}
2.2.3 后序遍历
由二叉树后序遍历的操作定义,容易写出后序遍历的递归算法,代码如下:
void PostOrder(BiNode* root)
{
if (root == NULL) return; //递归结束条件
PostOrder(root->lchild); //后序递归遍历root左子树
PostOrder(root->rchild); //后序递归遍历root右子树
printf("%c", root->data); //访问根节点的数据域
}
2.2.4 层序遍历
在进行层序遍历时,访问某一层的结点后,在对各个节点的左孩子和右孩子顺序访问,这样一层一层进行,先访问的节点其左右孩子也要先访问,这符合队列的操作特性。因此,在进行层序遍历时,设置一个队列存放已访问的结点,代码如下:
void LevelOrder(BiNode* root)
{
BiNode* q = NULL, * Q[QMaxSize]; //顺序队列
int front, rear; //队头队尾
front = rear = -1; //初始化为-1
if (root == NULL) return; //如果二叉树为空,函数结束
Q[++rear] = root; //非空根节点入队
while (front != rear)
{
q = Q[++front];
printf("%c", q->data);
if (q->lchild != NULL) Q[++rear] = q->lchild;
if (q->rchild != NULL) Q[++rear] = q->rchild;
}
}
2.2.5 二叉树的创建
先理清楚一个概念:
拓展二叉树: 将二叉树中每个节点的空指针指向一个虚结点,其值为一个特定值以表示其为空,把这样处理后的二叉树原二叉树的拓展二叉树。
我们知道,对于任意一组序列,无论是前序,中序,后序,还是层序,我们无论知道哪一个序列,大多数情况下都无法直接构造出正确的二叉树。
但我们构造时,使用拓展二叉树,就一定可以得到唯一确定的一棵二叉树。
这里以拓展二叉树的前序序列举例,代码如下:
BiNode* CreatBiTree(BiNode* root)
{
char ch;
scanf("%c", &ch); //输入结点信息
if (ch == '#') root = NULL; //递归结束
else
{
root = (BiNode*)malloc(sizeof BiNode); //生成新节点
root->data = ch; //新节点数据域
root->lchild = CreatBiTree(root->lchild); //递归建立左子树
root->rchild = CreatBiTree(root->rchild); //递归建立右子树
}
return root;
}
2.2.6 二叉树的销毁
二叉链表是动态分配内存,二叉链表的结点是在程序运行过程中动态申请的,在二叉树表变量退出作用域前,要释放二叉链表的存储空间。可以对二叉链表进行后序遍历,在访问结点时进行释放,代码如下:
void DestoryBiTree(BiNode* root)
{
if (root == NULL) return;
DestoryBiTree(root->lchild);
DestoryBiTree(root->rchild);
free(root);
}
3.三叉链表
三叉链表是一种链表数据结构的扩展形式,它允许每个节点同时链接到前一个节点、后一个节点以及一个额外的父节点。这使得三叉链表在某些场景下比传统的单向或双向链表更加灵活和方便。
三叉链表的节点通常由三个指针组成:prev
、next
和parent
。这些指针分别指向前一个节点、后一个节点和父节点。
通过这种结构,三叉链表可以实现以下功能:
-
正向和反向遍历:由于每个节点都有指向前一个和后一个节点的指针,你可以从任意节点开始遍历整个链表,无论是正向还是反向。
-
父节点链接:每个节点还有一个指针指向其父节点。这对于从某个节点向上导航至其父节点非常有用,尤其是在树形结构中。
-
多叉树表示:通过使用三叉链表,可以轻松地将多叉树表示为一个链表结构。每个节点的父节点指针可以指向其树中的父节点,而后续节点的指针可以指向其子节点。
尽管三叉链表提供了更多的灵活性和功能,但它也带来了更多的空间开销和复杂性。在实际应用中,需要根据具体情况权衡使用三叉链表的优势和劣势。
五、森林
1.森林的逻辑结构
在数据结构中,森林是由多个独立的树(树结构)组成的集合。每棵树都是由节点和它们之间的关联关系(通常是父子关系)构成的。
森林可以通过多种方式表示,其中两种常见的方法是:
-
使用多个根节点:每棵树都有一个根节点,根节点之间没有父子关系。这种表示方法适用于树的根节点集合已知的情况,每个根节点代表一棵树。
-
使用指针或链接:每个节点包含指向其子节点的指针或链接。节点之间通过这些指针或链接建立关联关系。通过这种方式,可以在森林中方便地遍历树的节点。
森林数据结构常用于解决一些特定的问题,例如图的连通分量、无向图的生成树等。它提供了一种有效的方式来组织和操作多个独立的树结构。
2.树、森林与二叉树的转换
物理结构上看,数的孩子兄弟表示法和二叉树的二叉链表是相同的,树的孩子兄弟表示法的第一个孩子指针和右兄弟指针分别相当于二叉链表的左孩子指针和右孩子指针。换言之,给定一棵树,可以找到唯一的一棵二叉树与之对应。
2.1 树转换成二叉树
将一棵树转换成二叉树的常见方法是通过改变节点之间的链接关系,以便每个节点最多有两个子节点。这可以通过以下步骤实现:
- 选择树的根节点作为二叉树的根节点。
- 对于树中的每个节点,将其第一个子节点作为其左子节点,并将其右兄弟节点作为其右子节点。如果节点没有右兄弟节点,则将其右子节点设置为null。
- 对于每个节点的右子节点,将其第一个子节点作为其左子节点,并将其右兄弟节点作为其右子节点。如果节点没有右兄弟节点,则将其右子节点设置为null。
- 递归地应用步骤2和步骤3,直到所有节点都被处理。
通过这种转换,原始的树结构被转换成了二叉树结构,其中每个节点最多有左子节点和右子节点。这种转换允许使用二叉树的算法和技巧来处理原始树的问题。
2.2 森林转化成二叉树
将森林转换成二叉树可以通过逐个处理森林中的每棵树,并将每棵树转换为对应的二叉树。
以下是将森林转换为二叉树的步骤:
- 对于森林中的每棵树:
-
选择一棵树的根节点作为二叉树的根节点。
-
对于每棵树的节点:
- 如果节点有左子树(子节点),将其作为二叉树节点的左子节点。
-
如果节点有右兄弟节点,则将右兄弟节点作为二叉树节点的右子节点。
-
对于每棵树的根节点之间的链接关系:
- 选择一棵树的根节点作为二叉树的根节点。
- 将其他树的根节点作为前一棵树根节点的右子节点。
通过这些步骤,可以将森林转换为对应的二叉树。每个树的根节点成为二叉树的根节点,树的节点之间的链接关系转换为二叉树节点的左子节点和右子节点。
需要注意的是,如果森林中的每棵树都只有一个节点,则转换后的二叉树将只有一个节点。
2.3 二叉树转化成树或森林
将二叉树转换成树或森林可以通过修改节点之间的链接关系,以便每个节点可以有多个子节点。这可以通过以下步骤实现:
-
对于给定的二叉树,选择其中一个节点作为根节点。
-
对于每个节点:
- 如果节点有左子节点,将其作为父节点的子节点之一。
-
如果节点有右子节点,将其作为父节点的兄弟节点(即父节点的下一个子节点)。
-
对于二叉树中的每个节点,将其右子节点设置为null,以断开其与右兄弟节点的链接。
通过这些步骤,可以将二叉树转换成一棵树,其中每个节点可以有多个子节点。如果原始的二叉树包含多个独立的二叉树(森林),则可以为每个独立的二叉树重复上述步骤,从而将二叉树转换成森林。
需要注意的是,转换后的树或森林可能会失去原始二叉树中节点之间的某些顺序或关系。
六、 最优二叉树
1.哈夫曼算法
哈夫曼树(Huffman Tree),也称为最优二叉树(Optimal Binary Tree),是一种特殊的二叉树,常用于数据压缩和编码的算法中。它是一种带权路径长度最短的树,其中节点的权值表示其在编码中的频率或概率。
构建哈夫曼树的基本思想是通过合并权值较小的节点来逐步构建树,使得权值较大的节点位于树的较低层,从而实现最优的编码效果。
以下是构建哈夫曼树的步骤:
-
给定一组权值或频率,将每个权值或频率作为一个独立的节点。
-
从这些节点中选择权值最小的两个节点作为叶子节点,创建一个新的父节点,并将这两个节点设置为新节点的左右子节点。
-
将新节点的权值设置为两个子节点的权值之和。
-
将新节点插入到原节点集合中,并删除原来的两个子节点。
-
重复步骤2至4,直到只剩下一个节点,即构建完成的哈夫曼树的根节点。
在构建完成的哈夫曼树中,较频繁出现的字符或数据对应的节点位于较低层,而较不频繁出现的字符或数据对应的节点位于较高层。这样,可以使用较短的编码表示频繁出现的字符或数据,从而实现数据的高效压缩和编码。
哈夫曼树常用于哈夫曼编码算法中,该编码算法将字符或数据映射到哈夫曼树中的叶子节点,并通过叶子节点路径上的0和1来表示编码。编码后的数据可以实现高效的压缩和解压缩操作。
哈夫曼编码
哈夫曼编码(Huffman Coding)是一种变长编码方法,用于将字符或数据进行有效压缩。它基于哈夫曼树的概念,通过给出频率较高的字符较短的编码,从而实现压缩率的提高。
下面是哈夫曼编码的详细步骤:
-
统计字符或数据的频率:遍历待编码的字符或数据流,统计每个字符或数据的出现频率。
-
构建哈夫曼树:根据字符或数据的频率构建哈夫曼树。频率越高的字符或数据对应的节点位于树的较低层,频率越低的字符或数据对应的节点位于较高层。
-
分配编码:从哈夫曼树的根节点开始,遍历每个叶子节点,并分配编码。在遍历过程中,向左走的路径表示编码位0,向右走的路径表示编码位1。每个叶子节点所经过的路径即为其对应字符或数据的编码。
-
生成编码表:将每个字符或数据与其对应的编码存储在编码表中,以便后续的编码和解码操作。
-
进行编码:根据生成的编码表,将待编码的字符或数据流转换为对应的哈夫曼编码。
-
进行解码:使用相同的哈夫曼树和编码表,将哈夫曼编码解析为原始的字符或数据。
通过哈夫曼编码,频率较高的字符或数据被赋予较短的编码,而频率较低的字符或数据被赋予较长的编码。这样,可以实现对原始数据的高效压缩。
需要注意的是,为了确保编码的唯一解析性,哈夫曼编码要求没有任何一个字符或数据的编码是其他字符或数据编码的前缀。这被称为哈夫曼编码的前缀码性质,保证了编码的解码过程是无歧义的。
哈夫曼编码在数据压缩、通信传输和存储等领域有广泛应用,能够有效地减小数据的存储空间和传输带宽。
加油hh
666很好