哈夫曼编码(Huffman Coding)理解+哈夫曼树(Huffman Tree)构造方法

前言

  其实这个东西我在NOIP2017初赛前就已经学过了(做往年的NOIP初赛题备战的时候碰到的),只不过一直没有用,于是就忘记了。。。

哈夫曼编码

简介

  哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)。

方法

  我们可以举个栗子:现在有一份电报,里面有7个字符 ( a1 a2 a3 a4 a5 a6 a7 ),它们的出现频率分别是U:(20 19 18 17 15 10 1)。
  给频率最小的两个字符a6与a7分别指定为“1”与“0”(不必在意分给谁“1”谁“0”),然后将它们的频率相加再与原来的 a1~a5组合并重新排序成新的:
   ( a1 a2 a3 a4 a5 a6′ )
  U′:(20 19 18 17 15 11)
  对a5与a′6分别指定“1”与“0”后,再作频率相加并重新按频率排序得:
  U″:(26 20 19 18 17)
  U″′:(35 26 20 19)
  U″″:(39 35 26)
  U″″′:(61 39)
  直到最后得 U″″″:(100)
  赫夫曼编码的具体方法:先按出现的频率大小排队,把两个最小的频率相加,作为新的频率和剩余的频率重新排队,再把最小的两个频率相加,再重新排队,直到最后只剩下1个。每次相加时都将“0”和“1”赋与相加的两个频率,读出时由该符号开始一直走到最后的“1”, 将路线上所遇到的“0”和“1”按最低位到最高位的顺序排好,就是该符号的赫夫曼编码。这个过程可以简单地理解为合并果子。
  然后,上面那个例子,每个字符的编码就分别为:a1(01),a2(00),a3(111),a4(110),a5(101),a6(1001),a7(1000)。这样一来,它就保证了字符频率越大,码长越小;频率越小,码长越长。

哈夫曼树

定义

  给定n个权值作为n个叶子结点,构造一棵二叉树,若带权路径长度(WPL)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

概念

  我们发现哈夫曼树的定义中有“带权路径长度”,那么这是什么?请慢慢看吧。
  
  1、路径和路径长度
  在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。
  
  2、结点的权及带权路径长度
  若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
  
  3、树的带权路径长度
  树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。
  树的带权路径长度记为WPL=(W1*L1+W2*L2+W3*L3+…+Wn*Ln),N个权值Wi(i=1,2,…n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,…n)。

构造

  假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:
  (1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
  (2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
  (3)从森林中删除选取的两棵树,并将新树加入森林;
  (4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
  这么做的话,运用堆可以将其优化到 O(nlog2n) O ( n l o g 2 n )

正确性

  那么,为何这样构造可以使它的带权路径长度最小呢?
  其实这个的证明很简单。
  一开始,我们有n个叶子节点,它们都有权值Wi(i=1,2,…n),记它们为实点。
  我们每次操作都是选出两个权值最小的根节点,比如选出a和b,然后新建一个虚点c,令a、b分别为c的左右儿子。我们就相当于让a、b两棵子树中的所有点的深度+1,那么也会让它们的叶子结点深度+1。
  而我们知道,a、b这两个根节点的权值全权来源于它们各自子树的叶子结点的权值,而这又会使a、b的子树中的叶子结点将来计算答案时的系数(即它们的深度)+1,所以会使WPL+=w[a]+w[b]。
  最后,我们始终是要进行n-1次操作的,我们每次又是取最小的两个w加进答案,所以。。。
  这样很贪心是不是?但是我们同样也能证明若在某一次操作时,我们不选最小的两个,答案只会更劣。
  比如设有三个点a、b、c,满足w[a]<w[b]<w[c]。如果我们合并a、c变成d,再合并b、d,那么c的深度就会被+两次,而它的权值又是最大的,显然不对。
  更严谨地说,对于先合并的点,最终建成哈夫曼树后,它的深度会越大。比如有点a,b,c,d,e,权值满足w[a]<w[b]<w[c]<w[d]<w[e],那么我们先取出a,b,合并成f;接下来如果我们不继续将f跟其他点合并,而去合并另外两个点,那么w[d]+w[e]>w[c]+w[e]>w[c]+w[d]>w[f],所以f又要被合并。
  至于用严谨的数学方法证明的话,我也不大会,哪位dalao懂的话不妨在评论区指点一下。。。

模板

#include 
#include 
#define fs for(i=last[x];i;i=next[i])
struct myComp  
{  
    inline bool operator()(const int &a,const int &b){return w[a]multiset<int,myComp>s;//堆优化
multiset<int,myComp>::iterator it;
int a,b,l[N],r[N];//l[i]、r[i]分别表示点i的左右儿子
void code(int x)
{
    int i;
    fs s.insert(tov[i]);
    while(s.size()>1)
    {
        it=s.begin();a=*it;s.erase(it);
        it=s.begin();b=*it;s.erase(it);
        w[++p]=w[a]+w[b];
        l[p]=a,r[p]=b;
        s.insert(p);
    }
    if(!s.empty())
    {
        it=s.begin();
        l[x]=*it;
        s.erase(it);
    }
    fs code(tov[i]);
}

你可能感兴趣的:(学习小记,哈夫曼编码)