【贪心】哈夫曼编码&哈夫曼树

编码

概念

众所周知,计算机以01串来储存和运算。
所以,如果我们想要存一个字符或汉字,例如a,计算机会将它变为一个01串,这个串就是a的编码。

由编码而产生的问题

歧义

如果我们输入了一个词:cat。
如果a的编码是1,c的编码是10,t的编码是11,那么“cat”对应的编码就是“10111”。好了,那么问题来了:10111这个串既是“cat”,也可以理解为“cta”,到底是哪种?如果计算机有庞大的词库,或许它会知道没有“cta”这个词,但是这样的复杂度就太高了。所以,在编码时我们要注意不能有歧义。

长度

当输入的文章很长时,每个字母转换成编码后,整个文章的编码会超级长(就算连着排编码,z的编码也是11010),耗费的空间也就很大。所以,我们还面临的一个问题是:要让文章的编码尽量短。

解决问题

歧义的问题先让它挂一会,先看长度的问题。

长度

为了减少长度,我们可以用变长码来优化。
什么意思?会改变长度的编码?当然不可能,变长码的意思是:给出现频率高的字符用短的编码,给出现频率低的用长的编码。这样可以有效优化总编码太长的问题。
何以见得“有效”,我们举个栗子:

abcdef
出现次数(千次)4513121695
定长码000001010011100101
变长码010110011111011100

定长码所需空间:3 * ( 45 + 13 + 12 + 16 + 9 + 5 ) = 300 千位
变长码所需空间:1 * 45 + 3 * ( 13 + 12 + 16) + 4 * ( 9 + 5) = 227 千位
减少了多少自己算。

歧义

看编码是否会有歧义,其实就是看有没有字符的编码是另一个字符编码的前缀。
我们巧妙地使用二叉树编码来避免这个问题:
将待编码的字符当做叶子节点,构造一棵这样的二叉树:
二叉树编码
(这棵树有些不平衡,右子树还可以大些)
将所有的左儿子的边赋为1(或0),所有右儿子的边赋为0(或1),会发现,根节点到每个叶节点的路径的01串不可能成为某个串的前缀(因为0的存在),也就避免了歧义的问题。
这个图中,a的编码就是111,b的编码为110,c的编码为10,d的编码为0。

哈夫曼树

哈夫曼(Huffman)树,也叫最优二叉树,就是上面的二叉树,并使编码长度最优。

方法

相信有人能看出:上面的二叉树每个叶节点到根的长度不同(废话),也就是说,每个字符的编码长度不同,这不正好就是变长码了吗?
所以,直接把出现频率低的放在下面,出现频率高的放在上面不就好了!
如果你看到这里就去实现且成功了,我给你出彩。
如果你没有成功,不要怕,让我们直接get哈夫曼前辈的算法:

构造一棵哈夫曼树
①每个字符自成一棵树,根为它自己且权值为它的出现次数;
②将所有树按根节点的权值从小到大排序;
③取出根节点权值最小的两颗树,合并起来,并将它们合并后根节点的权值赋为它们合并前根节点的权值之和;
④将第③步中合并得到的树的根节点放入②的序列中并维护其有序性;
⑤重复③、④直到只剩下一棵树。

看起来很多,其实很简单:用优先队列维护权值的从小到大,每次取2次top并pop掉,然后合并这两个top,将合并后的节点放入队列,重复执行N-1次(N为字符数)。

输出编码

树建好了,输出编码就很简单了,从根开始递归,把途经的路径存下来,一旦到达叶子节点就输出。

代码

你们最爱的代码:

//输入N,接下来输入N个数,代表出现次数,然后输出每个叶子节点的编码
#include<queue>
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;

#define MAXN 50
#define MAXT 100
struct Node
{
    int num,w;//num表示这个节点在Tree数组中的下标
    int f,lc,rc;
    bool operator < (const Node b) const {return w>b.w;}//优先队列比较方式
}Tree[MAXT+5];
int N,Size,Root;
char Ans[MAXN+5];

priority_queue<Node> Q;

void unite(Node x,Node y)//合并两个节点
{
    Tree[x.num].f=Tree[y.num].f=++Size;
    Tree[Size].lc=x.num;
    Tree[Size].rc=y.num;
    Tree[Size].num=Size;
    Tree[Size].w=Tree[x.num].w+Tree[y.num].w;
    Q.push(Tree[Size]);//放入队列
}

void print(int u,int Len)//Len记录长度
{
    if(!Tree[u].lc&&!Tree[u].rc){puts(Ans+1);return;}
    Len++;
    Ans[Len]='0';
    print(Tree[u].lc,Len);
    Ans[Len]='1';
    print(Tree[u].rc,Len);
    Ans[Len]=0;
}

int main()
{
    scanf("%d",&N);
    for(int i=1;i<=N;i++)
    {
        scanf("%d",&Tree[i].w);
        Tree[i].num=i;
        Q.push(Tree[i]);
    }
    if(N==1) printf("0");//特殊处理
    Size=N;
    for(int i=1;i<N;i++)
    {
        Node u1=Q.top();Q.pop();//注意top一次就要pop一次,不然取出来的节点是一样的
        Node u2=Q.top();Q.pop();
        unite(u1,u2);
    }
    for(int i=1;i<=Size;i++)//找根
        if(!Tree[i].f)
            Root=i;
    print(Root,0);
}
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值