哈夫曼树
哈夫曼树,又称最优树,时一类带权路径长度最短的树。
从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径,路径上的分支数目称做路径长度。
树的路径长度是从树根到每一结点的路径长度之和。
树的带权路径长度为树中所有叶子结点的带权路径长度之和,通常记作 WPL = ω1l1 + …… + ωnln
假设有 n 个权值{ ω1,ω2,……,ωn },试构造一棵有 n 个叶子结点的二叉树,每个叶子结点带权为 ωi,则其中带权路径长度 WPL 最小的二叉树称做最优二叉树或哈夫曼树。
(a)WPL = 7×2+5×2+2×2+4×2=36;
(b)WPL = 7×3+5×3+2×1+4×2=46;
(c)WPL = 7×1+5×2+2×3+4×3=35;
其中(c)树最小。可以验证它恰好为哈夫曼树,即其带权路径长度在所有带权为7、5、2、4的 4 个叶子结点的二叉树中居最小。
哈夫曼树的特点
- WPL最小
- 给定权值的哈夫曼树可能不只一棵
- 只有度为 2,度为 0 的结点—正则的(严格的)二叉树
- 一棵有 n 个叶子结点的哈夫曼树共有 2n-1 个结点
- 权值越大的叶结点越靠近根结点,而权值越小的叶结点越远离根结点
构造哈夫曼树算法
- 根据给定的 n 个权值 {ω1,ω2,……,ωn} 构成 n 棵二叉树的集合 F={T1,T2,……,Tn},其中每棵二叉树 Ti 中只有一个带权为 ωi 的根结点,其左右子树均空。
- 在 F 中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左、右子树上根结点的权值之和。
- 在 F 中删除这两棵树,同时将新得到的二叉树加入 F 中。
- 重复 1 和 2,直到 F 只含一棵树为止。这棵树便是哈夫曼树。
哈夫曼编码
若采用相同长度的不同码字代表相应的符号,就称为等长编码。
若要设计长短不等的编码,则必须是任一个字符的编码都不是另一个字符的编码的前缀,这种编码称做前缀编码。
可以利用二叉树来设计二进制的前缀编码。约定左分支表示字符‘0’,右分支表示字符‘1’(左0右1),则可以从根结点到叶子结点的路径上分支字符组成的字符串作为该叶子结点字符的编码。如此得到必为二进制前缀编码,二进制前缀编码也被称为哈夫曼编码。
示例
假设用于通信的电文由字符集{a,b,c,d,e,f,g}中的字母构成。它们在电文中出现的频度分别为 {0.31,0.16,0.10,0.08,0.11,0.20,0.04}
为这7个字母设计哈夫曼编码
为这7个字母设计等长编码,至少需要几位二进制数?
哈夫曼编码比等长编码使电文总长压缩多少?
先对权值进行排序。
每次从序列中取出两个结点。然后构成一个结点,重新放回序列。
然后再次重复操作,直至,队列剩余一个结点为止。
等长编码位数相同表示 7 种符号至少需要 3 位二进制位。
23 =8,所以需要三位二进制数
等长二进制三位共100个字符,占用100 * 3 = 300
哈夫曼编码:2 * 20+ 3 * 10+11* 3+4 * 4+8 * 4+16 * 3+ 31 * 2 = 261
压缩:(300 - 261)/ 300 = 0.13
哈夫曼树构造代码
typedef struct{
unsigned int weight;
unsigned int parent,lchild,rchild;
}HTNode, *HuffmanTree; //动态分配数组存储哈夫曼树
typedef char **HuffmanCode; //动态分配数组存储哈夫曼编码表
void HuffmanCoding(HuffmanTree &HT, HuffmanCode &HC, int *w, int n){
//w存放n个字符的权值(均>0),构造哈夫曼树HT,并求出n个字符的哈夫曼编码HC
if(n <= 1)//字符小于一个
return;
m = 2 * n - 1;//m为生成的中间节点个数
HT = (HuffmanTree)malloc((m+1) * sizeof(HTNode)); //0号单元未用
for(p = HT, i = 1; i <= n; i++, p++, w++)//字符初始化
*p = {*w, 0, 0, 0};
for(; i <= m; i++, p++)//中间节点初始化
*p = {0, 0, 0, 0};
for(i = n+1; i<= m; i++){ //建哈夫曼树
//在HT[1..i-1]选择parent为0且weight最小的两个结点,其序号分别为s1和s2
Select(HT, i-1, s1, s2);
HT[s1].parent = i;
HT[s2].parent = i;
HT[i].lchild = s1;
HT[i].rchild = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
//从叶子到根逆向求每个字符的哈夫曼编码
HC = (HuffmanCode)malloc((n+1) * sizeof(char *));//分配n个字符编码的头指针向量
cd = (char *)malloc(n * sizeof(char));//分配求编码的工作空间
cd[n-1] = "\0";//编码结束符
for(i = 1; i <= n; i++){ //逐个字符求哈夫曼编码
start = n - 1;//编码结束符位置
for(c = i, f = HT[i].parent; f!=0; c = f, f = HT[f].parent){//从叶子到根逆向求编码
if(HT[f].lchild == c)
cd[--start] = "0";
else
cd[--start] = "1";
}
HC[i] = (char *)malloc((n-start) * sizeof(char));//为第i个字符编码分配空间
strcpy(HC[i], &cd[start]);//从cd复制编码(串)到HC
}
free(cd);//释放工作空间
}
示例
已知某系统在通信联络中只可能出现 8 种字符,其概率分别为0.05,0.29,0.07,0.08,0.14,0.23,0.03,3.11,试设计哈夫曼编码。
哈夫曼编码和二进制编码的比较
- 码字不同。 哈夫曼所构造的码字不是唯一的,对于同一信息源,无论上述的顺序如何排序,它的平均码长是不会改变,所以他的优点是编码效率唯一性。而二进制编码所构成的码字是唯一。
- 长度不同。 哈夫曼树是依据字符出现概率来构造异字头的平均长度最短的码字,比较精准,二进制编码用预计规定的方法将文字、数字或其他对象编成二进制的数码,或将信息、数据转换成二进制电脉冲信号。二进制是最基础的编码。
- 稳定性不同。 哈夫曼编码的稳定性。如果改变其中一位数据就会产生改变。二进制编码具有抗干扰能力强,可靠性高等优点。