五、树与二叉树
目录
- 五、树与二叉树
- 树的基本概念
- 二叉树的概念
- 基础概念
- 常考性质
- 存储方式
- 二叉树遍历及线索二叉树
- 前中后以及层次遍历
- 线索二叉树
- 树、森林
- 树的存储结构
- 树、森林与二叉树的转换
- 树、森林的遍历
- 树与二叉树应用
- 哈夫曼树
- 并查集
树的基本概念
树是n个结点的有限集合,n=0时,称为空树。非空树满足:
除了根节点外,任何一个结点都有且仅有一个前驱
- 结点的层次(深度):从上往下数
- 结点的高度:从下往上数
- 树的高度(深度):总共有多少层
- 结点的度:有几个孩子(分支)
- 树的度:各节点的度的最大值
- 森林:m课互不相交的树的集合(m可为0,空森林)
树的性质:
- 结点数=总度数+1
- 度为m的树、m叉树的区别
- 度为m的树第i层至多有mi-1个结点(i>=1);m叉树第i层至多有mi-1个结点(i>=1)
- 高度为h的m叉树至多有mh-1/m-1个结点
- 高度为h的m叉树至少有h个结点;高度为h、度为m的树至少有h+m-1个结点
- 具有n个结点的m叉树的最小高度为向上取整 logm(n(m-1)+1)
二叉树的概念
基础概念
二叉树时n个结点的有限集合(n可以为0)
由一个根节点和两个互不相交的被称为根的左子树和右子树组成,左子树和右子树又分别是一棵二叉树
- 每个结点至多只有两棵子树
- 左右子树不能颠倒(二叉树是有序树)
五种形态:
特殊二叉树:
满二叉树、完全二叉树:
二叉排序树:
平衡二叉树:
常考性质
二叉树常考性质:
-
结点性质:n0=n2+1
-
第i层结点数
-
高度为h时最多结点数(满二叉树)
完全二叉树常考性质:
- 具有n个(n>0)结点的完全二叉树高度(两种推导)
- 可以由结点数n退出度为0、1、2的结点个数n0、n1、n2
总结:
存储方式
二叉树的顺序存储:
#define MaxSize 100
struct TreeNode {
int value;//结点中的数据元素
bool isEmpty;//判断结点是否为空
};
int main()
{
TreeNode t[MaxSize];//定义一个长度为MaxSize的数组t,按照从上至下,从左至右的顺序依次存储二叉树中的各个结点
for (int i = 0; i < MaxSize; i++)
{
t[i].isEmpty = true;//初始化时所有结点标记为空
}
}
完全二叉树中,有着如下基本操作:
非完全二叉树使用顺序存储:
二叉树的链式存储:
//定义储存数据类型
struct ElemType {
int value;
};
typedef struct BiTNode {
ElemType data;//数据域
struct BiTNode* lchild, * rchild;//左右孩子指针
} BiTNode, * BiTree;
//初始化
void InitTree(BiTree root) {
root = (BiTree)malloc(sizeof(BiTNode));
root->data = { 1 };
root->lchild = NULL;
root->rchild = NULL;
}
//插入新结点
bool InsertNode(BiTree T, ElemType val) {
BiTNode* p = (BiTNode*)malloc(sizeof(BiTNode));
p->data = val;
p->lchild = NULL;
p->rchild = NULL;
T->lchild = p;//作为左孩子
}
n个结点的二叉链表共有n+1个空链域
二叉树遍历及线索二叉树
前中后以及层次遍历
递归法:
先序遍历
//先序遍历
void PreOder(BiTree T) {
if (T != NULL) {
visit(T);//访问根节点
PreOder(T->lchild);//遍历左子树
PreOder(T->rchild);//遍历右子树
}
}
中序遍历
//中序遍历
void InOrder(BiTree T) {
if (T != NULL) {
InOrder(T->lchild);//遍历左子树
visit(T);//访问根节点
InOrder(T->rchild);//遍历右子树
}
}
后序遍历
//后序遍历
void PostOder(BiTree T) {
if (T != NULL) {
PostOder(T->lchild);
PostOder(T->rchild);
visit(T);
}
}
非递归法:
先序遍历(需要用到辅助栈)
//先序遍历
void PreOrder2(BiTree T)
{
SqStack S;
InitStack(S); BiTree p = T;//初始化栈S;p是遍历指针
while (p || !IsEmpty(S)) //栈不空或p不空时循环
{
if (p) //一路向左
{
visit(p); Push(S, p);//访问当前结点,并入栈
p = p->lchild;//左孩子不空,一直向左走
}
else //出栈,并转向出栈结点的右子树
{
Pop(S, p);//栈顶元素出栈
p = p->rchild;//向右子树走,p赋值为当前结点的右孩子
} //返回while循环继续进入if-else语句
}
}
中序遍历
//中序遍历
void InOrder2(BiTree T)
{
SqStack S;
InitStack(S);//初始化栈
BiTree p = T;//p为遍历指针
while (p || !IsEmpty(S))
{
if (p)
{
Push(S, p);//当前结点入栈
p = p->lchild;//左孩子不空,一直向左走
}
else //出栈,并转向出栈结点的右子树
{
Pop(S, p); visit(p);//栈顶元素出栈,访问出栈结点
p = p->rchild;//向右子树走,p赋值为当前结点的右孩子
}//返回while循环继续进入if-else语句
}
}
层次遍历(需要用到辅助队列)
//用于层序遍历的辅助队列
typedef struct LinkNode {
BiTNode* data;//存的是指针而非结点
struct LinkNode* next;
} LinkNode;
typedef struct {
LinkNode* front, * rear;//队头队尾
} LinkQueue;
void InitQueue(LinkQueue& Q) {
Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
//初始化时,front 、rear 都指向头节点
Q.front->next = NULL;
}
bool EnQueue(LinkQueue& Q, BiTNode* x) {
//判满?链式存储一般不需要判满,除非内存不足
LinkNode* s = (LinkNode*)malloc(sizeof(LinkNode));
if (s == NULL)return false;
s->data = x;
s->next = NULL;
Q.rear->next = s;//新节点插入到rear之后
Q.rear = s;//修改表尾指针
return true;
}
bool DeQueue(LinkQueue& Q, BiTNode* x) {
if (Q.front == Q.rear)return false;//队空
LinkNode* p = Q.front->next;//用指针p记录队头元素
x = p->data;//用x变量返回队头元素
Q.front->next = p->next;//修改头节点的next指针
if (Q.rear == p)//此次是最后一个节点出队
Q.rear = Q.front;//修改rear指针,思考一下为什么?
free(p); //释放节点空间
return true;
}
bool isEmpty(LinkQueue Q) {
return Q.front == Q.rear ? true : false;
}
//层序遍历
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);
}
}
应用:求树的深度
//求树的深度
int treeDepth(BiTree T)
{
if (T == NULL)
{
return 0;
}
else
{
int l = treeDepth(T->lchild);
int r = treeDepth(T->rchild);
//树的深度=Max(左子树深度,右子树深度)+1
return l > r ? l + 1 : r + 1;
}
}
由二叉树的遍历序列可以构造出二叉树
前序+中序:
后序+中序:
层序+中序:
若没有中序遍历则不能够构造出二叉树
线索二叉树
线索二叉树的存在,方便我们能够找到某个结点的前驱以及后继
线索二叉树利用叶子结点中的空链域对前驱线索以及后继线索进行存储,并且使用ltag、rtag进行标注,tag==0表示指针指向的是孩子,tag==1表示指针指向的是“线索”
新的结构体:
struct ElemType {
int value;
};
typedef struct ThreadNode {
ElemType data;//数据域
struct ThreadNode* lchild, * rchild;//左右孩子指针
int ltag, rtag;//左右线索标志
} ThreadNode, * ThreadTree;
ThreadNode* pre = NULL;//全局变量用于暂存当前访问结点的前驱
利用visit函数来进行线索化的操作:
void visit(ThreadNode* q) {
if (q->lchild == NULL) {//左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {//建立前驱结点的后继线索
pre->rchild = q;
pre->rtag = 1;
}
pre = q;
}
中序线索化:
//中序遍历二叉树,一边遍历一边线索化
void InThread(ThreadTree T) {
if (T != NULL) {
InThread(T->lchild);
visit(T);
InThread(T->rchild);
}
}
//创建中序线索化二叉树T
void CreatInThread(ThreadTree T) {
pre = NULL;
if (T != NULL) {//非空二叉树才能线索化
InThread(T);//中序线索二叉树
if (pre->rchild == NULL) {
pre->rtag = 1;//处理遍历的最后最后一个结点
}
}
}
//中序线索化(王道教材版)
void InThread_CSKaoYan(ThreadTree p, ThreadTree& pre) {
if (p != NULL) {
InThread_CSKaoYan(p->lchild, pre);//递归,线索化左子树
if (p->lchild == NULL) {//左子树为空,建立前驱线索
p->lchild = pre;
p->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {
pre->rchild == p;//建立前驱结点的后及线索
pre->rtag = 1;
}
pre = p;
InThread_CSKaoYan(p->rchild, pre);
}
}
//中序线索化二叉树(王道教材版本)
void CreatInThread_CSKaoYan(ThreadTree T) {
ThreadTree pre = NULL;
if (T != NULL) {
InThread_CSKaoYan(T, pre);
pre->rchild = NULL;//思考:为什么处理最后一个结点时没有判断rchild 是否为NULL?
pre->rtag = 1;//答:因为最后一个结点的右孩子必为空。
}
}
先序线索化:
//先序线索化,一边遍历一边线索化
void PreThread(ThreadTree T) {
if (T != NULL) {
visit(T);
if (0 == T->ltag) {//lchild不是前驱线索
PreThread(T->lchild);
}
PreThread(T->rchild);
}
}
//创建先序线索化二叉树T
void CreatPreThread(ThreadTree T) {
pre == NULL;
if (T != NULL) {
PreThread(T);
if (pre->rchild == NULL) {
pre->rtag = 1;//处理遍历的最后一个结点
}
}
}
//先序线索化(王道教程版本)
void PreThread_CSKaoYan(ThreadTree p, ThreadTree& pre) {
if (p != NULL) {
if (p->lchild == NULL) {
p->lchild = pre;
p->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {
pre->rchild == p;//建立前驱结点的后及线索
pre->rtag = 1;
}
pre = p;
if (0 == p->ltag) {
PreThread_CSKaoYan(p->lchild, pre);
}
PreThread_CSKaoYan(p->rchild, pre);
}
}
//先序线索化二叉树(王道教材版本)
void CreatPreThread_CSKaoYan(ThreadTree T) {
ThreadTree pre = NULL;
if (T != NULL) {
PreThread_CSKaoYan(T, pre);
if (pre->rchild = NULL)//处理遍历的最后一个结点
pre->rtag = 1;
}
}
后序线索化:
//后序线索二叉树
void PostThread(ThreadTree T) {
if (T != NULL) {
PostThread(T->lchild);
PostThread(T->rchild);
visit(T);
}
}
//创建后序线索二叉树T
void CreatPostThread(ThreadTree T) {
pre == NULL;
if (T != NULL) {
PostThread(T);
if (pre->rchild == NULL) {
pre->rtag = 1;//处理遍历的最后一个结点
}
}
}
//后序线索化(王道教程版本)
void PostThread_CSKaoYan(ThreadTree p, ThreadTree& pre) {
if (p != NULL) {
PostThread_CSKaoYan(p->lchild, pre);
PostThread_CSKaoYan(p->rchild, pre);
if (p->lchild == NULL) {
p->lchild = pre;
p->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {
pre->rchild == p;//建立前驱结点的后及线索
pre->rtag = 1;
}
pre = p;
}
}
//后序线索化二叉树(王道教材版本)
void CreatPostThread_CSKaoYan(ThreadTree T) {
ThreadTree pre = NULL;
if (T != NULL) {
PostThread_CSKaoYan(T, pre);
if (pre->rchild = NULL)//处理遍历的最后一个结点
pre->rtag = 1;
}
}
而通过线索二叉树找到前驱后继又是另一大问题
中序线索二叉树找后继:
//中序线索二叉树找中序后继
//找到以P为根的子树中,第一个被中序遍历的结点
ThreadNode* FirstNode(ThreadNode* p) {
//循环找到最左下结点(不一定是叶结点)
while (0 == p->ltag) {
p = p->lchild;
}
return p;
}
//在中序线索二叉树中找到结点p的后继结点
ThreadNode* NextNode(ThreadNode* p) {
//在右子树中最左下结点
if (0 == p->rtag)return FirstNode(p->rchild);
else return p->rchild;
}
//对中序线索二叉树进行中序遍历(利用线索实现的非递归算法),空间复杂度为O(1);
void InOrder(ThreadNode* T) {
for (ThreadNode* p = FirstNode(T); p != NULL; p = NextNode(p)) {
visit(p);
}
}
中序线索二叉树找前驱:
//中序线索二叉树找中序前驱
//找到以p为根的子树中,最后一个被中序遍历的结点
ThreadNode* LastNode(ThreadNode* p) {
//循环找到最右下结点(不一定是叶结点)
while (0 == p->rtag)p = p->rchild;
return p;
}
//在中序线索二叉树中找到结点p的前驱结点
ThreadNode* PreNode(ThreadNode* p) {
//左下子树中最右结点
if (0 == p->ltag)return LastNode(p->lchild);
else return p->lchild;
}
//对中序线索二叉树进行逆向中序遍历
void RevInOrder(ThreadNode* T) {
for (ThreadNode* p = LastNode(T); p != NULL; p = PreNode(p)) {
visit(p);
}
}
先序以及后序找前驱后继的思路:
对于先序线索二叉树找前驱以及后序线索二叉树找后继都较为复杂,需要对不同情况进行分类讨论。
树、森林
树的存储结构
双亲表示法
//树——双亲表示法(顺序存储法)
#define MAX_TREE_SIZE 100
typedef struct {
int data; //数据元素
int parent;//双亲位置域
}PTNode;
typedef struct {
PTNode nodes[MAX_TREE_SIZE];//双亲表示
int n;//结点数
}PTree;
优点:找双亲(父节点)很方便
缺点:找孩子不方便,只能从头到尾遍历整个数组
适用于“找父亲”多“找孩子”少的应用场景,如:并查集
孩子表示法(顺序存储+链式存储结合)
//树——孩子表示法(顺序+链式存储)
struct CTNode {
int child;//孩子节点在数组中的位置
struct CTNode* next;//下一个孩子
};
typedef struct {
int data; //数据元素,数据元素类型不定
struct CTNode* firstChild;//第一个孩子
}CTBox;
typedef struct {
CTBox nodes[MAX_TREE_SIZE];//双亲表示
int n, r;//结点数和根的位置
}CTree;
优点:找孩子很方便
缺点:找双亲(父节点)不方便,只能遍历每个链表
适用于“找孩子”多,“找父亲”少的应用场景,如:服务流程树
孩子兄弟表示法
//树——孩子兄弟表示法(链式存储)
typedef struct CSNode {
int data; //数据域,数据类型不定
struct CSNode* firstChild, * nextsibiling;//第一个孩子和右兄弟指针
}CSNode, * CSTree;
与二叉树类似,采用二叉链表实现,每个结点内保存数据元素和两个指针,但两个指针的含义与二叉树结点不同。
用这种表示法存储树或森林时,从存储视角来看形态上与二叉树类似
树、森林与二叉树的转换
树转二叉树
森林转二叉树
二叉树转树
二叉树转森林
树、森林的遍历
树的先根遍历
与相应二叉树的先序序列相同
树的后根遍历
与相应二叉树的中序序列相同
层次遍历(广度优先)用队列实现
森林的先序遍历
效果等同于依次对各个树进行先根遍历,也等同于对相应二叉树先序遍历
森林中序遍历
效果等同于依次对各个树进行后根遍历,也等同于对相应二叉树中序遍历
树与二叉树应用
哈夫曼树
带权路径长度:
哈夫曼树定义:
哈夫曼树构造:
哈夫曼编码:
并查集
利用双亲表示法的存储方式,每个子集合以一棵树表示,所有表示子集合的树,构成表示全集合的森林,根节点的双亲结点为负数
初始化:
#define SIZE 10
int UFSets[SIZE];//集合元素数组
//初始化
void Initial(int S[])
{
for (int i = 0; i < SIZE; i++)
S[i] = -1;
}
查:
//查,找到x所属集合(返回x所属根节点)
int Find(int S[], int x)
{
while (S[x] >= 0)
x = S[x];
return x;//根节点S[]小于0
}
优化查:
//查优化,压缩路径
int Find2(int S[], int x)
{
int root = x;
while (S[root] >= 0) root = S[root];//循环找到根
while (x != root) //压缩路径
{
int t = S[x];//t指向x的父节点
S[x] = root;//x直接挂到根节点下
x = t;
}
return root;
}
并:(并之前要先找到两个元素根节点)
//并,将两个集合根节点传入,合并为一个
void Union(int S[], int Root1, int Root2)
{
//判断为不同集合
if (Root1 == Root2) return;
//将根Root2连接到另一根Root1下面
S[Root2] = Root1;
}
优化并:
//并优化,让小树合并到大树,用根节点的绝对值表示树的结点总数
void Union2(int S[], int Root1, int Root2)
{
if (Root1 == Root2) return;
if (S[Root2] > S[Root2]) //Root2更大,绝对值更小,结点数更小
{
S[Root1] += S[Root2];//累加结点总数
S[Root2] = Root1;//小树合并到大树
}
else
{
S[Root2] += S[Root1];
S[Root1] = Root2;
}
}
优化前后对比:
主要参考:王道考研课程
后续会持续更新考研408部分的学习笔记,欢迎关注。
github仓库(含所有相关源码):408数据结构笔记