编码
概念
众所周知,计算机以01串来储存和运算。
所以,如果我们想要存一个字符或汉字,例如a,计算机会将它变为一个01串,这个串就是a的编码。
由编码而产生的问题
歧义
如果我们输入了一个词:cat。
如果a的编码是1,c的编码是10,t的编码是11,那么“cat”对应的编码就是“10111”。好了,那么问题来了:10111这个串既是“cat”,也可以理解为“cta”,到底是哪种?如果计算机有庞大的词库,或许它会知道没有“cta”这个词,但是这样的复杂度就太高了。所以,在编码时我们要注意不能有歧义。
长度
当输入的文章很长时,每个字母转换成编码后,整个文章的编码会超级长(就算连着排编码,z的编码也是11010),耗费的空间也就很大。所以,我们还面临的一个问题是:要让文章的编码尽量短。
解决问题
歧义的问题先让它挂一会,先看长度的问题。
长度
为了减少长度,我们可以用变长码来优化。
什么意思?会改变长度的编码?当然不可能,变长码的意思是:给出现频率高的字符用短的编码,给出现频率低的用长的编码。这样可以有效优化总编码太长的问题。
何以见得“有效”,我们举个栗子:
a | b | c | d | e | f | |
---|---|---|---|---|---|---|
出现次数(千次) | 45 | 13 | 12 | 16 | 9 | 5 |
定长码 | 000 | 001 | 010 | 011 | 100 | 101 |
变长码 | 0 | 101 | 100 | 111 | 1101 | 1100 |
定长码所需空间: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);
}