树
基本概念
非线性数据结构,以分支关系定义的层次结构
非空树的特点:
- 有且仅有一个根结点
- 没有后继的节点称为"叶子结点"(或终端结点)
- 有后继的结点称为"分支结点"(或非终端结点)
- 除了根结点外,任何一个结点都有且仅有一个前驱
结点之间的关系描述
- 祖先结点
- 子孙结点
- 双亲结点(父结点)
- 孩子结点
- 兄弟结点
- 堂兄弟结点
- 路径:只能从上往下
结点、树的属性描述
- 深度:结点的层次--从上往下数
- 高度:从下往上数
- 树的高度(深度):总共多少层
- 结点的度:有几个孩子(分支),非叶子结点的度>0,叶子结点的度=0
- 树的度:各结点的读的最大值
有序树、无序树
- 有序树:逻辑上看,树中结点的各子树从左至右是有次序的,不能互换
- 无序树:逻辑上看,树中结点的各子树从左至右是无次序的,可以互换
森林
- 森林是m(m≥0)课互不相交的树的集合
- 森林和树的相互转换
常见考点
- 结点数 = 总度数 + 1
- 度为m的树和m叉树的区别
- 度为m的树第i层至多有【m的(i-1)次方】个结点(i≥1),m叉树第i曾至多有这些
- 高度为h的m差树至多有【(m^h-1)/m-1】个结点
- 高度为h的m叉树至少有h个结点;高度为h、度为m的树至少有【h+m-1】个结点
- 具有n个结点的m叉树的最小高度为【logm(n(m-1)+1)】
基本操作
//definition给出树T的定义
//cur_e是T中某个结点
InitTree(&T):构造空树T
DestroyTree(&T):销毁树T
CreateTree(&T,definition):按definition定义构造树T
ClaerTree(&T):将树T清为空树
TreeEmpty(T):若树T为空树,则返回TRUE,否则FALSE
TreeDepth(T):返回树的深度
Root(T):返回树的根
Value(T,cur_e):返回cur_e的值
Assign(T,cur_e,value):结点cur_e赋值给value
Parent(T,cur_e):若cur_e是T的非根结点,则返回它的双亲,否则函数值为"空"
LeftChild(T,cur_e):若cur_e是T的非叶子结点,则返回它的最左孩子,否则返回"空"
RightSibling(T,cur_e):若cur_e有右兄弟,则返回它的右兄弟,否则函数值为"空"
InsertChild(T,cur_e):插入c为T中p指结点的第i个子树
DeleteChild(&T,&p,i):删除T中p所指结点的第i个子树
TraverseTree(T,Visit()):按某种次序对T的每个结点调用函数visit()一次且多次,一旦visit()失败,则操作失败
二叉树
基本概念
二叉树是n(n≥0)个结点的有限集合
- 每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点)
- 二叉树的子树有左右之分,其次序不能任意颠倒
满二叉树
- 一颗高度为h,且含有2^h-1个结点的二叉树
- 只有最后一层有叶子结点
- 不存在度为1的结点
- 按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1;结点i的父节点为i/2(向下取整)
完全二叉树
- 概念:当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应
- 只有最后两层可能有叶子结点
- 最多只有一个度为1的结点
- 同满二叉树的i编号
- i≤n/2为分支结点,i≥n/2为叶子结点
- 如果某结点只有一个孩子,那么一定是左孩子
二叉排序树
一颗二叉树或者是空二叉树,或者是具有如下性质的二叉树:
- 左子树上所有结点的关键字均小于根结点的关键字
- 右子树上所有结点的关键字均大于根结点的关键字
- 左子树和右子树又各是一颗二叉排序树
平衡二叉树
树上任一结点的左子树和右子树的深度之差不超过1
常见考点
- 设非空二叉树中读为0、1、2的结点个数分别为n₀、n₁、n₂,则n₀=n₂+1(叶子结点比二分支结点多一个)
- 完全二叉树的常考性质
- 具有n个结点的完全二叉树的高度h为log₂(n+1)或log₂n+1
- 对于完全二叉树,可以由结点数n推出度为0、1和2的节点个数为n₀、n₁和n₂
基本操作
InitBiTree(&T):构造空二叉树T
DestroyBiTree(&T):销毁二叉树T
CreateBiTree(&T,definition)
CreateTree(&T,definition):按definition定义构造二叉树T
ClaerBiTree(&T):将树T清为空树
BiTreeEmpty(T):若树、二叉T为空树,则返回TRUE,否则FALSE
BiTreeDepth(T):返回树的深度
Root(T):返回树的根
Value(T,e):返回e的值
Assign(T,&e,value):结点e赋值给value
Parent(T,e):若cur_e是T的非根结点,则返回它的双亲,否则函数值为"空"
LeftChild(T,e):返回e的左孩子,若e无左孩子,则返回"空"
RightChild(T,e):返回e的右孩子,若e无右孩子,则返回"空"
LeftSibling(T,e):返回e的左兄弟,若e是T的左孩子或无左兄弟,则返回"空"
RightSibling(T,e):返回e的右兄弟,若e是T的右孩子或无右兄弟,则返回"空"
InsertChild(T,p,LR,c):根据LR为0或1,插入c为T中所指结点的左或右子树。p所指节点的原有左或右子树则成为c的右子树
DeleteChild(T,p,LR):根据LR为0或1,删除T中p所指结点的左或右子树
PreOrderTraversers(T,Visit()):先序遍历T,对每个结点调用函数Visit一次且仅一次。一旦visit()失败,则操作失败
InOrderTraverse(T,Visit()):中序遍历T
PostOrderTraverse(T,Visit()):后序遍历
LevelOrderTraverse(T,Visit()):层序遍历
存储结构
顺序存储
一定要把二叉树的结点编号与完全二叉树对应起来,只适合存储完全二叉树
#define MaxSize 100
struct TreeNode{
ElemType value; //结点中的数据元素
bool isEmpty; //结点是否为空
}
/*
TreeNode t[MaxSize];
定义一个长度为MaxSize的数组t,按照从上至下、从左至右的顺序依次存储完全二叉树中的各个结点
可以让第一个位置孔雀,保证数组下标和结点编号一致
初始化时所有结点标记为空
for(int i = 0;i <MaxSize;i++){
t[i].isEmpty = true;
}
基本操作:
i的左孩子:2i
i的右孩子:2i+1
i的父结点:i/2
i所在层次:log₂(n+1)或log₂n+1
*/
链式存储
找到指定节点p的左右孩子简单,找父结点困难
typedef struct BiTNode{
ElemType data; //数据域
struct BiTNode *lchild,*rchild;//左、右孩子指针
}BiTNode,*BiTree;
//n个结点的二叉链表共有n+1个空链域
基本操作
遍历:按照某种次序把所有结点都访问一遍
二叉树的递归特性:
- 要么是空二叉树
- 要么是由“根+左+右”组成的二叉树
//先序遍历
void PreOrder(BiTree T){
if(T != NULL){
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
//中序遍历
void InOrder(BiTree T) {
if (T != NULL) {
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
//后序遍历
void PostOrder(BiTree T) {
if (T != 0) {
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
//求树的深度
int treeDepth(BiTree T) {
if (T == 0)
return 0;
else {
int l = treeDepth(T->lchild);
int r = treeDepth(T->rchild);
//树的深度 = Max(l,r) + 1
return l > r ? l + 1 : r + 1;
}
}
/*
最简单的visit()函数是:
Status PrintElement(TElemTyppe e){ //输出元素e的值
printf(e); //实际使用时加格式串
return OK;
}
*/
二叉树的层序遍历
- 初始化一个辅助队列
- 根结点入队
- 若队列非空,则队头结点出队,访问该结点,并将其左右孩子插入队尾(如果有的话)
- 重复上一步直至队列为空
//链队列结点
typedef struct LinkNode{
BiTNode * data;
struct LinkNode* next;
}LinkNode;
typedef struct{
LinkNode *front,*rear; //队头队尾
}LinkQueue;
void LeveOrder(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); //右孩子入队
}
}
线索二叉树
作用
在有n个结点的二叉链表中必定存在n+1个空链域,利用这些空链域来存放结点的前驱和后继的信息。
规定:
- 若结点有左子树,则其lchild域指针指示其左孩子,否则令lchild域指示其前驱
- 若结点有右子树,则其rchild域指针指示其右孩子,否则令rchild域指示其前驱
- 避免混淆,增加两个标志域LTag,RTag
- "对于相应的tag为1的结点来说,找相应的前驱和后继非常方便"
- "对于存在左右孩子的结点,如何找前驱和后继呢?"
- 线索链表:以这种结点结构构成的二叉链表作为二叉树的存储结构
- 线索:指向结点前驱和后继的指针
- 线索二叉树:加上线索的二叉树
- 线索化:对二叉树以某种次序遍历使其变为线索二叉树的过程
线索二叉树的存储结构
//线索二叉树
typedef struct ThreadNode {
ElemType data;
struct ThreadNode* lchild, * rchild;
int ltag, rtag; //左、右线索标志
}ThreadNode,*ThreadTree;
基本操作
中序前驱
//辅助全局变量,用于查找结点p的前驱
BiTNode *p; //p指向目标结点
BiTNode *pre = NULL; //指向当前访问结点的前驱
BiTNode *final = NULL;//用于记录最终结果
//土方法:在中序遍历的过程中使用pre指向当前结点的前驱结点
void findPre(BiTree T){
if (T != NULL){
findPre(T->lchild);
visit(T);
findPre(T->rchild);
}
}
void visit(BiTNode* q){
if (q == p1) //当前访问结点刚好是结点p
final = pre1; //找到p的前驱
else
pre1 = q; //pre指向当前访问的结点
}
//中序遍历二叉树,一边遍历一边线索化
ThreadNode *pre = NULL;
void InThread(ThreadTree T){
if (T != NULL){
InThread(T->lchild);
threadvisit(T);
InThread(T->rchild);
}
}
void threadvisit(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;
}
//中序线索化二叉树T
void CreateInThread(ThreadTree T) {
ThreadTree pre = NULL;
if (T != NULL){ //非空二叉树才能线索化
InThread(T, pre); //中序线索化二叉树
pre->rchild = NULL; //处理遍历的最后一个结点
pre->rtag = 1;
/*
中序遍历的最后一个结点的右孩子指针必为空
if(pre->rchild == NULL)
pre->rtag = 1;
*/
}
}
//王道
void InThreadWD(ThreadTree p, ThreadTree &pre){
if (p != 0){
InThreadWD(p->lchild,pre);
if (p->lchild == NULL){
p->lchild = pre;
p->ltag = 1;
}
if (p->rchild == NULL && pre != NULL){
p->rchild = p;
p->rtag = 1;
}
pre = p; //标记当前结点成为刚刚访问过的结点
InThreadWD(p->rchild, pre); //递归,线索化右子树
}
}
先序线索化
void PreThread(ThreadTree p,ThreadTree &pre){
if (p != NULL) {
//Previsit(T);
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(p->ltag == 0) //lchild 不是前驱线索
PreThread(p->lchild,pre);
PreThread(p->rchild,pre);
}
}
//先序线索化二叉树T
void CreatePreThread(ThreadTree T){
ThreadTree pre = NULL;
if (T != NULL) {
PreThread(T, pre);
if (T->rchild == NULL) //处理遍历的最后一个结点
pre->rtag = 1;
}
}
后序线索化
void PostThread(ThreadTree p,ThreadTree &pre) {
if (p != NULL) {
PostThread(p->lchild,pre);
PostThread(p->rchild,pre);
//visit()
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 CreatePostThread(ThreadTree T) {
ThreadTree pre = NULL;
if (T != NULL) {
PostThread(T, pre);
if (pre->rchild = NULL)
pre->rtag = 1;
}
}
线索二叉树找前驱/后继
中序线索二叉树
找后继
/*
1、若 p->rtag == 1,则next = p->rchild
2、若p->rtag == 0,【左中右】该点存在右孩子,则后继为右子树的最左结点
*/
//找到以P为根的子树中,第一个被中序遍历的结点
ThreadNode* Firstnode(ThreadNode* p) {
//循环找到最左下结点
while (p->rtag == 1)
p = p->rchild;
return p;
}
//
ThreadNode* Nextnode(ThreadNode* p){
if (p->rtag == 0)
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的中序前驱pre
1、若p->ltag == 1,则pre = p->lchild
2、若p->ltag == 0,【左根右】该点存在左孩子,则前驱为左子树的最右结点
*/
//找到以P为根的子树中,最后一个被中序遍历的结点
ThreadNode* Lastnode(ThreadNode* p){
//循环找到最右下结点(不一定是叶结点)
while (p->rtag == 0)
p = p->rchild;
return p;
}
//找前驱
ThreadNode* Prenode(ThreadNode* p){
if (p->ltag == 0)
return Lastnode(p->lchild);
else
return p->lchild;
}
//对中序线索二叉树进行逆向中序遍历
void RevInorder(ThreadNode* T){
for (ThreadNode* p = Lastnode(T); p != NULL; p = Prenode(p))
visit(p);
}
先序线索二叉树
找先序后继
/*
1、若p->rtag == 1,则p->next = p->rchild;
2、若p->rtag == 0,【根左右】
如果有左孩子,则先序后继为左孩子
如果无左孩子,则先序后继为右孩子
*/
ThreadNode* Nextnode(ThreadNode *p){
if(p->rtag == 1)
p->next = p->rchild;
if(p->rtag == 0){
if(p->ltag == 0)
return p->lchild;
else
return p->rchild;
/*
if(p->ltag == 0)
return p->next = p->lchild;
else
return p->next = p->rchild;
*/
}
找先序前驱
/*
【根左右】p的左右孩子结点在先序线索二叉树中无论如何都只能是p的后继
不可能在左右子树中找到前驱
方法1:从头开始先序遍历
方法2:如果能找到p的父结点,则根据父结点进行找后继的操作得到p的前驱
Ⅰ、p是左孩子---p的父结点即为其前驱
Ⅱ、p是右孩子且左兄弟为空---p的父结点即为其前驱
Ⅲ、p为右孩子且左兄弟非空---左兄弟子树最后一个被先序遍历的结点(左兄弟子树的最右结点)
*/
TreeNode * findPrePre(TreeNode *p){
if (p->ltag == 1)
return p->lchild;
//如果p有左孩子,pre--三叉链表父结点
TreeNode *pre = p->pre;
if (!pre)
return pre;
if (pre->lchild == p)
return pre;
pre = pre->lchild;
while (pre->rtag == 0)
pre = pre->rchild;
return pre;
}
后序线索二叉树
找后序前驱
/*
若p->ltag == 1,则pre = p->lchild;
若p->ltag == 0,【左右根】
有右孩子,后序前驱为右孩子
无右孩子,后序前驱为左孩子
*/
ThreadNode *findBackPre(TreeNode *p){
if(p->rtag == 0)
return p->rchild;
else
return p->lchild;
}
找后序后继
/*
若p->rtag == 1,则next = p->rchild
若p->rtag == 0,必有右孩子,【左右根】后序遍历中左右子树只可能是前驱不可能是后继
方法1:从头开始后序遍历
方法2:如果能找到父结点,则根据父结点进行找后继的操作得到p的后继
Ⅰ、p是右孩子---父结点为后继
Ⅱ、p是左孩子且右兄弟树为空---父结点为后继
Ⅲ、p是左孩子且右兄弟树不空---右兄弟子树中第一个被后序遍历的结点(右兄弟子树的最左结点)
Ⅳ、如果p是根节点,则p没有后继结点
*/
TreeNode *findBackBack(TreeNode *p){
if(p->rtag ==1)
return p->rchild;
TreeNode *pre = p->pre;
if(!pre || pre->rchild == p || pre->rtag == 1)
return pre;
pre = pre->rchild;
while(pre->ltag == 0)
pre = pre->lchild;
while(pre->rtag == 0)
pre = pre->rchild;
return pre;
}
树
逻辑结构
双亲表示法(顺序存储)
- 每个结点中保存指向双亲的"指针"
- 根结点固定存储在0,-1表示没有双亲
- 优:查指定结点的双亲很方便
- 缺:查指定结点的孩子只能从头遍历
- 空数据导致遍历更慢
typedef struct { //树的结点定义
ElemType data; //数据元素
int parent; //双亲位置域,int型变量,其实是双亲结点在数组中存放的位置下标
}PTNode;
typedef struct { //树的类型定义
PTNode nodes[MAX_TREE_SIZE];//双亲表示
int n; //结点数
}PTree;
/*
增:新增数据元素无需按逻辑上的次序存储,直接添加即可
删:删除后更改PTree中的结点数 n-1
PLAN1:直接将parent设置为-1
PLAN2:将尾部数据移动,填充这个空白
【注】若删除的并非叶子结点,则需要找到删除结点的子节点一并删除
*/
孩子表示法(顺序+链式存储)
- 顺序存储各个结点,每个结点中保存孩子链表头指针
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;
孩子兄弟表示法(链式存储)
- 可以使用熟悉的二叉树操作来处理树
- 树与二叉树的相互转换,本事是用孩子表示法存储树
- 森林中各个树的根结点之间视为兄弟关系
typedef struct CSNode{
ElemType data; //数据域
struct CSNode *firschild; //第一个孩子
struct CSNode *nextsibling; //右兄弟指针
}CSNode,*CSTree;
//在存储角度上,和二叉链表相同,将firschild看作左指针,nextsibling看作右指针
树、森林的遍历
树的遍历
先根遍历
若树非空,先访问根结点,再依次对每棵子树进行先根遍历
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); //访问根结点
}
}
层次遍历(用队列实现)---广度优先遍历
- 若树非空,则根结点入队
- 若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
- 重复上一步直到队列为空
森林的遍历
森林是m(m≥0)棵互不相交的树的集合。每棵树去掉根结点后,其各个子树又组成森林
先序遍历
若森林非空
- 访问森林中第一棵树的根结点
- 先序遍历第一棵树中根结点的子树森林
- 先序遍历除去第一棵树之后剩余的树狗曾的森林
效果等同于依次对各个树进行先根遍历
中序遍历
若森林非空
- 中序遍历森林中第一棵树的根结点的子树森林
- 访问中第一棵树的根结点
- 中序遍历除去第一棵树之后剩余的树狗曾的森林
效果等同于依次对各个树进行后根遍历==【依次对二叉树的中序遍历】
中序遍历
若森林非空
- 中序遍历森林中第一棵树的根结点的子树森林
- 访问中第一棵树的根结点
- 中序遍历除去第一棵树之后剩余的树狗曾的森林
效果等同于依次对各个树进行后根遍历==【依次对二叉树的中序遍历】
哈夫曼树(最优二叉树)
- 结点的权:有某种现实含义的数值(如:表示结点的重要性等)
- 结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该节点上权值的乘积
- 树的带权路径长度:树中所有叶结点的带权路径长度之和
- 构造哈夫曼树
- 构造森林
- 构造新结点,从森林中选取权值最小的两棵树作为左右子树,将这个树放入森林
- 循环
- 直至剩一个
- 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大
- 哈夫曼树的结点总数为2n-1
- 哈夫曼树中不存在度为1的结点
- 哈夫曼树并不唯一,但WPL必然相同且为最优
哈夫曼编码
将字符频次作为字符结点权值,构造哈夫曼树,即可得哈夫曼编码,可用于数据压缩
- 固定长度编码--每个字符用相等长度的二进制位表示
- 可变长度编码--允许对不同字符用不等长的二进制为表示
- 若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码
typedef struct{
unsigned int weight;
unsigned int parent,lchild,rchild;
}HTNode,*HuffmanTree;
typedef char * * HuffmanCode;
void HuffmanCoding(HuffmanTree &HT,HuffmanCode &HC,int *w,int n){
//w存放n个字符的权值(均>0),构造哈夫曼树HT,并求出n个字符的哈夫曼编码HC
if(n <= 1)
return
m = 2 * n - 1;
HT = (HuffmanTree)malloc((m+1) * sizeof(HYNode)); //0号单元未用
for(p=HT+1,i=1;i<=n;++i,++p,++w)
*p = {*w,0,0,0};
for(; i<=m;++i,++p)
*p = {0,0,0,0};
for(i=n+1;i<=m,++i){ //建哈夫曼树
//在HT[1...i-1]选择parent为0且weight最小的两个结点,其序号分别为s1和s2;
Select(HT,i-1,s1,s2);
HT[s1].parent = i;
HT[s2].parent = i;
HT[i].lchild = s1;
HT[i].rchild = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
//从叶子到根逆向求每个字符的哈夫曼编码
HC = (HuffmanCode)malloc((n+1) * sizeof(char *)); //分配n个字符编码的头指针向量
cd = (char *)malloc(n*sizeof(char)); //分配求编码的工作空间
cd[n-1] = "\0"; //编码结束符
for(i=1;i<=n;++i){ //逐个字符求哈夫曼编码
start = n - 1; //编码结束符位置
for(c=i,f=HT[s1].parent;f!=0;c=f,f=HT[f].parent) //从叶子到根逆向求编码
if(HT[f].lchild == c)
cd[--start] = "0";
else
cd[--start] = "1";
HC[i] = (char *)malloc((n-start) * sizeof(char)); //为第i个字符编码分配空间
strcpy(HC[i],&cd[start]); //从cd赋复制编码(串)到HC
}
free(cd); //释放工作空间
}//HuffmanCoding