1.哈夫曼树的定义:
什么是哈夫曼树?
又称为最优二叉树,是带权路径长度之和最小的二叉树。
带权路径长度之和又是什么?
设二叉树有n个叶子结点,每个叶子结点的权重为Wk,从根节点到每个叶子结点的长度为Ik,则所有叶子结点带权路径长度( Wk * Ik)的和为该树的带权路径长度之和(WPL)。
WPL = ∑nk=1 Wk * Ik;
这些只是概念上的东西,所谓带权路径长度之和最小不过就是为了让经常出现的东西不像其他东西一样占用过多空间,看下面这个例子理解这句话:
对于这个例题相信大家都会这样写出代码,对应的判定树:
但是如果有很多人的成绩要经过这样的转换,而这些人学习又很好,都考了八十分以上,那岂不是每次都要经过四次判断才能完成转换。所以需要对这个树进行修改,我们假设下面一种情况来体会分析的过程:
对于上面那个判断树,1到根节点的长度为1,2到根节点的长度为2,3到根节点的长度为3,4、5到根节点的长度为4,所以我们套用前面的公式得到用上面那个判断树的查找效率就是:
但是0-59(对应一分)所占的比例只有0.05,而后面的都有0.3、0.4,显然每次第一个判断是否在0-59内是不合适的,我们根据所占比例改为:
这样修改之后我们可以清晰地看到所占比例(相当于权重)的分数的到根节点的长度变短了,所以查找效率提升了。这就是哈夫曼树的真谛。
2.哈夫曼树的创建及原理:
根据上面的例子我们可以看出,我们每次都先把出现频率最低的放在下面,所以我们构造哈夫曼树时,每次都把权重最小的两棵二叉树合并。看这个例子体会过程:注意第四步并不是把4和6合并,而是4和5,因为每次都把权重最小的两棵二叉树合并。
而最小堆就是小的元素在上,大的元素在下,很适合我们像上图这样边取出边合并边存入,所以我们的数据就放入最小堆来实现哈夫曼树:
这是具体的实现,其中关于堆的函数可以看这片文章https://mp.csdn.net/postedit/104214196,不过这里面是最大堆的函数实现,稍作改动即为最小堆。
由于堆其实就是优先队列,我们可以用STL库中的优先队列priority_queue来进行数据的存放:(其实和用最小堆来实现不过是在调用函数时可以用库里面的不用自己定义了)
priority_queue<int,vector<int>,greater<int> > q;
//priority_queue对应最小堆的定义形式,为什么这样写自己去查优先级设置。
int main()
{
int n;
cin>>n;
int i,weight[n];
for(i=0;i<n;i++){
cin>>weight[i];
q.push(weight[i]);
}
while(q.size()>1){
int x=q.top()//得到堆顶值(最小值)
q.pop();
int y=q.top();//得到堆顶值(最小值)
q.pop();
q.push(x+y);//合并两个权重最小的子树
//根据需要加一些其他的
}
}
这样做最终得到的只有根节点的值,但哈夫曼树的真正作用其实不是哈夫曼树的本身,而是他在创建时每次合并后根据其他需要进行一些别的运算。
3.哈夫曼树的特点:
(1)没有度为1的节点
因为是两两合并得到的,不可能有单独的一个出来。
(2)n个叶子结点的哈夫曼树有一共有2n-1个节点
因为没有度为1的节点,还有公式n0=n2+1(也就是叶子结点数比度为2的节点个数多一个),所以。。。
(3)哈夫曼树的任意非叶节点的左、右子树交换后依然还是哈夫曼树。
(4)对于同一组权重,可以有多个不同结构的哈夫曼树,但他们的WPL一定都相同!!! 看下面这个例子:
4.哈夫曼编码:
字符串中字符的编码:
对于几十万单词的英语论文,我们不可能为每个字母开辟相同的编码空间,万一a出现了100000次,而z只出现了一次,那z的空间就白白浪费了。解决这件事就用到了哈夫曼树的原理。看这个例子:
解决方案如下:
显然前两种和我们说的那种情况一样,那么我们如何来实现不等长编码呢?
再看下面的例子:
如例子中这样建树,对应a=00,u=01,x=10,z=11
这样建树,对应a=0,x=10,u=110,z=11,这时就是不等长编码了。那么问题又来了,怎样让编码的代价(cost)最小呢?,这就用到哈夫曼树的原理,就和之前的例子:
我们只需要得到每个字母出现的次数(权重),然后每次都让最小的两个子树合并,左为0右为1一直到叶子结点(也就是字母出现次数存放的地方)为字母从上到下记录其编码。 例:
只给出了最终的结果,其中过程自己去画体会。