树和二叉树
一、树
1.1 树的基本概念
1.1.1 树的定义
- 树是一种数据结构,它是由n个有限节点组成一个具有层次关系的集合
1.1.2 树的特点
- 每个节点有零个或多个子节点
- 没有父节点的节点称为根节点
- 每一个非根节点有且只有一个父节点
- 除了根节点外,每个子节点可以分为多个不相交的子树
1.1.3 树的结构
1.1.4 树的基本术语
- 节点的高度:节点到叶子节点的最长路径
- 节点的深度:根节点到这个节点所经历的边的个数
- 节点的层数:节点的深度 + 1
- 节点的度:结点拥有的子树的数目
- 叶子:度为零的结点
- 分支结点:度不为零的结点
- 树的度:树中结点的最大的度
- 层次:根结点的层次为1,其余结点的层次等于该结点的双亲结点的层次加1
- 树的高度:树中结点的最大层次
- 无序树:树中结点的各子树之间的次序是不重要的,可以交换位置
- 有序树:树中结点的各子树之间的次序是重要的, 不可以交换位置
- 森林:0个或多个不相交的树组成
1.1.5 树的性质
- 树的节点数 = 所有节点的度数之和 + 1
- 度为m的树中第i层上最多有 m i − 1 m^{i-1} mi−1个结点
- 已知度m和高度h
- 求树的最少节点数:让1~h-1层节点数都为1,最后一层节点数为m
- 求树的最多节点数:让树成为满m叉树
1.2 树的存储结构
1.2.1 双亲表示法
- 采用一组连续空间来存储每个节点
- 在每个节点中设置一个伪指针
- 伪指针指示其双亲节点在数组中的位置
1.2.2 孩子表示法
- 将每个节点的孩子节点用单链表连接
1.2.3 孩子兄弟表示法
- 又叫二叉树表示法
- 以二叉链表作为树的存储结构
- 节点内容包含3个部分:孩子节点、数据、兄弟节点
二、二叉树
2.1 二叉树的基本概念
2.1.1 二叉树的定义
- 二叉树是每个节点最多有两个子树的树结构
- 它有五种基本形态
2.1.2 二叉树的性质
- 二叉树第i层上的节点数目最多为 2 i − 1 2^{i−1} 2i−1 个
- 深度为k的二叉树最多有 2 k − 1 2^k−1 2k−1个节点(满二叉树)
- 包含n个节点的二叉树的高度至少为 l o g 2 ( n + 1 ) log_2(n+1) log2(n+1)
- 树的节点数 = 所有节点的度数之和 + 1
- 在任意一颗二叉树中,若终端节点的个数为 n 0 n_0 n0, 度为2的节点数为 n 2 n_2 n2, 则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
2.2 特殊的二叉树
2.2.1 满二叉树
- 高度为h,并且由
2
h
−
1
2^h−1
2h−1个结点的二叉树
2.2.2 完全二叉树
- 叶子结点只能出现在最下层和次下层, 最下层的叶子结点集中在树的左部
- 一棵满二叉树必定是一棵完全二叉树
- 完全二叉树未必是满二叉树
- 完全二叉树中, 度为1的节点数 = 0个或者1个(计算时可以用这个快速计算, 配合
n
0
=
n
2
+
1
n_0=n_2+1
n0=n2+1)
2.2.3 二叉查找树
- 也叫二叉排序树、二叉搜索树
- 左子树节点比根节点值小
- 右子树节点比根节点值大
- 没有键值相等的节点
2.2.4 平衡二叉树
- 树上任一结点的左子树和右子树的深度之差不超过1
2.3 二叉树的存储结构
2.3.1 顺序存储结构
2.3.1.1 定义
- 一般用数组存二叉树的节点
- 只要知道根节点的存储位置,就可以通过下标计算,把整棵树串起来
- 适用于完全二叉树、满二叉树
2.3.1.2 特点
- 节点x存储在下标i的位置
- 该节点的左节点存储在下标为2i的位置
- 该节点的右节点存储在下标为2i+1的位置
- 下标i/2的位置,存储的就是该节点的父节点
- 空间利用率不高,容易造成空间浪费
2.3.1.3 结构图
2.3.2 链式存储结构
2.3.2.1 定义
- 只要知道根节点,就可以通过左右子节点的指针把整棵二叉树串起来
- 适用于二叉树
2.3.2.2 特点
- 二叉链表中至少包含3个域:数据域data、左指针域lchild、右指针域rchild
2.3.2.3 结构图
2.4 二叉树的实现
2.4.1 二叉树的结构体
typedef struct TreeNode *BinTree;
struct TreeNode
{
int Data; // 存值
BinTree Left; // 左儿子结点
BinTree Right; // 右儿子结点
};
2.4.2 二叉树的三种遍历方法
2.4.2.1 前序遍历
2.4.2.1.1 实现逻辑
- 遍历过程:根节点—>左节点---->右节点
2.4.2.1.2 递归代码
void PreOrderTraversal(BinTree BT)
{
if (BT)
{
printf("%d", BT->Data); // 打印根
PreOrderTraversal(BT->Left); // 进入左子树
PreOrderTraversal(BT->Right); // 进入右子树
}
}
2.4.2.1.3 非递归代码
void PreOrderTraversal(BinTree BT)
{
BinTree T = BT;
Stack S = CreateStack(); // 创建并初始化堆栈 S
while (T || !IsEmpty(S))
{ // 当树不为空或堆栈不空
while (T)
{
Push(S, T); // 压栈,第一次遇到该结点
printf("%d", T->Data); // 访问结点
T = T->Left; // 遍历左子树
}
if (!IsEmpty(S))
{ // 当堆栈不空
T = Pop(S); // 出栈,第二次遇到该结点
T = T->Right; // 访问右结点
}
}
}
2.4.2.2 中序遍历
2.4.2.2.1 实现逻辑
- 遍历过程:左节点—>根节点---->右节点
2.4.2.2.2 递归代码
void InOrderTraversal(BinTree BT)
{
if (BT)
{
InOrderTraversal(BT->Left); // 进入左子树
printf("%d", BT->Data); // 打印根
InOrderTraversal(BT->Right); // 进入右子树
}
}
2.4.2.2.3 非递归代码
void InOrderTraversal(BinTree BT)
{
BinTree T = BT;
Stack S = CreateStack(); // 创建并初始化堆栈S
while (T || !IsEmpty(S))
{ // 当树不为空或堆栈不空
while (T)
{
Push(S, T); // 压栈
T = T->Left; // 遍历左子树
}
if (!IsEmpty(S))
{ // 当堆栈不空
T = Pop(S); // 出栈
printf("%d", T->Data); // 访问结点
T = T->Right; // 访问右结点
}
}
}
2.4.2.3 后序遍历
2.4.2.3.1 实现逻辑
- 遍历过程:左节点—>右节点---->根节点
2.4.2.3.2 递归代码
void PostOrderTraversal(BinTree BT)
{
if (BT)
{
PostOrderTraversal(BT->Left); // 进入左子树
PostOrderTraversal(BT->Right); // 进入右子树
printf("%d", BT->Data); // 打印根
}
}
2.4.2.3.3 非递归代码
void PostOrderTraversal(BinTree BT)
{
BinTree T = BT;
Stack S = CreateStack(); // 创建并初始化堆栈 S
vector<BinTree> v; // 创建存储树结点的动态数组
Push(S, T);
while (!IsEmpty(S))
{ // 当树不为空或堆栈不空
T = Pop(S);
v.push_back(T); // 出栈元素进数组
if (T->Left)
Push(S, T->Left);
if (T->Right)
Push(S, T->Right);
}
reverse(v.begin(), v.end()); // 逆转
for (int i = 0; i < v.size(); i++) // 输出数组元素
printf("%d", v[i]->Data);
}
2.4.2.4 层序遍历
2.4.2.4.1 实现逻辑
- 从上至下,从左至右访问所有结点
- 基于队列实现过程
- 根结点入队
- 从队列中取出一个元素
- 访问该元素所指结点
- 若该元素所指结点的左孩子结点非空,左孩子结点入队
- 若该元素所指结点的右孩子结点非空,右孩子结点入队
- 循环2-4, 直到队列中为空
2.4.2.4.2 非递归代码
void LevelOrderTraversal(BinTree BT)
{
queue<BinTree> q; // 创建队列
BinTree T;
if (!BT)
return;
q.push(BT); // BT 入队
while (!q.empty())
{
T = q.front(); // 访问队首元素
q.pop(); // 出队
printf("%d", T->Data);
if (T->Left) // 如果存在左儿子结点
q.push(T->Left); // 入队
if (T->Right)
q.push(T->Right);
}
}
2.4.2.5 结论
- 不能唯一确定一颗二叉树的是:先序序列和后序序列
- 先序遍历第一个节点为根节点;后序遍历最后一个节点为根节点
- 前序序列和中序序列的关系相当于以前序序列为入栈次序,以中序序列为出栈顺序
- 前序序列与后序序列刚好相反的时候,二叉树的高度 = 节点数(即每层只有一个节点)
- 后序遍历可以找到m到n直接的路径(其中m是n的祖先)
- 两个序列确定二叉树
- 性质见树的性质
2.4.3 线索二叉树
2.4.3.1 基本概念
- 对一棵二叉树中所有节点的空指针域按照某种遍历方式加线索的过程叫作线索化
- 线索二叉树是一种物理结构
- 引入线索的目的是加快对二叉树的遍历
- n个节点的线索二叉树上含有线索数量为n+1个
- 线索二叉树就是利用二叉树的n+1个空指针来存放节点的前驱和后继信息的
- 后续线索二叉树不能有效解决求后续后继的问题,后续线索树的遍历仍需要栈的支持
2.4.3.2 结构
ltag=0,表示指向节点的左孩子 | rtag=0,表示指向节点的右孩子 |
---|---|
ltag=1,则表示lchild为线索,指向节点的直接前驱 | rtag=1,则表示rchild为线索,指向节点的直接后继 |
2.4.3.3 线索化过程
- 对二叉树进行前、中、后序遍历
- 节点右子节点为空的指针域指向它的后继节点
- 节点左子节点为空的指针域指向它的前驱节点
三、树和森林
3.1 森林和二叉树的转换
3.1.1 树与二叉树
- 在兄弟节点之间加一连线
- 对每个节点,只保留它与第一个孩子的连线
- 以树根为轴心,顺时针旋转45°
3.2 树和森林的遍历对应关系
- 将森林中的每棵树转换成相应的二叉树
- 每棵树的根也可以视为兄弟关系,在每棵树的之间加一根连线
- 以第一棵树的根为轴心旋转45°
3.3 树和森林的遍历对应关系
树 | 森林 | 二叉树 |
---|---|---|
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
四、树与二叉树的应用
4.1 哈夫曼树、最优二叉树
4.1.1 定义
- 树的带权路径长度最小的二叉树是哈夫曼树
路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。
节点的权和带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。
WPL = 路径长度 * 结点权值
4.1.2 特点
- 没有度为 1 的结点
- n个叶结点的哈夫曼树共有 2n-1 个结点
- 哈夫曼树的任意非叶结点的左右子树交换后仍是哈夫曼树
- 对同一组权值,可能存在不同构的多棵哈夫曼树
- 哈夫曼树不一定是完全二叉树
4.1.3 构造
假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:
(1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和
(3)从森林中删除选取的两棵树,并将新树加入森林
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
4.2 哈夫曼编码
4.2.1 概念
- 前缀编码:没有一个编码是另一个编码的前缀的编码
- 对频率较高的字符使用较短的编码,频率低的字符使用较高的编码。这样保证总体使用的编码长度会更少,从而实现到了数据压缩的目的
4.2.2 实现逻辑
以字符串“aaa bb cccc dd e”为例子
4.2.2.1 统计频率
- 先求出这个字符串中每个字符出现的频率
字符 | c | ’ ’ | a | b | d | e |
---|---|---|---|---|---|---|
频率 | 4 | 4 | 3 | 2 | 2 | 1 |
4.2.2.2 构造哈夫曼树
- 根据字符的频率放入优先队列中进行排序。然后根据这些字符构建一棵哈夫曼树
字符 | c | ’ ’ | a | b | d | e |
---|---|---|---|---|---|---|
频率 | 4 | 4 | 3 | 2 | 2 | 1 |
频率:
哈夫曼树:
- 给哈夫曼树编码,左0右1
- 得出编码表:
字符 | e | d | b | a | ’ ’ | c |
---|---|---|---|---|---|---|
编码 | 1110 | 1111 | 110 | 00 | 01 | 10 |
- 空间计算:出现次数*编码长度
4.3 并查集
4.3.1 定义
- 并查集是一种简单的集合表示,支持3种操作
- 并查集的存储结构是双亲表示法存储的树,主要是为了方便两个主要的操作