1.背景
代码(码字):Q{001,00,010,01}表示字符a,b,c,d
同一序列:0100001
产生两种译码(产生歧义):01 00 001;010 00 01。
二元前缀码:任何字符的代码不能作为其他字符字符代码的前缀
利用二元前缀码译码:从第一个字符开始依次读入每个字符(0或1),如果发现读到的子串与某个码字相等,就将这个子串译作对应的码字;然后从下一个字符开始继续这个过程,知道读完输入的字符串为止。
二元前缀编码存储:二叉树结构,每个字符作为树叶,对应这个字符的前缀码看作根到这篇树叶的一条路径,每个节点通向左儿子的便记作0,通向右儿子的边记作1。
字符集合C={x1,x2,…,xn}
xi的频率是f(xi)
d(xi)表示字符xi二进制位数,也就是xi的码长
二元前缀编码:二叉树
码字:树叶
码字的二进制位数:树叶的深度
存储一个字符所使用的二进制数的平均值
最优二元前缀码:每个码字平均使用二进制位数最小的前缀码
2.问题
给定字符集C={x1,x2,…,xn}和每个字符的频率f(xi),求关于C的一个最优前缀码。
3.解析
构造最优前缀码的贪心算法就是哈夫曼算法(Huffman)
算法实现步骤:
1)初始化n个单节点的树,每个字符的概率记在树的根中,用作树的权重;
2)找到两棵权重最小的树,把它们作为新树中的左右子树,并把权重和记作新的权重记录在新树的根中;
3)重复第二步直到只剩一棵单独的树。
算法正确性证明:
证明:假设Huffman算法对于规模为k的字符集都得到最优前缀码,那么对于规模为k+1的字符集也得到最优前缀码.
n=2,字符集C={x1,x2},对任何代码的字符至少都需要1位二进制数字.Huffman算法得到的代码是0和1,是最优前缀码.
假设Huffman算法对于规模为k的字符集都得到最优前缀码,考虑规模为k+1的字符集C={x1,x2,…,xk+1},其中x1,x2∈C是频率最小的两个字符.令C’ = (C-{x1,x2})∪{z},f(z) = f(x1) + f(x2).
根据归纳假设,算法得到一棵关于字符集C’,频率f(z)和f(xi)(i=3,4,…,k+1)的最优前缀码的二叉树T’.
将x1,x2作为z的儿子附到T’上,得到树T,那么T是关于C = (C’-{z})∪{x1,x2}的最优前缀码的二叉树.
反证:如果T不是关于C的最优前缀码二叉树,则存在更优树T*,B(T*)<B(T),且由引理1,其树叶兄弟是x1和x2.去掉T中x1和x2,得到T’ .根据引理2,B(T*’) = B(T*) - (f(x1) + f(x2)) < B(T) - (f(x1) + f(x2)) = B(T’). 这与T’是一棵关于C’的最优前缀码的二叉树矛盾. 因此,T就是C的最优解。
算法实例:
{1,2,2,8,10,10,16,18}
4.设计
算法伪代码
算法 Huffman(C)
输入:C={x1,x2,...,xn},f(xi),i=1,2,...,n
输出:Q //队列
n←|C|
Q←C //频率递增队列Q
for i←1 to n-1 do
z←Allocate-Node() //生成结点
z.left←Q中最小元 //最小作z左儿子
z.right←Q中最小元 //最小作z右儿子
f(z)←f(x)+f(y)
Insert(Q,z) //将z插入Q
return Q
5.分析
算法时间复杂度
O(nlogn)频率排序;for 循环 O(n),插入操作 O(logn),算法时间复杂度是 O(nlogn) .
6.源码
算法源码地址
https://github.com/Ying917/Algorithm-Analysis/blob/master/Huffman.cpp