数据结构与算法---树的学习
7.树和二叉树
7.1树的定义
树(Tree)是n(n>=0)个结点的有限集
- 若n = 0 称为空树
- 若n > 0,则它满足如下两个条件
- 有且仅有一个特定的称为根的结点
- 其余结点可分为m(m>=0)个互不相交的有限集 T1,T2,T3...Tm,其中每一个集合本身又是一棵树,并称为根的子树(SubTree)。
树的结构:
1.结点之间又分支
2.具有层次关系
树的定义是一个递归的定义
7.1.1树的基本术语
结点:数据元素以及指向子树的分支
根结点:非空树中无前驱结点的结点
结点的度:结点拥有的子树数
树的度:树内各结点的度的最大值
叶子:终端结点,无子树,度=0;
分支结点:非终端结点,根节点以外的分支结点称为内部节点
兄弟结点:拥有共同双亲的结点为兄弟结点,无同双亲的结点,但是在同一层上结点的称为堂兄弟
结点的祖先:从根到该结点所经分支上的所有结点。
结点的子孙:从某结点为根的子树中的任一结点。
树的深度:树中结点的最大层次。
有序树:树中结点的各子树从左至右有次序(最左边的为第一个孩子)
无序树:树中结点的各子树无次序
森林:是m(>=0)课互不相交的树的集合,把根结点删除了就变成了森林。
一棵树可以看成是一个特殊的森林,给森林中各子树加上一个双亲结点,森林就变成了树。
树一定是森林 森林不一定是树
结点的子树的根称为该结点的孩子,该结点称为孩子的双亲。
树结构和线性结构的比较
线性结构 | 树结构 |
---|---|
第一个数据元素:无前驱 |
根节点(只有一个):无双亲 |
最后一个数据元素:无后继 |
叶子结点(可以有多个):无孩子 |
其他数据元素: 一个前驱一个后继 |
其他节点(中间节点):一个双亲,多个孩子 |
一对一 | 一对多 |
7.1.2二叉树的定义
二叉树是n(n>=0)个结点的有限集,它或者是空集(n=0),或者由一个根节点及两颗互不相交的分别称作这个根的左子树和右子树的二叉树组成。
特点:
- 每个结点最多有俩孩子(二叉树中不存在度大于2的结点)
- 子树有左右之分,其次序不能颠倒
- 二叉树可以是空集合,根可以有空的左子树或空的右子树
注意:二叉树不是树的特殊情况,他们是两个概念
二叉树结点的子树要区分左子树和右子树,即使只有一颗子树也应该说明是左还是右。
树当结点只有一个孩子时,就不需要区分左右次序。
二叉树的每个结点位置都是固定的,可以是空,但是不能说它没有位置。
树的结点位置是相对于别的结点来说的,没有别的结点时,顺序就无所谓。
二叉树的5种基本形态:
7.2树和二叉树的抽象数据类型定义
因为树的操作很多,下面只介绍重要的几个操作
CreateBiTree(&T,definition){ 初始条件: definition给出二叉树T的定义 操作结果: 按definition构造二叉树 } PreOrderTraverse(T){ 初始条件: 二叉树T存在 操作结果: 先序遍历T,对每个节点访问一次。 } InOrderTraverse(T){ 初始条件: 二叉树T存在 操作结果: 中序遍历T,对每个节点访问一次。 } PostOrderTraverse(T){ 初始条件: 二叉树T存在 操作结果: 后序遍历T,对每个节点访问一次。 } Assign(T,cur_e){ 初始条件: 树T存在,cur_e是T中某个结点 操作结果: 结点cur_e赋值为value }
7.3二叉树的性质
性质1:
在二叉树的第i层上至多有2i-1个结点(i>=1),第i层上最少有1个结点;
性质2:
深度为k的二叉树至多有2k-1个结点(k>=1),最少有k个结点;
性质3:
对任何一颗二叉树T,如果其叶子数为n0,度为2的结点为n2,则n0 = n2+1;
两种特殊形式的二叉树
满二叉树:
一颗深度为k且有2k-1个结点的二叉树称为满二叉树;
特点:
1.每一层上的结点数都是最大结点数(即每层都满)
2.叶子节点全部都在最底层
对满二叉树结点位置进行编号
-
编号规则:从根节点开始,自上而下,自左而右
-
每一结点位置都有元素
满二叉树在同样深度的二叉树中结点个数最多
满二叉树在同样深度的二叉树中叶子结点个数最多
完全二叉树
深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树
注:在满二叉树中,从最后一个结点开始,连续
去掉任意
个结点,即使一颗完全二叉树.
特点:
- 叶子只可能分布在层次最大的两层上。
- 对任一结点,如果其右子树的最大层次为i,则其左子树的最大层次必为 i 或 i + 1 .
满二叉树一定是完全二叉树
完全二叉树不一定是满二叉树
性质4:
具有n个结点的完全二叉树的深度为⌊ log2n ⌋ + 1
注:⌊ x ⌋:称为x的底,表示小于x的最大整数,也叫向下取整。 ⌊ 3.5 ⌋ = 3
⌈ x ⌉ 表示大于x的最小整数,向上取整(取顶) ⌈ 3.5 ⌉ = 4
性质5:
如果有一棵有n个结点的完全二叉树(深度为⌊ log2n ⌋ + 1)的结点按层序编号(从第1层到第⌊ log2n ⌋ + 1层,每层从左到右),则对任一结点 i (1<=i<=n)有:
- 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是节点⌊ i / 2 ⌋。
- 如果2i > n,则结点i为叶子结点,无左孩子;否则其左孩子是节点2i。
- 如果2i + 1 > n,则结点 i 无右孩子;否则,其右孩子是结点2i + 1。
7.4二叉树的存储结构
7.4.1二叉树的顺序存储
实现:按满二叉树的结点层次编号,一次存放二叉树中的数据元素;
//二叉树的顺序存储表示 #define MAXSIZE 100 Typedef TElemType SqBiTree[MAXSIZE] SqBiTree bt; //bt是个数组,最多可以存放100个元素
二叉树的顺序存储缺点:
最坏情况:深度为k的且只有k个结点的单支树需要长度为2k-1的以为数组
特点:
结点间关系蕴含在其存储位置中;
浪费空间,适用于存满二叉树和完全二叉树;
7.4.2 二叉树的链式存储结构
二叉链表的存储结构
typedef struct BiNode{ TElemType data; struct BiNode *lchild, *rchild; //左右孩子指针 }BiNode,*BiTree;
在n个结点的二叉链表中,有n + 1个空指针域
分析:总共有n个结点,所以必有2n个链域。除根节点外,每个结点有且仅有一个双亲,所以只会有n - 1个结点的链域存放指针,指向非空子女结点。
空指针数目= 2n - (n - 1) = n + 1;
三叉链表
typedef struct BiNode{ TElemType data; struct BiNode *lchild, *parent,*rchild;//左右孩子指针和双亲指针 }BiNode,*BiTree;
7.5 🔺二叉树的遍历
- 遍历定义——顺着某一条搜索路径巡防二叉树中的结点,使得每个结点均被访问一次,而且仅仅被访问一次(又称周游)。
- “访问”的含义很广,可以是对结点作各种处理,如:输出结点的信息,修改结点的数据值等,但要求这种访问不能破坏原来的数据结构。
- 遍历目的——得到树中所有结点的一个线性排列。
- 遍历用途——它是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心。
7.5.1 遍历二叉树算法描述
依次遍历二叉树中的三个组成部分,就是遍历了整个二叉树
设: L:遍历左子树 D:访问根节点 R:遍历右子树
先左后右,现在就只有三种情况
DLR——先 (根) 序遍历
LDR——中 (根) 序遍历
LRD——后 (根) 序遍历
先序遍历二叉树 | 中序遍历二叉树 | 后序遍历二叉树 |
---|---|---|
若二叉树为空,则空操作 否则 执行下面操作 | 若二叉树为空,则空操作 否则 执行下面操作 | 若二叉树为空,则空操作 否则 执行下面操作 |
1)访问根节点 | 1)中序遍历左子树 | 1)后序遍历左子树 |
2)先序遍历左子树 | 2)访问根节点 | 2)后序遍历右子树 |
3)先序遍历右子树 | 3)中序遍历右子树 | 3)访问根节点 |
由二叉树的递归定义可知,遍历左子树和遍历右子树可如同遍历二叉树一样 “递归”
进行。
7.5.2 根据遍历序列确定二叉树
- 若二叉树中各结点的值均不相同,则二叉树结点的先序序列,中序序列和后序序列都是唯一的。
- 由二叉树的先序序列和中序序列,或者二叉树的后序序列和中序序列可以确定唯一一颗二叉树。
7.5.3 遍历的算法实现
7.5.3.1先序遍历
若二叉树为空,则空操作;
若二叉树非空{
访问根节点(D);
前序遍历左子树(L);
前序遍历右子树(R);
}
二叉树先序遍历算法
Status PreOrderTraverse(BiTree T){ if(T == NULL) return OK; //空二叉树 else{ visit(T); //访问根节点 例如:输出根节点printf("%d\t",T->data); PreOrderTraverse(T->lchild); //递归遍历左子树 PreOrderTraverse(T->rchild); //递归遍历右子树 } }
7.5.3.2 中序遍历
若二叉树为空,则空操作;
否则:
中序遍历左子树(L);
访问根节点(D);
中序遍历右子树(R);
Status InOrderTraverse(BiTree T){ if(T == NULL) return OK; //空二叉树 else{ InOrderTraverse(T->lchild); //递归遍历左子树 visit(T); //访问根节点 例如:输出根节点printf("%d\t",T->data); InOrderTraverse(T->rchild); //递归遍历右子树 } }
完整代码示例:
//一棵二叉树, 如上图所示。分别进行先序遍历、中序遍历、后序遍历,并输出遍历序列。 #include <iostream> using namespace std; struct BTNode { //树的结点结构,二叉链表表示法 int data; BTNode *lchild, *rchild; }; BTNode *create_BTNode(int data) { //为data生成一个结点 BTNode *p = new BTNode; p->data = data; p->lchild = p->rchild = 0; return p; } void merge_BItree(BTNode *parent, BTNode *lchild, BTNode *rchild) { //将parent、lchild、rchild三个结点合并起来,形成树形结构 parent->lchild = lchild; parent->rchild = rchild; } void PreOrderTraverse(BTNode *t) { //先序遍历二叉树的递归算法 if (t != 0) { cout << t->data << " "; PreOrderTraverse(t->lchild); PreOrderTraverse(t->rchild); } } void InOrderTraverse(BTNode *t) { //中序遍历二叉树的递归算法 if (t != 0) { InOrderTraverse(t->lchild); cout << t->data << " "; InOrderTraverse(t->rchild); } } void PostOrderTraverse(BTNode *t) { //后序遍历二叉树的递归算法 if (t != 0) { PostOrderTraverse(t->lchild); PostOrderTraverse(t->rchild); cout << t->data << " "; } } int main() { //建立一棵二叉树,进行遍历操作; BTNode *a, *b, *c, *d, *e, *f, *g; a = create_BTNode(1); b = create_BTNode(2); c = create_BTNode(3); d = create_BTNode(4); e = create_BTNode(5); f = create_BTNode(6); g = create_BTNode(7); //将各结点进行连接,形成一棵二叉树 merge_BItree(e, 0, g); merge_BItree(d, e, f); merge_BItree(b, c, d); merge_BItree(a, b, 0); cout << "先序遍历: " << endl; PreOrderTraverse(a); //先序遍历 cout << endl; cout << "中序遍历: " << endl; InOrderTraverse(a); //中序遍历 cout << endl; cout << "后序遍历: " << endl; PostOrderTraverse(a); //后序遍历 cout << endl; }
7.5.3.3 后序遍历
若二叉树为空,则空操作;
否则:
后序序遍历左子树(L);
后序序遍历右子树(R);
访问根节点(D);
Status PostOrderTraverse(BiTree T){ if(T == NULL) return OK; //空二叉树 else{ PostOrderTraverse(T->lchild); //递归遍历左子树 PostOrderTraverse(T->rchild); //递归遍历右子树 visit(T); //访问根节点 例如:输出根节点printf("%d\t",T->data); } }
遍历算法的分析:
- 时间效率: O(n) //每个结点只访问一次;
- 空间效率:O(n) //栈占用最大辅助空间
7.5.4遍历二叉树的非递归算法
7.5.4.1中序遍历非递归算法
基本思想:
- 建立一个栈;
- 根节点进栈,遍历左子树;
- 根节点出栈,输出根节点,遍历右子树;
算法实现:
//用二叉链表来实现 Status InOrderTraverse(BiTree T){ BiTree p; //指针变量P; InitStack(S); //初始化栈S; p=T; //将指针变量P指向二叉树的根节点; while( p || !StackEmpty(S) ){ //如果p指向的节点非空或者栈内非空,执行循环; if( p ){ Push(S,p); //入栈; p = p->lchild; //入栈后将p指向入栈结点的左子树; }else{ Pop(S,q); //出栈,并且将出栈的结点赋给q; printf("%d\t",q->data); //输出出栈结点q的元素值; p = q->rchild; //将p指向出栈结点q的右孩子; } } return OK; }
7.5.5二叉树的层次遍历
对于一棵二叉树,从根节点开始,按从上到下、从左到右的顺序一层层来遍历访问每一个结点,每一个结点仅仅访问一次;
算法设计思路:使用一个队列
;
- 将根节点进队;
- 队不空时循环:从队列中出列一个结点*p,访问它;
- 若它有左孩子结点,将左孩子结点进队;
- 若它有右孩子结点,将右孩子结点进队;
大致思路如下,以下图为例:
- 先将根节点a入队,然后去寻找a结点的左右孩子;
- 找到a的左右孩子时,a出队,然后b和f入队; //第一层结束;
- 这时队列中有b和f,然后去找b的左右孩子;
- 找到后b出队,c和d入队;
- 然后f出队,将f的左孩子g入队; //第二层结束;
- 这时队列中还有c、d、g;
- 将c出队,找c的左右孩子,但是没有,然后将d出队,将d孩子e入队;
- 然后g出队,将g的孩子h入队; //第三层结束
- 依次类推,e和h出队;
- 出队顺序就是 a,b,f,c,d,g,e,h;
层次遍历队列类型定义如下:
typedef struct(){ BTNode data[MaxSize]; //存放队中元素; int front,rear; //队头队尾指针 }SqQueue; //顺序循环队列类型;
二叉树层次遍历算法:
void LevelOrder(BTNode *b){ BTNode *p; SqQueue *qu; InitQueue(qu); //初始化队列 enQueue(qu, b); //根节点指针进入队列 while (!QueueEmpty(qu)){ //队不为空,则循环 deQueue(qu, p); //出队结点p printf("%c ", p->data); //输出结点p的元素值 if(p->lchild != NULL){ enQueue(qu, p->lchild); //有左孩子将其进队 } if(p->rchild != NULL){ enQueue(qu, p->rchild); //有右孩子将其进队 } } }
7.5.6 二叉树的算法应用
7.5.6.1二叉树的建立算法
按先序遍历序列建立二叉树的二叉链表
例如:已知先序序列为:
ABCDEGF
- 从键盘输入二叉树的结点信息,建立二叉树的存储结构;
- 在建立二叉树的过程中按照二叉树先序方式建立;
下图所示二叉树,按下列顺序读入字符,中间空的都用#来表示:ABC##DE#G##F###
二叉树的建立算法:
Status CreateBiTree(BiTree &T){ scanf(&ch); //或者cin>>ch; if(ch == '#') T = NULL; else{ if(!(T = (BiTNode *) malloc(sizeof(BiTNode)))){ //或者C++中T=new BiTNode; exit(OVERFLOW); } T->data = ch; CreateBiTree(T->lchild); //构造左子树 CreateBiTree(T->rchild); //构造右子树 } return OK; }
7.5.6.2 复制二叉树
- 如果是空树,递归结束;
- 否则,申请新结点空间,复制根节点(也算是先序复制)
- 递归复制左子树;
- 递归复制右子树;
算法实现:
int Copy(BiTree T, BiTree &NewT){ if(T == NULL){ //如果是空树返回0; NewT = NULL; return 0; }else{ NewT = new BiTNode; NewT->data = T->data; Copy(T->lchild, NewT->lchild); Copy(T->rchild, NewT->rchild); } }
7.5.6.3 计算二叉树的深度
- 如果是空树,则深度为0;
- 否则,递归计算左子树的深度记为m,递归计算右子树的深度记为n,二叉树的深度则为m与n的较大者+1;
伪代码实现: 计算二叉树的深度是在后序遍历二叉树的基础上进行的运算;
int Depth( BiTree T ){ if(T == null ) return 0; //如果是空树返回0; else{ int m = Depth(T->lChild); //递归计算左子树的深度记为m; int n = Depth(T->rChild); //递归计算左子树的深度记为n; if(m>n){ //为什么要加1,因为算出来以后还要加上根节点; return (m+1); }else{ return (n+1); } } }
7.5.6.4 计算二叉树结点总数
- 如果是空树,则结点个数为0;
- 否则,结点个数为左子树的结点个数+右子树的结点个数再+根节点的1;
伪代码实现:
int NodeCount(BiTree T){ if(T == null) return 0; else{ return NodeCount(T->lchild) + (T->rchild) + 1; } }
7.5.6.5 计算二叉树叶子结点总数
- 如果是空树,则叶子结点个数为0;
- 否则,为左子树的叶子结点个数+右子树上的叶子结点个数;
伪代码实现:
int LeadCount(BiTree T){ if(T == null) //如果是空树返回0 return 0; if(T->lchild == null && T->rchild == null) return 1; //如果是叶子结点返回1 else{ return LeadCount(T->lchild) + LeadCount(T->rchild); } }
完整代码实现:
//编程实现:创建一棵二叉树, //如上图所示,然后统计叶子结点的数目,并求树的高度。 #include "stdlib.h" #include <iostream> using namespace std; typedef char TElemType; typedef struct BiTNode { TElemType data; struct BiTNode *lchild, *rchild; //左右孩子指针 } BiTree; BiTree *CreBiTree() { BiTree *bt; TElemType x; scanf("%c", &x); if (x == '%') { bt = NULL; //空指针 } else { //建立根结点 bt = (BiTree *)malloc(sizeof(BiTree)); bt->data = x; bt->lchild = CreBiTree(); //构造左子树链表,并将左子树根结点指针赋给根结点的左孩子域; bt->rchild = CreBiTree(); //构造右子树链表,并将右子树根结点指针赋给根结点的右孩子域; } return bt; //返回根结点的指针; } int LeafCount = 0; int LeafNum(BiTree *bt) //按照前序遍历方式统计叶子结点数目 { if (bt != NULL) { if (bt->lchild == NULL && bt->rchild == NULL) { LeafCount++; } LeafNum(bt->lchild); LeafNum(bt->rchild); } return LeafCount; } int HighTree(BiTree *bt) { //求树的高度 int h, hl, hr; if (bt == NULL) { h = 0; } else { hl = HighTree(bt->lchild); hr = HighTree(bt->rchild); h = (hl > hr ? hl : hr) + 1; } return h; } int main(int argc, char *argv[]) { BiTree *BT; cout << "Create a binary tree:" << endl; BT = CreBiTree(); //建立一棵二叉树,输入ABC%%DE%G%%F%%% %表示结束符 cout << "TheBiRree leaves number: " << LeafNum(BT) << endl; cout << "The BiTree high: " << HighTree(BT) << endl; return 0; }
7.6 线索二叉树
利用二叉链表中的空指针域:
如果某个结点的左孩子为空,则将空的左孩子指针域改为指向其前驱;如果某结点的右孩子为空,则将空的右孩子指针域改为指向其后继;
这种改变指向的指针称为线索;
加上了线索的二叉树称为线索二叉树,对二叉树按某种遍历次序使其变为线索二叉树的过程叫线索化;
为区分lchild和rchild指针到底是指向孩子的指针还是指向前驱或者后继的指针,对二叉链表中每个结点增设两个标志域ltag和rtag,并约定:
ltag = 0;lchild指向该结点的左孩子;
ltag = 1;lchild指向该结点的前驱;
rtag = 0;rchild指向该结点的右孩子;
rtag = 1;rchild指向该结点的后继;
存储结点结构:
typedef struct BiThrNode { int data; int ltag,rtag; struct BiThrNode *lchild,rchild; }BiThrNode,*BiThrTree;
## 7.7 树和森林
树(Tree)是n(n>=0)个结点的有限集
- 若n = 0 称为空树
- 若n > 0,则它满足如下两个条件
- 有且仅有一个特定的称为根的结点
- 其余结点可分为m(m>=0)个互不相交的有限集 T1,T2,T3...Tm,其中每一个集合本身又是一棵树,并称为根的子树(SubTree)。
森林是m(m>=0)棵互不相交的树的集合;
7.7.1树的存储结构
1.双亲表示法
实现:定义结构数组,存放树的结点,每个结点含两个域;
- 数据域:存放结点本身信息;
- 双亲域:之时本结点的双亲结点在数组中的位置;
特点:找双亲容易,找孩子难;
类型描述:
typedef struct PTNode { TElemType data; int parent; //双亲位置域 }PTNode; 树结构: #define MAX_TREE_SIZE 100 typedef struct { PTNode nodes[MAX_TREE_SIZE]; int r,n; //根结点的位置和结点个数; }PTree;
2.孩子链表
把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存,则n个结点有n个孩子链表(叶子的孩子链表为空表)。而n个头指针又组成一个线性表,用顺序表(含n个元素的结构数组)存储。
类型描述:
孩子结点结构:child next typedef struct CTNode { int child; struct CTNode *next; }*ChildPtr;
双亲结点结构: data firstchild typedef struct { TElemType data; ChildPtr firstchild; //孩子链表头指针 }CTBox;
树结构: #define MAX_TREE_SIZE 100 typedef struct { CTBox nodes[MAX_TREE_SIZE]; int r,n; //根结点的位置和结点个数; }CTree;
特点:找孩子容易,找双亲难;
3.孩子兄弟表示法
二叉树表示法,二叉链表表示法
实现:用二叉链表作为树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点;
存储结构:
typedef struct CSNode { ElemType data; struct CSNode *firstchild,*nextsibling; }CSNode,*CSTree;
7.7.2 树和二叉树以及森林的转换
给定一棵树,可以找到唯一的一棵二叉树与之对应;
将树转换成二叉树
- 加线:在兄弟之间加一条线
- 抹线:对每个结点,除了左孩子外,去除其与其余孩子之间的关系
- 以树的根结点为轴心,将整树顺时针转45°。
口诀:树变二叉树:
兄弟相连留长子
7.7.2.1 二叉树转换成树
- 加线:若p结点时双亲结点的左孩子,则将p的右孩子,右孩子的右孩子...沿分支找到所有右孩子,都与p的双亲用线连起来;
- 抹线:抹掉原二叉树中双亲与右孩子之间的连线;
- 调整:将结点按层次排列,形成树结构;
口诀:二叉树变树
左孩右右连双亲,去掉原来右孩线
7.7.2.2 森林与二叉树的转化
- 森林转换为二叉树(二叉树与多棵树之间的关系)
- 将各棵树分别转换成为二叉树
- 将每棵树的根结点用线相连
- 以第一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转,构成二叉树结构
口诀:森林变二叉树:
树变二叉根相连
7.7.2.3 二叉树转换为森林
- 抹线:将二叉树中根结点与其右孩子的连线,及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二叉树;
- 还原:将孤立的二叉树还原成树;
口诀: 二叉树变森林
去掉全部右孩线,孤立二叉再还原
7.7.3 树与森林的遍历
1.树的遍历(三种方式)
- 先根(次序)遍历:
- 若树不空,则先访问根结点,然后依次先根遍历各棵子树;
- 后根遍历:
- 若树不空,则先依次后根遍历各棵子树,然后访问根结点;
- 按层次遍历:
- 若树不空,则自上而下自左至右访问树中每个结点;
2.森林的遍历
将森林看作由三部分构成:
- 森林中第一棵树的根结点;
- 森林中第一棵树的子树森林
- 森林中其他树构成的森林;
先序遍历:
若森林不空,则:
- 访问森林中第一棵树的根结点;
- 先序遍历森林中第一棵树的子树森林;
- 先序遍历森林中(除第一棵树之外)其余树构成的森林;
即:依次从左至右对森林中的每一棵树进行先根遍历
中序遍历:
若森林不空,则:
- 中序遍历森林中第一棵树的子树森林;
- 访问森林中第一棵树的根结点;
- 中序遍历森林中(除第一棵树之外)其余树构成的森林;
即:依次从左至右对森林中的每一棵树进行后根遍历
7.8 哈夫曼树
7.8.1 哈夫曼树的基本概念
路径:从树中的一个结点到另一个结点之间的分支就构成这两个结点间的路径;
结点的路径长度:两结点间路径上的分支数;
(a)从A到B,C,D,E,F,G,H,I的路径长度分别为:1、1、2、2、3、3、4、4
(b)从A到B,C,D,E,F,G,H,I的路径长度分别为:1、1、2、2、2、2、3、3
树的路径长度:从树根到每一个结点的路径长度之和。
结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树
权(weight):将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权;
结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积;
树的带权路径长度:树中所有叶子结点的带权路径长度之和。
哈夫曼树:最优树 带权路径长度(WPL)最短的树
注:带权路径长度最短时在度相同的树中比较而得出的结果,因此有最优二叉树,最优三叉树之称等等;
哈夫曼树:最优二叉树 带权路径长度(WPL)最短的二叉树
满二叉树不一定是哈夫曼树
哈夫曼树中权值越大离根越近
具有相同带权结点的
7.8.2 哈夫曼树的构造算法
1)根据n个给定的权值{w1,w2,....wn}构成n棵二叉树的森林F={T1,T2,....,Tn},其中Ti只有一个带权的wi的根结点;
2)在F中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和;
3)在F中删除这两棵树,同时将新的到的二叉树加入到森林中;
4)重复(2)(3),直到森林中只有一棵树为止,这棵树即为哈夫曼树;
口诀:
1)构造森林全是根
2)选用两小造新树
3)删除两小添新人
4)重复2、3剩单根
哈夫曼树的结点的度数只有0和2,没有度为1的结点;
包含n个叶子结点的哈夫曼树中共有2n-1个结点;
包含n棵树的森林要经过n-1次合并才能形成哈夫曼树,共产生n-1个新结点
总结:
- 在哈夫曼算法中,初始时有n棵二叉树,要经过n-1次合并最终形成哈夫曼树;
- 经过n-1次合并产生n-1个新结点,且这n-1个新结点都是具有两个孩子的分支结点;
可见:哈夫曼树中共有n+n-1 = 2n-1个结点,且其所有的分支节点的度均不为1;
7.8.3 哈夫曼树构造算法的实现
采用顺序存储结构----一维结构数组
结点类型定义:
typedef struct { int weight; int parenet,lch,rch; }HTNode, *HuffmanTree;
例:有n=8,权值为w={7,19,2,6,32,3,21,10},构造哈夫曼树
weight | parent | lch | rch | |
---|---|---|---|---|
1 | 7 | 11 | 0 | 0 |
2 | 19 | 13 | 0 | 0 |
3 | 2 | 9 | 0 | 0 |
4 | 6 | 10 | 0 | 0 |
5 | 32 | 15 | 0 | 0 |
6 | 3 | 9 | 0 | 0 |
7 | 21 | 13 | 0 | 0 |
8 | 10 | 11 | 0 | 0 |
9 | 5 | 10 | 3 | 6 |
10 | 11 | 12 | 4 | 9 |
11 | 17 | 12 | 1 | 8 |
12 | 28 | 14 | 10 | 11 |
13 | 40 | 15 | 2 | 7 |
14 | 60 | 15 | 5 | 12 |
15 | 100 | 0 | 13 | 14 |
> 哈夫曼树构造算法的实现: > > 1.初始化HT[1......2n-1]:lch=rch=parent=0; > > 2.输入初始n个叶子结点:置HT[1....n]的weight值 > > 3.进行以下n-1次合并,依次产生n-1个结点HT[i],i=n+1....2n-1: > > a) 在HT[i...i-1]中选两个未被选过(从parent == 0的结点中选)的weight最小的两个结点HT[s1]和HT[S2],s1、s2为两个最小结点的下标; > > b) 修改HT[s1]和HT[s2]的parent值:HT[s1].parent=i;HT[s2].parent=i; > > c) 修改新产生的HT[i]:、 > > HT[i].weight = HT[s1].weigth + HT[s2].weight; > > HT[i].lch = s1; HT[i].rch = s2;
void CreatHuffmanTree(HuffmanTree HT,int n){//构造哈夫曼树---哈夫曼算法 if(n<=1) return; m=2*n-1; //数组共2n-1个元素 HT=new HTNode[m+1]; //0号单元未用,HT[m]表示根结点; for(i=1;i<=m;++i){ //将2n-1个元素的lch、rch、parent置为0 HT[i].lch=0; HT[i].rch=0; HT[i].parent=0; } for(i=1;i<=n;++i) cin>>HT[i].weigth; //输入前n个元素的weight值 //初始化结束,下面开始建立哈夫曼树 for(i=n+1;i<=m;i++){ //合并产生n-1个结点--构造哈夫曼树 Select(HT,i-1,s1,s2); //在HT[k](1<=k<=i-1)中选择两个其双亲域为0,且权值最小的结点,并返回他们在HT中的序号s1和s2 HT[s1].parent=i; HT[s2].parent=i; //表示从F中删除s1,s2 HT[i].lch=s1; HT[i].rch=s2; //s1、s2分别作为i的左右孩子 HT[i].weight=HT[s1].weight+HT[s2].weight; //i的权值为左右孩子权值之和 } }
7.8.4 哈夫曼编码
问题:什么样的前缀编码能使得电文总长最短? ---哈夫曼编码
方法:
- 统计字符集中每个字符在电文中出现的平均概率(概率越大,要求编码越短).
- 利用哈夫曼树的特点:权越大的叶子离根越近;将每个字符的概率值作为权值,构造哈夫曼树。则概率越大的结点,路径越短。
- 在哈夫曼树的每个分支上标上0或1:
- 结点的左分支0,右分支标1;
- 把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码。
两个问题:
- 为上面哈夫曼编码能保证时前缀编码?
- 因为没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其他叶结点编码的前缀;
- 为什么哈夫曼编码能够保证字符编码总长最短?
- 因为哈夫曼树的带权路径长度最短;
性质1:哈夫曼编码是前缀码
性质2:哈夫曼编码是最优前缀码
#### 7.8.4.1 哈夫曼编码的算法实现
void CreatHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n) {//从叶子到根逆向求每一个字符的哈夫曼编码,存储在编码表HC中 HC = new char *[n+1]; //分配n个字符编码的头指针矢量 cd = new char[n]; //分配临时存放编码的动态数组空间 cd[n-1] = '\0'; //编码结束符 for(int i=1;i<=n;++i){ //逐个字符求哈夫曼编码 start = n-1; c=i; f=HT[i].parent; while(f!=0){ //从叶子结点开始向上回溯,直到根结点 --start; //回溯一次start向前指一个位置 if(HT[f].lchild == c) cd[start] = '0';//结点c时f的左孩子,则生成代码0 else cd[start] = '1'; //结点c时f的右孩子,则生成代码1 c=f; f=HT[f].parent; //继续向上回溯 } //求出第i个字符的编码 HC[i] = new char[n-start]; //为第i个字符串编码分配空间 strcpy(HC[i],&cd[start]);//将求得的编码从临时空间cd复制到HC的当前行中 } delete cd; //释放临时空间 }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)