目录
- 第五章:树
- 5.1树的基本概念
- 5.1.1树的定义
- 5.1.2 基本术语
- 5.1.3 树的性质
- 5.2二叉树的概念
- 5.2.1 二叉树的定义与特性
- 5.2.2 几种特殊的二叉树
- 5.2.3 二叉树的性质
- 5.2.4 完全二叉树的性质
- 5.2.5 二叉树的存储结构
- 1. 顺序存储
- 重要的基本操作
- 非完全二叉树
- 2. 链式存储
- 逆向寻找父节点
- 5.3二叉树的遍历和线索二叉树
- 5.3.1二叉树的遍历
- 1. 先序遍历(根左右 NLR)
- 2. 中序遍历(左根右 LNR)
- 3. 后续遍历(左右根 LRN)
- 4. 求树的深度(递归应用)
- 5. 非递归遍历
- 6. 层次遍历
- 7. 由遍历序列构造二叉树
- 5.3.2线索二叉树
- 1. 线索二叉树的概念与作用
- 2.线索二叉树的存储结构
- 3. 二叉树的线索化
- 1. 中序线索化
- 2. 先序线索化
- 3. 后序线索化
- 4. 线索树的寻找前驱后继的各种情况(多理解)
- 5.4树、森林
- 5.4.1树的存储结构
- 1. 双亲表示法(顺序存储):
- 2. 孩子表示法(顺序+链式)
- 3. 孩子兄弟表示法(链式)
- 5.4.2树、森林与二叉树的转换
- 5.4.3树、森林的遍历
- 1. 树的遍历
- 先根遍历
- 后根遍历
- 层序遍历(队列实现)
- 2. 森林的遍历
- 5.5树与二叉树的应用
- 5.5.1 哈夫曼树和哈夫曼编码
- 1. 带权路径长度的定义
- 2. 哈夫曼树的定义(最优二叉树,不唯一)
- 3. 哈夫曼树的构造
- 4. 哈夫曼树的特点
- 5.哈夫曼编码(最短二进制前缀编码)
- 5.5.2 并查集(双亲表示法)
- 1. 并查集的存储结构
- 2. 并查集的代码实现
- 初始化
- 并查
- 时间复杂度
- union操作的优化(不要瘦高的树)
- 并查集的进一步优化(find的优化,压缩路径)
- 优化总结
- ~~5.5.3二叉排序树(BST)~~
- 1. 二叉排序树的定义
- 2. 查找操作
- 3. 插入操作
- 4. 二叉排序树的构造
- 5.5.2平衡二叉树(AVL)
第五章:树
5.1树的基本概念
节点数 = 度的和+1或者节点数
5.1.1树的定义
树是n个结点的有限集。
- 空树:n=0
- 根结点、分支结点、叶子结点
- 非空树的特性
- 子树
在n个结点的树中有n-1条边
5.1.2 基本术语
结点之间的关系描述
- 祖先、子孙、双亲、兄弟结点(亲兄弟同父节点、堂兄弟同一层次)
- 路径(路径只有从上往下,需要从下往上的就不存在路径)
- 路径长度(树根到每个节点的路径长度的总和)
结点、树的属性描述
- 结点的层次(深度,默认从1)——从上往下
- 结点的高度——从下往上
- 树的高度——总共多少层
- 结点的度——有几个孩子
- 树的度——各结点的度的最大值
有序树(家谱,从左到右有次序不可以互换)、无序树(左右位置没有逻辑关系,可以互换)
森林(多颗互不相交的树的集合,允许有空森林,跟树一样节点数可以为0)
森林与树的转换
- 当森林的树拥有共同根节点就是一棵树
5.1.3 树的性质
-
树中的结点数等于所有结点的度数之和加1。(这个1是根节点,因为每个节点的度代表他的直接子节点个数,这些全部相加就差根节点的个数也就是1)
-
度为m的树第i层上至多有m^i-1个结点(这是因为树的度代表最多一个节点拥有的最多的子节点个数,所以至多就是指数级别的关系)
度为m的数、m叉数的区别
- m叉树,只要每个节点的孩子数小于等于m,并且不要求存在一个节点的度等于m,所以一棵二叉树也一定是一棵三叉树,四叉树·······
- 度为m的树,一定要有一个节点的度是m,所以不可能是空树,对于m叉树来说就可以是空树
度为m,有n个节点的树:树的高度最高是,n-(m+1)+2 = n-m+1
5.2二叉树的概念
5.2.1 二叉树的定义与特性
定义:
二叉树是n(n>=0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两颗互不相交的分别称作这个根的左子树和右子树的二叉树组成。
特点:
- 每个结点最多有俩孩子(二叉树中不存在度大于2的结点)。
- 二叉树可以是空集合,根可以有空的左子树和空的右子树。
- 二叉树有左右之分,次序不能颠倒。(有序,且子树也是二叉树)
5.2.2 几种特殊的二叉树
-
满二叉树:一颗深度为k且有2^k-1个结点的二叉树称为满二叉树。每一层上的结点数都达到最大。叶子全部在最低层。
-
完全二叉树:深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点一 一对应时,称之为完全二叉树。(缺少的只会是最后一个节点,递归这种缺少才能满足,就是不能缺少8号节点,因为这样8后边的所有节点都会变换序号,与满二叉树不一样了)
完全二叉树的最多且唯一一个度为1的节点一定有的是左孩子 -
二叉排序树
-
平衡二叉树
5.2.3 二叉树的性质
5.2.4 完全二叉树的性质
5.2.5 二叉树的存储结构
1. 顺序存储
#define MaxSize 100
struct TreeNode{
ElemType value; //结点中的数据元素
bool isEmpty; //结点是否为空
}
main(){
TreeNode t[MaxSize];
for (int i=0; i<MaxSize; i++){
t[i].isEmpty = true;
}
}
重要的基本操作
二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来;
非完全二叉树
2. 链式存储
每个节点有两个指针域,n个节点就有2n个指针域,但是只有n-1个指针域指向n-1个子节点,所以有n+1个空指针域
//二叉树的结点
struct ElemType{
int value;
};
typedef struct BiTnode{
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
}BiTNode, *BiTree;
//定义一棵空树
BiTree root = NULL;
//插入根节点
root = (BiTree) malloc (sizeof(BiTNode));
root -> data = {1}; //{}是必须的,因为这里对结构体初始化
root -> lchild = NULL;
root -> rchild = NULL;
//插入新结点
BiTNode *p = (BiTree) malloc (sizeof(BiTNode));
p -> data = {2};
p -> lchild = NULL;
p -> rchild = NULL;
root -> lchild = p; //作为根节点的左孩子
逆向寻找父节点
三叉链表
5.3二叉树的遍历和线索二叉树
5.3.1二叉树的遍历
1. 先序遍历(根左右 NLR)
若二叉树为空,不用操作
若二叉树非空:
- 访问根节点
- 先序遍历左子树
- 先序遍历右子树
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
void PreOrder(BiTree T){
if(T!=NULL){
visit(T); //访问根结点
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchild); //递归遍历右子树
}
}
2. 中序遍历(左根右 LNR)
若二叉树为空,不用操作
若二叉树非空:
- 先序遍历左子树
- 访问根节点
- 先序遍历右子树
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild); //递归遍历左子树
visit(T); //访问根结点
InOrder(T->rchild); //递归遍历右子树
}
}
3. 后续遍历(左右根 LRN)
若二叉树为空,不用操作
若二叉树非空:
- 先序遍历左子树
- 先序遍历右子树
- 访问根节点
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
void PostOrder(BiTree T){
if(T!=NULL){
PostOrder(T->lchild); //递归遍历左子树
PostOrder(T->rchild); //递归遍历右子树
visit(T); //访问根结点
}
}
4. 求树的深度(递归应用)
5. 非递归遍历
-
先序遍历
-
中序遍历
-
后序遍历
6. 层次遍历
//二叉树的结点(链式存储)
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
//链式队列结点
typedef struct LinkNode{
BiTNode * data;
typedef LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front, *rear;
}LinkQueue;
//层序遍历
void LevelOrder(BiTree T){
LinkQueue Q;
InitQueue (Q); //初始化辅助队列
BiTree p;
EnQueue(Q,T); //将根节点入队
while(!isEmpty(Q)){ //队列不空则循环
DeQueue(Q,p); //队头结点出队
visit(p); //访问出队结点
if(p->lchild != NULL)
EnQueue(Q,p->lchild); //左孩子入队
if(p->rchild != NULL)
EnQueue(Q,p->rchild); //右孩子入队
}
}
7. 由遍历序列构造二叉树
给定的二叉树的遍历序列是固定的;给定遍历序列得到的二叉树是不固定的
- 先序序列 + 中序序列
- 后序序列 + 中序序列
- 层序序列 + 中序序列
key: 找到树的根节点,并根据中序序列划分左右子树,再找到左右子树根节点
这也是为什么一定要有中序序列的原因
其他的遍历序列不能判断左子树和右子树的相对位置关系
5.3.2线索二叉树
从某个节点的前驱后继、遍历都很不方便
1. 线索二叉树的概念与作用
在二叉树的结点上加上线索的二叉树称为线索二叉树,对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化。
2.线索二叉树的存储结构
- 中序线索二叉树——线索指向中序前驱、中序后继
//线索二叉树结点
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
-
先序线索二叉树——线索指向先序前驱、先序后继
-
后序线索二叉树——线索指向后序前驱、后序后继
3. 二叉树的线索化
全局变量pre
1. 中序线索化
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
//全局变量pre, 指向当前访问的结点的前驱
TreadNode *pre=NULL;
void InThread(ThreadTree T){
if(T!=NULL){
InThread(T->lchild); //中序遍历左子树
visit(T); //访问根节点
InThread(T->rchild); //中序遍历右子树
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){ //左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if(pre!=NULL && pre->rchild = NULL){
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q;
}
//中序线索化二叉树T
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{ //非空二叉树才能进行线索化
InThread(T); //中序线索化二叉树
if(pre->rchild == NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
2. 先序线索化
注意【转圈】问题,当ltag==0时,才能对左子树先序线索化
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
//全局变量pre, 指向当前访问的结点的前驱
TreadNode *pre=NULL;
//先序遍历二叉树,一边遍历一边线索化
void PreThread(ThreadTree T){
if(T!=NULL){
visit(T);
if(T->ltag == 0) //lchild不是前驱线索
//并且由于中序和后序都已经处理完了左子树,所以用不到lchild
PreThread(T->lchild);
PreThread(T->rchild);
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){ //左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if(pre!=NULL && pre->rchild = NULL){
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q;
}
//先序线索化二叉树T
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{ //非空二叉树才能进行线索化
PreThread(T); //先序线索化二叉树
if(pre->rchild == NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
3. 后序线索化
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
//全局变量pre, 指向当前访问的结点的前驱
TreadNode *pre=NULL;
//先序遍历二叉树,一边遍历一边线索化
void PostThread(ThreadTree T){
if(T!=NULL){
PostThread(T->lchild);
PostThread(T->rchild);
visit(T); //访问根节点
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){ //左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if(pre!=NULL && pre->rchild = NULL){
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q;
}
//先序线索化二叉树T
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{ //非空二叉树才能进行线索化
PostThread(T); //后序线索化二叉树
if(pre->rchild == NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
中序和后序都已经处理完了左子树,所以用不到lchild,也就不同考虑他的转圈问题
4. 线索树的寻找前驱后继的各种情况(多理解)
5.4树、森林
5.4.1树的存储结构
1. 双亲表示法(顺序存储):
每个结点中保存指向双亲的指针
数据域:存放结点本身信息。
双亲域:指示本结点的双亲结点在数组中的位置。
#define MAX_TREE_SIZE 100 //树中最多结点数
typedef struct{ //树的结点定义
ElemType data;
int parent; //双亲位置域
}PTNode;
typedef struct{ //树的类型定义
PTNode nodes[MAX_TREE_SIZE]; //双亲表示
int n; //结点数
}PTree;
基本操作:
- 增:新增数据元素,无需按逻辑上的次序存储,只要在最后增加并且写上其父节点;(需要更改结点数n)
- 删(叶子结点):① 将伪指针域设置为-1;②用后面的数据填补(不会中间出现空数据);(需要更改结点数n)
- 查询:①优点-查指定结点的双亲很方便;②缺点-查指定结点的孩子只能从头遍历,空数据导致遍历更慢;
- 所以删除时用②用后面的数据填补(不会中间出现空数据);(需要更改结点数n)这个方法更好
- 求双亲很方便,但是求结点的孩子需要遍历整个结构
2. 孩子表示法(顺序+链式)
孩子链表:把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存储,则n个结点有n个孩子链表(叶子的孩子链表为空表)。而n个头结点又组成一个线性表,用顺序表(含n个元素的结构数组)存储。
问题在于:找孩子方便,找双亲不方便,需要遍历整个结构(n个结点的孩子链表指针域指向的n个孩子链表)
struct CTNode{
int child; //孩子结点在数组中的位置
struct CTNode *next; // 下一个孩子
};
typedef struct{
ElemType data;
struct CTNode *firstChild; // 第一个孩子
}CTBox;
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n, r; // 结点数和根的位置
}CTree;
3. 孩子兄弟表示法(链式)
第一个孩子与右兄弟
特点:
- 最大的优点是实现树转换成二叉树和相互转换
- 易于查找结点的孩子
- 缺点是从当前结点查找双亲很麻烦,可以为每个结点设置一个parent指针指向父结点
- 孩子表示法存储的树,在物理上是二叉树
typedef struct CSNode{
ElemType data; //数据域
struct CSNode *firstchild, *nextsibling; //第一个孩子和右兄弟指针, *firstchild 看作左指针,*nextsibling看作右指针
}CSNode. *CSTree;
5.4.2树、森林与二叉树的转换
本质:森林中各个树的根结点之间视为兄弟关系
5.4.3树、森林的遍历
1. 树的遍历
先根后根都是深度优先,层次是广度优先遍历
先根遍历
- 若树非空,先访问根结点,再依次对每棵子树进行先根遍历;(与对应二叉树的先序遍历序列相同)树的深度优先遍历
void PreOrder(TreeNode *R){
if(R!=NULL){
visit(R); //访问根节点
while(R还有下一个子树T)
PreOrder(T); //先跟遍历下一个子树
}
}
后根遍历
树的深度优先遍历
- 若树非空,先依次对每棵子树进行后根遍历,最后再返回根节点;(与对应二叉树的中序遍历序列相同,但是直接看的话还是后序遍历序列,不转化成二叉树来看的话就直接用后序遍历求解)
void PostOrder(TreeNode *R){
if(R!=NULL){
while(R还有下一个子树T)
PostOrder(T); //后跟遍历下一个子树
visit(R); //访问根节点
}
}
层序遍历(队列实现)
广度优先遍历
若树非空,则根结点入队;
若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队;
重复以上操作直至队尾为空;
2. 森林的遍历
先序遍历:等同于依次对各个树进行先根遍历;也可以先转换成与之对应的二叉树,对二叉树进行先序遍历;
中序遍历:等同于依次对各个树进行后根遍历;也可以先转换成与之对应的二叉树,对二叉树进行中序遍历;
5.5树与二叉树的应用
5.5.1 哈夫曼树和哈夫曼编码
1. 带权路径长度的定义
权值大于零
2. 哈夫曼树的定义(最优二叉树,不唯一)
3. 哈夫曼树的构造
每次都选取权值最小的结点构成一棵树,并且新的树的根节点的权值为两个子结点的权值之和,重复这个步骤就能构成哈夫曼树
4. 哈夫曼树的特点
叶子结点n,度为二的结点n-1
5.哈夫曼编码(最短二进制前缀编码)
- 固定长度编码
- 可变长度编码(无歧义)
5.5.2 并查集(双亲表示法)
1. 并查集的存储结构
2. 并查集的代码实现
初始化
并查
时间复杂度
union操作的优化(不要瘦高的树)
并查集的进一步优化(find的优化,压缩路径)
优化总结
5.5.3二叉排序树(BST)
1. 二叉排序树的定义
左子树结点值<根结点值<右子树结点值
2. 查找操作
typedef struct BSTNode{
int key;
struct BSTNode *lchild, *rchild;
}BSTNode, *BSTree;
//在二叉排序树中查找值为key的结点(非递归)
//最坏空间复杂度:O(1)
BSTNode *BST_Search(BSTree T, int key){
while(T!=NULL && key!=T->key){ //若树空或等于跟结点值,则结束循环
if(key<T->key) //值小于根结点值,在左子树上查找
T = T->lchild;
else //值大于根结点值,在右子树上查找
T = T->rchild;
}
return T;
}
//在二叉排序树中查找值为key的结点(递归)
//最坏空间复杂度:O(h)
BSTNode *BSTSearch(BSTree T, int key){
if(T == NULL)
return NULL;
if(Kry == T->key)
return T;
else if(key < T->key)
return BSTSearch(T->lchild, key);
else
return BSTSearch(T->rchild, key);
}
3. 插入操作
//在二叉排序树中插入关键字为k的新结点(递归)
//最坏空间复杂度:O(h)
int BST_Insert(BSTree &T, int k){
if(T==NULL){ //原树为空,新插入的结点为根结点
T = (BSTree)malloc(sizeof(BSTNode));
T->key = k;
T->lchild = T->rchild = NULL;
return 1; //插入成功
}
else if(K == T->key) //树中存在相同关键字的结点,插入失败
return 0;
else if(k < T->key)
return BST_Insert(T->lchild,k);
else
return BST_Insert(T->rchild,k);
}
4. 二叉排序树的构造
//按照str[]中的关键字序列建立二叉排序树
void Crear_BST(BSTree &T, int str[], int n){
T = NULL; //初始时T为空树
int i=0;
while(i<n){
BST_Insert(T,str[i]); //依次将每个关键字插入到二叉排序树中
i++;
}
}
5.5.2平衡二叉树(AVL)
- 平衡二叉树的定义
在插入和删除二叉树的结点时,要保证任意结点的左右子树的高度差的绝对值不超过1,将这样的树称为平衡二叉树。
//平衡二叉树结点
typedef struct AVLNode{
int key; //数据域
int balance; //平衡因子
struct AVLNode *lchild; *rchild;
}AVLNode, *AVLTree;