✍、目录脑图
1、线索二叉树
普通二叉树有如下两个问题:
-
普通二叉树遍历只能从根节点开始遍历,不能从一个指定结点开始中序遍历。
-
普通二叉树中如果只知道指向当前结点的指针,没法找到当前结点在中序遍历序列的前驱。(例如下图中只知道指向 F 结点的指针 p,如何找到结点在中序遍历序列的前驱呢?)
那么我们如何解决上述问题呢?
思路:
- 从根结点出发,重新进行一次中序遍历,指针 q 记录当前访问的结点,指针 pre 记录上一个被访问的结点(前驱)
- 下一个结点被访问(visit)之前,我们需要将 pre 前驱指针指向 q 所指向的结点,然后 q 指针指向下一个要访问的结点
- 所以现在 pre 指针所指向的结点就是 q 指针所指向结点的中序遍历的前驱。用这样的思路我们可以让 q 不断的指向后一个被访问的结点,然后 pre 也跟着依次的向后移动。
- 当 q 和 p 指向了同一个结点,也就是 q == p,那么就说明 pre 所指向的结点就是 p 所指向结点的前驱
- 继续 pre 向后移, q 也向后移。此时 pre == p,则 q 所指向的结点就是 p 所指向结点的后继
上述的操作是非常不方便的,线索二叉树就是为了解决上述问题的。
- n 个结点的二叉树,有 n + 1 个空链域。我们可以利用这些空链域来记录前驱、后继的信息。
- 如上图,D 的左子树是空链域,因为 D 没有前驱结点,是第一个结点,所以让其左子树指向NULL
- G 的左右子树都是空链域,让左子树指向其前驱结点 D,让右子树指向其后继结点 B
- E 的左右子树都是空链域,让左子树指向其前驱结点 B,让右子树指向其后继结点 A
- F 的左右子树都是空链域,让左子树指向其前驱结点 A,让右子树指向其后继结点 C
- C 的右子树是空链域,因为 C 没有后继结点,是最后一个结点,所以让其右子树指向 NULL
定义:一个结点的左孩子指针和右孩子指针指向的是前驱和后继而不是左右孩子的话,我们把这种类型的指针称为线索,指向前驱的是前驱线索,指向后继的是后继线索。
那么还存在一个问题,如果我们的右孩子指针指向的就是右孩子而不是后继,那么如何找后继呢?
1.1、线索二叉树的存储结构
普通二叉树的链式存储结点:
// 二叉树的结点(链式存储)
typedef struct BiTNode {
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
线索二叉树的链式存储结点:
// 线索二叉树的结点
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag; // 左、右线索标志
}ThreadNode,*ThreadTree;
1.1.1、中序线索二叉树的存储
1.2、先序线索二叉树
1.3、后序线索二叉树
1.4、三种线索二叉树的对比
- 中序线索二叉树 ➡ 线索指向中序前驱、中序后继
- 先序线索二叉树 ➡ 线索指向先序前驱、先序后继
- 后序线索二叉树 ➡ 线索指向后序前驱、后序后继
2、二叉树的线索化
2.1、中序线索化
中序遍历二叉树,一边遍历一边线索化
- 首先中序遍历左子树,首先访问到的是 D 结点,D 结点没有前驱,所以将 pre 指针指向NULL,并将标志修改为 1 (代表这个孩子指针是线索)
- 之后访问下一个结点,pre 指向 q 指向的结点,q 指向下一个结点
- 此时 q 指向的结点为 G,判断左子树,如果为空,则建立前驱线索并修改标志为 1 ,判断其右子树,如果为空,则建立后继线索并修改标志为 1
- 当访问最后一个结点时,pre 和 q 均指向最后一个结点,要检查 pre-> rchild 是否为 NULL,如果是,则令标志为 1
// 线索二叉树的结点
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag; // 左、右线索标志
}ThreadNode,*ThreadTree;
// 中序遍历二叉树,一边遍历一边线索化
void InThread(ThreadTree T){
if(T != NULL){
InThread(T->lchild); // 中序遍历左子树
visit(T); // 访问根节点
InThread(T->rchild); // 中序遍历右子树
}
}
// 全局变量 pre,指向当前访问结点的前驱(开始访问第一个结点,无前驱,所以指向NULL)
ThreadNode *pre = NULL;
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;
}
// 中序线索化二叉树T
void CreateInThread(ThreadTree T){
pre=NULL; // pre初始为NULL
if(T != NULL){ // 非空二叉树才能线索化
InThread(T); // 中序线索化二叉树
if(pre->rchild == NULL){
pre->rtag=1; // 处理遍历的最后一个结点
}
}
}
2.2、先序线索化
先序遍历二叉树,一边遍历一边线索化
- 首先访问根结点,判断左右子树,发现左右子树都非空,则不操作,让 pre 指向 q所指向的结点,q指向下一结点
- 访问结点 B,发现 B 的左右子树都非空,则不操作,让 pre 指向 q所指向的结点,q指向下一结点
- 之后访问 D 结点,发现其左子树为空,则建立前驱线索(将其左子树指向前驱结点B),之后我们就要先序遍历 D结点的左子树,但是发现此时左子树为 B,这样下去就会造成循环。所以我们在先序遍历二叉树时,要让左子树不是前驱线索时再进行
// 先序遍历二叉树,一边遍历一边线索化
void PreThread(ThreadTree T){
if(T != NULL){
visit(T); // 先处理根结点
if(T->ltag == 0){ // lchild不是前驱线索
PreThread(T->lchild);
}
PreThread(T->rchild); // 先序遍历右子树
}
}
// 全局变量 pre,指向当前访问结点的前驱
ThreadNode *pre = NULL;
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;
}
// 先序线索化二叉树T
void CreateInThread(ThreadTree T){
pre=NULL; // pre初始为NULL
if(T != NULL){ // 非空二叉树才能线索化
InThread(T); // 中序线索化二叉树
if(pre->rchild == NULL){
pre->rtag=1; // 处理遍历的最后一个结点
}
}
}
2.3、后序线索化
// 线索二叉树的结点
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag; // 左、右线索标志
}ThreadNode,*ThreadTree;
// 后序遍历二叉树,一边遍历一边线索化
void PostThread(ThreadTree T){
if(T != NULL){
PostThread(T->lchild); // 中序遍历左子树
PostThread(T->rchild); // 中序遍历右子树
visit(T); // 访问根节点
}
}
// 全局变量 pre,指向当前访问结点的前驱(开始访问第一个结点,无前驱,所以指向NULL)
ThreadNode *pre = NULL;
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;
}
// 后序线索化二叉树T
void CreateInThread(ThreadTree T){
pre=NULL; // pre初始为NULL
if(T != NULL){ // 非空二叉树才能线索化
InThread(T); // 中序线索化二叉树
if(pre->rchild == NULL){
pre->rtag=1; // 处理遍历的最后一个结点
}
}
}
2.4、总结
3、线索二叉树找前驱/后继
3.1、中序线索二叉树找中序后继
例如:我们要在中序线索二叉树中找到指定结点 *p 的中序后继 next,next = p的右子树中最左下结点
- 若 p -> rtag == 1, 说明右子树被线索化,那么右子树就是其中序后继,则 next = p -> rchild
- 若 p-> rtag == 0,说明右子树未被线索化,右子树肯定是非空,则 p 必然有右孩子。
- 因为是中序遍历,那么右孩子中第一个被中序遍历的结点必然是 p 的中序后继。
- 假设 p 只有 1 个右孩子,并且这个右孩子只有一个结点,那么这个结点就是 p 的后继
- 假设 p 右孩子有下一层,那么下一层最先被访问的结点(最左下结点),就是 p 的后继
- 所以中序后继 next = p的右子树中最左下结点
// 找到以 P 为根的子树中,第一个被中序遍历的结点
ThreadNode *Firstnode(ThreadNode *p){
// 循环找到最左下结点(不一定是叶结点)
while(p->ltag == 0){
p = p->lchild;
}
return p;
}
// 在中序线索二叉树中找到结点 p 的后继结点
ThreadNode *Nextnode(ThreadNode *p){
// 右子树最左下结点(右子树当中第一个被遍历到的结点)
if(p->rtag == 0){
return Firstnode(p->rchild);
}else{
return p->rchild; // rtag == 1 直接返回后继线索
}
}
既然我们能遍历到结点的后继结点,那么我们就可以对中序线索二叉树进行遍历
// 对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
// 传入我们要遍历的树的根节点的指针T
void Inorder(ThreadNode *T){
for(ThreadNode *p = Firstnode(T);p != NULL;p=Nextnode(p)){
visit(p);
}
}
3.2、中序线索二叉树找中序前驱
例如:我们要在中序线索二叉树中找到指定结点 *p 的中序前驱pre,pre= p的左子树中最右下结点
-
若 p -> ltag == 1, 说明左子树被线索化,那么左子树就是其中序后继,则 pre = p -> lchild
-
若 p-> ltag == 0,说明左子树未被线索化,那么左子树肯定非空,则 p 必然有左孩子。
-
因为是中序遍历,那么左孩子中最后一个中序遍历的结点必然是 p 的中序前驱。
-
假设 p 只有 1 个左孩子,并且这个左孩子只有一个结点,那么这个结点就是 p 的前驱
-
假设 p 左孩子有下一层,那么下一层最后访问的结点(最右下结点),就是 p 的前驱
-
所以中序前驱 pre = p 的左子树中最右下结点
-
// 找到以p为根的子树中,最后一个被中序遍历的结点
ThreadNode *Lastnode(ThreadNode *p){
// 循环找到最右下结点(不一定是叶节点)
while(p->rtag == 0){
p = p->rchild;
return p;
}
}
// 在中序线索二叉树中找到结点p的前驱结点
ThreadNode *Prenode(ThreadNode *p){
// 左子树中最右下结点
if(p->ltag == 0){
return Lastnode(p->lchild);
}else{
return p->lchild; //ltag==1 直接返回前驱结点
}
}
既然我们能遍历到结点的前驱结点,那么我们就可以对中序线索二叉树进行逆向中序遍历
// 对中序线索二叉树进行逆向中序遍历
void RevInorder(ThreadNode *T){
for(ThreadNode *p = Lstnode(T);p != NULL;p = Prenode(p){
visit(p);
})
}
3.3、先序线索二叉树找先序后继
-
若 p -> rtag == 1, 说明右子树被线索化,那么右子树就是其先序后继,则 next= p -> rchild
-
若 p-> rtag == 0,说明右子树未被线索化,那么右子树肯定非空,则 p 必然有右孩子。
4、树的存储结构
4.1、树的逻辑结构回顾
树是 n(n≥0)个结点的有限集合,n=0时,称为空树,这是一种特殊情况。在任意一棵非空树中应满足:
- 有且仅有一个特定的称为根的结点
- 当 n>1 时,其余结点可分为 m(m>0) 个互不相交的有限集合 T1、T2、T3、…Tm ,其中每个集合本身又是一棵树,并且称为树结点的子树。
4.2、双亲表示法(顺序存储)
#define MAX_TREE_SIZE 100 // 树中最多结点数
typedef struct{ // 树的结点定义
ElemType data; // 数据元素
int parent; // 双亲位置域
}PTNode;
typedef struct{ // 树的类型定义
PTNode nodes[MAX_TREE_SIZE]; // 双亲表示
int n; // 结点数
}PTree;
- 若我们要新增数据元素,无需按逻辑上的次序存储,只需要存储其父节点的数组下标即可
- 若我们要删除G这个结点,有两种删除方法
- 将G这个结点的双亲指针设为 -1
- 将尾部数据移动,填充G处的 data 和 parent
4.3、孩子兄弟表示法(链式存储)🔥
-
左指针指向第一个孩子结点,右指针指向兄弟结点
-
A 是根节点,A的第一个孩子是B,所以A左连接B,B的兄弟结点是C,所以让B右连接C,D是C的兄弟结点,所以让C右连接D。
-
B的第一个孩子是E,所以让B左连接E,E的兄弟结点是F,所以让E右连接F。
-
E的第一个孩子是K,所以让E左连接K
-
C的第一个孩子是G,所以让C左连接G
-
D的第一个孩子是H,所以让D左连接H,I、J都是是H的兄弟结点,所以让H右连接I、J
-
A是根节点,左边的B是第一个孩子,B的右边C、F、L都是B的兄弟
-
D连在B的左边,所以D是B的第一个孩子,H是D的兄弟
-
G连在D的左边,所以G是D的孩子
-
E连在C的左边,所以E是C的第一个孩子,I连在E的左边,所以I是E的第一个孩子,J连在E的右边,所以J是E的兄弟
4.4、森林和二叉树的转换
森林转化为二叉树
- B、C、D 是兄弟结点,各个树的根节点视为兄弟关系,所以将C、D右连接B
- E是B的第一个孩子,所以将E左连接B,F是E的兄弟结点,所以将F右连接E
- K是E的第一个孩子,所以将K左连接E,L是K的兄弟结点,所以将L右连接L
二叉树转化为森林
-
A、C、F、L是兄弟结点,分别为二叉树的跟结点
-
A的左边连接了B,所以B是A的第一个孩子,B的左边连接了D,所以D是B的第一个孩子,D的左边连接了G,所以G是D的第一个孩子,D的右边连接了H,所以D和H是兄弟结点
-
C的左边连接了E,所以E是C的第一个孩子,E的左边连接了I,所以I是E的第一个孩子,E的右边连接了J,所以E和J是兄弟结点。
-
F的左边连了K,所以K是F的第一个孩子
4.5、小结
5、树、森林的遍历
5.1、树的先根遍历
先根遍历:若树非空,先访问根节点,再依次对每棵子树进行先根遍历。
// 树的先根遍历
void PreOrder(TreeNode *R){
if(R!=NULL){
visit(R); // 访问根结点
while(R还有下一个子树T){
PreOrder(T); // 先根遍历下一棵子树
}
}
}
5.2、树的后根遍历
- 后根遍历:若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点。
- 树的后根遍历也叫做深度优先遍历。
5.3、树的层次遍历
树的层次遍历也叫做广度优先遍历。
5.4、森林的先序遍历
先序遍历森林:
- 若森林为空,则按如下规则进行遍历
- 访问森林中第一棵树的根结点,先序遍历第一棵树中根结点的子树森林,先序遍历除去第一棵树之后剩余的树构成的森林。(效果等同于依次对各个树进行先根遍历)
或者将其转化为对应的二叉树,先序遍历森林效果等同于依次对二叉树的先序遍历。
5.5、森林的中序遍历
另一种方法就是将其转换成与之对应的二叉树,中序遍历森林效果等同于依次对二叉树的中序遍历。
5.5、小结
6、二叉排序树
6.1、二叉排序树的定义
二叉排序树,又称二叉查找树(BST,Binary Search Tree),一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
- 左子树上所有结点的关键字均小于根结点的关键字
- 右子树上所有结点的关键字均大于根结点的关键字
- 左子树和右子树又各是一棵二叉排序树
左子树的结点值<根节点值<右子树结点值
6.2、二叉排序树的查找
若树非空目标值与根节点的值比较,若相等,则查找成功。若小于根节点,则在左子树上查找,否则在右子树上查找。
// 二叉排序树结点
typedef struct BSTNode{
int key;
struct BSTNode *lchild,*rchild;
}BSTNode,*BSTree;
// 在二叉排序树中查找值为key的结点(传入根节点指针和值)
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的结点(递归实现)
BSTNode *BSTSearch(BSTree T,int key){
if(T==NULL){
return NULL; // 查找失败
}
if(key==T->key){
return T; // 查找成功
}else if(key<T->key){
return BSTSearch(T->lchild,key); // 在左子树中找
}else{
return BSTSearch(T->rchild,key); // 在右子树中找
}
}
两者不同点在于最坏空间复杂度不同
6.3、二叉排序树的插入
若原二叉排序树为空,则直接插入结点;否则,若关键字k小于根节点值,则插入到左子树,若关键字k大于根节点值,则插入到右子树。
注意:新插入的结点一定是叶子结点
// 在二叉排序树插入关键字为k的新结点(递归实现)
int BST_Insert(BSTree &L,int k){
if(T==NULL){ // 原树为空,新插入的结点为根结点
T=(BSTree)malloc(sizeof(BSTNode));
T->key=k;
T->lchild=T->rchild=NULL;
return 1; // 返回1,插入成功
}else if(k==T->key){ // 树中存在相同关键字的结点,插入失败
return 0;
}else if(k<T->key){ // 插入到T的左子树
return BST_Insert(T->lchild,k);
}else{ // 插入到T的右子树
return BST_Insert(T->rchild,k);
}
}
6.4、二叉排序树的构造
// 按照str[]中的关键字序列建议二叉排序树
void Create_BST(BSTree &T,int str[],int n){
T=NULL; //初始时T为空树
int i = 0;
while(i<n){ // 依次将每个关键字插入到二叉排序树中
BST_Insert(T,str[i]);
i++;
}
}
不同的关键字序列可能得到同款二叉排序树,也可能得到不同款二叉排序树
6.5、二叉排序树的删除
先搜索到目标结点:
- 若被删除结点z是叶子结点,则直接删除,不会破坏二叉排序树的性质
- 若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置
- 如13这个结点,它只有左子树,我们删除 13,然后让其子树顶上。这样就依然可以保证二叉排序树的性质
- 若结点z有左、右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况
- 删除有两种方式,第一种我们可以使用结点z的直接后继替代z(也就是z结点右子树中第一个被中序遍历的结点(或者说右子树中值最小的结点))
{z的后继:z的右子树中最左下结点(该结点一定没有左子树)}
- 第二种我们可以使用结点z的直接前驱替代z(也就是z结点左子树中最后一个被中序遍历的结点(或者说左子树中值最大的结点))
{z的前驱:z的左子树中最右下结点(该结点一定没有右子树}
- 删除有两种方式,第一种我们可以使用结点z的直接后继替代z(也就是z结点右子树中第一个被中序遍历的结点(或者说右子树中值最小的结点))
例如,我们要删除值为50的结点,我们采用第一种方法,找其右子树中值最小的结点替代,此时60结点只有右子树,我们删除60结点,让其子树顶上,如下:
若我们采用第二种方法,找其左子树中值最大的结点替代,如图:
6.6、查找效率分析
查找成功的平均查找长度ASL
查找失败的平均查找长度ASL
6.7、小结
7、平衡二叉树
平衡二叉树(Balanced Binary Tree),简称平衡树(AVL数):树上任一结点的左子树和右子树的高度之差不超过1。
结点的平衡因子 = 左子树高度 - 右子树高度
一棵平衡二叉树或者是空树,或者是具有下列性质的二叉排序树:
- 左子树与右子树的高度之差的绝对值小于等于1
- 左子树和右子树也是平衡二叉树
- 平衡二叉树结点的平衡因子的值只可能是 -1、0或1
- 只要有任一结点的平衡因子绝对值大于1,就不是平衡二叉树。
// 平衡二叉树结点
typedef struct AVLNode{
int key; // 数据域
int balance; // 平衡因子
struct AVLNode *lchild,*rchild;
}AVLNode,*AVLTree;
7.1、平衡二叉树的插入
在二叉排序树种插入新结点后,如何保持平衡?
例如上图我们插入结点 67 后,右边的结点受到了不平衡的影响,我们需要进行调整,调整的对象是从插入点往回找到第一个不平衡的结点,调整以该结点为根的子树。
最小不平衡子树:例如上图的 66 和 70 结点都不平衡,我们要找所含结点最小的,70有3个结点,66有5个结点,所以我们将70为根节点组成的子树称为最小不平衡子树。
7.2、调整最小不平衡子树
7.2.1、LL
LL:在A的左孩子的左子树中插入导致不平衡
我们调整至平衡,还要使其保持二叉排序树特性。
这里个人感觉讲的并不易于理解,推荐听一下青岛大学王卓老师的这块比较好理解。
7.2.2、RR
7.2.3、代码思路
7.2.4、LR
7.2.5、RL
7.2.6、小结
7.3、王卓平衡二叉树的调整
- LL型:C<B<A,所以我们调整之后也得保持C<B<A
- LR型:B<C<A,所以我们调整之后也得保持B<C<A
7.3.1、LL型
- B结点带左子树α一起上升
- A结点成为B的右孩子
- 原来B结点的右子树β作为A的左子树
7.3.2、RR型
- B结点带右子树β一起上升
- A结点成为B的左孩子
- 原来B结点的左子树α作为A的右子树
例如:
7.3.3、LR型
- C结点穿过A、B结点上升
- B结点成为C的左孩子,A结点成为C的右孩子
- 原来C结点的左子树β作为B的右子树,原来C结点的右子树γ作为A的左子树
例如:
7.3.4、RL型
8、哈夫曼树
8.1、带权路径长度
结点的权:有某种现实含义的数值(如:表示结点的重要性等)
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积。
- 例如上图结点为3的带权路径长度 = 3 × 3 = 9
树的带权路径长度:树中所有叶结点的带权路径长度之和
8.2、哈夫曼树的定义
哈夫曼树:带权路径长度最小的二叉树称为哈夫曼树。
8.3、哈夫曼树的构造(王道)
给定 n 个权值分别为W1,W2,W3,W4…Wn 的结点,构造哈夫曼树的算法描述如下:
- 将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F
- 构造一个新结点,从F中选取两棵根节点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根节点的权值之和
- 从F中删除刚才选出的两棵树,同时将新得到的树加入F中
- 重复步骤2和3,直到F中只剩下一棵树为止
这里的构造可能对于初学者不好理解,推荐听青岛大学王卓老师的课
8.4、哈夫曼树的构造(王卓)
路径:从树中的一个结点到另一个结点之间的分支构成这两个结点间的路径
结点的路径长度:两结点间路径上的分支数
树的路径长度:从树根到每一个结点的路径长度之和。记作TL
结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树。
权:将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
结点的带权路径长度:从根结点到该结点之间的路径长度与该节点的权的乘积。
树的带权路径长度:树中所有叶子结点的带权路径长度之和。记作WPL(Weighted Path Length)
哈夫曼树:最优树(带权路径长度(WPL)最短的树)
注意:"带权路径长度最短"是在"度相同"的树中比较而得的结果,因此有最优二叉树、最优三叉树之称等等。
哈夫曼树:最优二叉树(带权路径长度(WPL)最短的二叉树)
构造哈夫曼树的方法:
- 根据n个给定的权值{W1,W2,W3,…Wn}构成n棵二叉树的森林F={T1,T2,…Tn},其中Ti只有一个带权为Wi的根节点
- 在F中选取两棵根节点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根节点的权值为其左右子树上根节点的权值之和。
- 在F中删除这两棵树,同时将新的到的二叉树加入森林中。
- 重复2和3,直到森林中只有一棵树为止,这颗树即为哈夫曼树。
王卓老师的口诀:1.构造森林全是根,2.选用两小造新树,3.删除两小添新人,4.重复2、3剩单根
-
构造森林全是根。有几个结点就有几个根节点,从而组成森林。
- 例如上方有4个结点,则将其都作为根节点组成森林。
-
选择两个权值最小的结点,将其权值加和,得到的权值为父结点,两个权值最小的结点为左右子树。
- 例如上方权值最下的两个结点为 2 和 4,将其权值加和为6,则 6 为父结点, 2 和 4 为左右子树。
-
删除刚才选择的那两个权值最小的结点,将剩余结点与第二步得到的父结点排成一排结成森林。
- 删除2和4结点,将 7、5、6三个结点作为根组成森林
-
继续选择两个权值最小的结点,将其权值加和,得到的权值为父结点,两个权值最小的结点为左右子树。
- 继续选择 5、6 两个结点,权值加和为11,则 11 为父结点,5、6为左右子树。
包含n个叶子结点的哈夫曼树中共有 2n-1 个结点。(n个结点,两两合并,总共合并n-1次,产生n-1个新结点)
哈夫曼树的结点的度为0或2,没有度为1的结点。
度为1的结点是n个,度为2的结点是 n-1 个
8.5、哈夫曼树的应用
8.5.1、哈夫曼编码(王卓)
- 统计字符集每个字符在电文中出现的平均概率(概率越大,要求编码越短)
- 利用哈夫曼树的特点:权越大的叶子离根越近,将每个字符的概率值作为权值,构造哈夫曼树。则概率越大的结点,路径越短。
- 在哈夫曼树的每个分支上标上0或1,结点的左分支标0,右分支标1,把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码。
例题:
- 构造哈夫曼树
- 左分支标0,右分支标1,把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码。