哈夫曼树(算法笔记)

本文内容基于《算法笔记》和官方配套练题网站“晴问算法”,是我作为小白的学习记录,如有错误还请体谅,可以留下您的宝贵意见,不胜感激


一、哈夫曼树

1.合并果子

在这里插入图片描述
在上面的合并果子问题中,消耗体力之和可以通过把叶子结点的权值乘以它们各自的路径长度再求和来获得。
证明:可以从叶子结点开始,将其父结点拆分为左右子结点构成的结点,可以发现每一个根结点的权值都依赖于其左右子结点构成。每向上合并一层,就需要用一次叶子结点的值,所以这颗树的权值之和就等于叶子结点的权值乘以它们各自的路径长度再求和。(注:只有在根结点的权值由其左右子结点的权值构成时才可以这样计算)
叶子结点的路径长度:从根结点出发到达该结点的边数;
叶子结点的带权路径长度:叶子结点的权值乘以其路径长度;
树的带权路径长度:所有叶子结点的带权路径长度之和;
合并果子问题就转换成了:已知n个数,寻找树的所有叶子结点的权值恰好为这n个数,并且使得这棵树的带权路径长度最小。带权路径长度最小的树被称为哈夫曼树(又称为最优二叉树),对同一组叶子结点,哈夫曼树形态不唯一,但其最小带权路径长度唯一。

2.哈夫曼树的构建方法

贪心法构建哈夫曼树:
在合并的过程中总是先合并权值最小的两个结点,这样可以保证权值越大的结点被使用的次数越少(可以参考上面的证明),于是具体方法如下:
①初始状态下共有n个结点(结点的权值分别是给定的n个数),将它们视作n棵只有一个结点的树;
②合并其中根结点权值最小的两棵树,生成两棵树根结点的父结点,权值为这两个根结点的权值之和,这样树的数量就减少了一个;
③重复操作②,直到只剩下一棵树为止,这棵树就是哈夫曼树;

通过上面的过程,可以发现一个现象:哈夫曼树不存在度为1的结点,并且权值越高的结点相对来说越接近根结点。

3.求带权最短路径

实际问题往往只涉及求带权最短路径,所以并不需要构建一颗实际的哈夫曼树,所以针对哈夫曼树的合并构建思想,即总是合并权值最小的两个结点,可以采用优先级队列或者堆来实现(堆所解决的就是动态更新数据流中最值),且采用小顶堆实现。每次从堆内取出最小的两个结点,生成新结点后压入堆内,并且需要一个变量记录生成的新结点数值。
(1)合并果子问题
在这里插入图片描述
完整代码如下:

#include<cstdio>
#include<queue>
using namespace std;

struct cmp{
	bool operator () (int x , int y){
		return x > y;
	}
};

int main(){
	int n;
	scanf("%d", &n);
	priority_queue <int , vector<int> , cmp> pq;
	for(int i = 0; i <= n - 1; i++){
		int height;
		scanf("%d", &height);
		pq.push(height); 
	} 
	int ans = 0;
	while(pq.size() > 1){   //取优先级靠前的两位数 
		int temp = pq.top();  //第一位数 
		pq.pop();
		temp += pq.top();     //第二位数 
		pq.pop();
		pq.push(temp);
		ans += temp;
	}
	printf("%d", ans);
} 

(2)树的最小带权路径长度
在这里插入图片描述
完整代码如下:

#include<cstdio>
#include<queue>
using namespace std;

struct cmp{
	bool operator () (int x , int y){
		return x > y;
	}
};

int main(){
	int n;
	scanf("%d", &n);
	priority_queue <int , vector<int> , cmp> pq;
	for(int i = 0; i <= n - 1; i++){
		int height;
		scanf("%d", &height);
		pq.push(height); 
	} 
	int ans = 0;
	while(pq.size() > 1){   //取优先级靠前的两位数 
		int temp = pq.top();  //第一位数 
		pq.pop();
		temp += pq.top();     //第二位数 
		pq.pop();
		pq.push(temp);
		ans += temp;
	}
	printf("%d", ans);
} 

二、哈夫曼编码

首先,从二叉树的角度来看编码,对一颗二叉树,可以将所有左分支都标记为0、所有右分支都标记为1,那么对树上的任意一个结点,都可以根据从根结点出发到达它的分支顺序得到一个编号,并且这个编号是所有结点中唯一的。并且,对于任何一个叶子结点,其编号一定不会成为其他任何一个结点编号的前缀
在实际数据传输中,是通过二进制来传输数据的,即01串,所以需要将字符编码成01串的形式。但这个过程容易产生一个问题:即如果有某一种字符的编码拼接在当前字符编码之后能产生另一种字符编码,这样会导致解码的不确定性。所以,产生了前缀编码:其中任何一个字符的编码都不是另一个字符的编码的前缀。其存在意义在于防止解码的不确定性。
前缀编码的实现可以利用二叉树进行编码,但得到的不一定是最短的,从效率的角度来讲,肯定是需要最短的前缀编码。可以发现,如果把字符出现的频数作为各自叶子结点的权值,那么字符串编码成01串后的实际长度实际上就是这棵树的带权路径长度。显然,这可以用哈夫曼树来解决,用优先级队列求出的是最小前缀编码长度,而具体的编码需要通过构架具体的哈夫曼树来实现。
在这里插入图片描述
完整代码如下:

#include<cstdio>
#include<queue>
#include<cstring> 
using namespace std;

const int MAXN = 100;
char str[MAXN] = {};
int hashTable[MAXN] = {};

struct cmp{
	bool operator () (int x , int y){
		return x > y;
	}
};

int main(){
	scanf("%s", str);
	for(int i = 0; i < strlen(str); i++) hashTable[str[i]]++; 
	priority_queue <int , vector<int> , cmp> pq;
	for(int i = 'A'; i <= 'Z'; i++) 
		if(hashTable[i] > 0) pq.push(hashTable[i]); 
	int ans = 0;
	while(pq.size() > 1){   //取优先级靠前的两位数 
		int temp = pq.top();  //第一位数 
		pq.pop();
		temp += pq.top();     //第二位数 
		pq.pop();
		pq.push(temp);
		ans += temp;
	}
	printf("%d", ans);
} 

这里给出我自己思考的哈夫曼树的实现方法
(1)初始化:即所有结点都设置为根结点;
(2)利用优先级队列取出最小的两个结点,并生成新结点的数据,新建结点,并将新结点的左右子结点设为取出的两个最小结点;
(3)反复重复(2)步骤,直到优先级队列中的元素个数等于1。
这时优先级队列里存放的元素就是根结点的数据,通过静态链表的散列,直接就可以找到根结点,继而可以实现二叉树的基本功能。


备注

1.哈夫曼编码是对确定的字符串来讲的;
2.动态查找表的创建方式就是不断通过查找进行插入新结点,因为过程中要保持自身的数据结构形态;

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瓦耶_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值