哈夫曼树和哈夫曼编码
哈夫曼树和哈夫曼编码 HUFFMAN
1. 哈夫曼树
-
引例:将百分制的考试成绩转换成五分制的成绩,且考虑学生成绩的分布的概率
- 判定方式一
- 代码逻辑
- 二叉判定树
- 查找效率
- 代码逻辑
- 判定方式二
- 代码逻辑
- 二叉判定树
- 查找效率
- 代码逻辑
- 我们发现,第二种判定树的搜索效率更好
- 判定方式一
-
核心问题:如何根据结点不同的查找频率构造更有效的搜索树?
-
哈夫曼树
- 带权路径长度(WPL):设二叉树有 n 个叶子结点,每个叶子结点带有权值 w k w_k wk,从根结点到每个叶子结点的长度为 l k l_k lk,则每个叶子结点的带权路径长度之和就是: W P L = Σ k = 1 n w k l k WPL = Σ_{k=1}^{n}w_kl_k WPL=Σk=1nwklk。
- 最优二叉树 or 哈夫曼树:即 WPL 最小的二叉树
-
哈夫曼树的构造:即构造一棵二叉树,同时使得 WPL 值最小
- 构造方式:每次把权值最小的两棵二叉树合并
- 算法实现思路
- 从当前“叶子结点列表”中取出两个最小的结点合并成一个二叉树,新的二叉树结点权值是这两个结点权值的和,并且将这个结点添加到“叶子结点列表”中去
- 循环上述流程,直到“叶子结点列表”只有一个一个结点时,哈夫曼树构造终止
- 代码实现逻辑:依据最小堆实现代码
- 每次从最小堆取两个最小的结点进行合成
- 算法分析:时间复杂度为 O(NlogN)
HuffmanTree Huffman(MinHeap H) { /* 假设 H->Size 个权值已经存在 H->Elements->Weight 里 */ /* 根据哈夫曼构建思路,一共只需要循环 n-1 次 */ /* 循环结束后,此时 H 中也只有一个结点了,就是根节点 */ HuffmanTree T; BuildHeap(H); // 根据传入的 H(至少是一个完全二叉树),将 H->Elements[] 按照权值调整为最小堆 for (int i = 1; i < H->Size; i++) { T = (HuffmanTree)malloc(sizeof(HuffmanTreeNode)); T->Left = DeleteMin(H); // 从最小堆里边删除两个最小的结点,共同构建一个二叉树 T->Right = DeleteMin(H); T->Weight = T->Left->Weight + T->Right->Weight; // 更新权值 Insert(H, T); } T = DeleteMin(H); return T; }
〖例〗 有五个叶子结点,它们的权值为{1,2,3,4,5},用此权值序列可以构造出形状不同的多个二叉树。
- 构造一颗哈夫曼树
此时 WPL = 12 - 其他情形
-
哈夫曼树的特点
- 没有度为 1 的结点
- 哈夫曼树的任意非叶节点的左右子树交换后仍是哈夫曼树
- n 个叶子结点的哈夫曼树共有 2n-1 个结点
- 简证 n 0 = n 2 + 1 , n 1 = 0 , n 0 = n = > n a l l = n 1 + n 2 + n 3 = 2 n − 1 n_0=n_2+1,n_1=0,n_0=n=>n_all=n_1+n_2+n_3=2n-1 n0=n2+1,n1=0,n0=n=>nall=n1+n2+n3=2n−1
- 对同一组权值{w1 ,w2 , …… , wn},存在不同构的两棵哈夫曼树
2. 哈夫曼编码
- 引言:给定一段字符串,我们可以对字符进行编码,我们有“等长编码”和“不等长编码”两种实现方式,那么在知道每个字符的使用频率的情况下,怎么编码才能使得字符串空间花费最小,这就是我们要研究的问题。
- 根据上述的思考,我们很容易知道,这个编码问题和哈夫曼树的联系,路径长度就是每个字符编码的位数,每个字符的权值就是其出现的频率,二者相加求和就是对字符串编码所需要的空间大小。因此,为了实现最佳编码机制,我们需要构建一个哈夫曼二叉树去实现。
- 二叉树编码
- 左右分支取 0、1,且字符只存在于叶节点上
- 等长二叉树编码
- 不等长二叉树编码(通过哈夫曼树实现则代价最小)
- 哈夫曼编码是通过哈夫曼树实现的编码机制,其 ①确保编码无二义性 ②总代价最小( c o s t = Σ 1 n 频率 × 路径长度 cost=Σ_1^n频率×路径长度 cost=Σ1n频率×路径长度)
- 举例:假设四个字符频率为 a:4,u:1,x:2,z:1
- 左右分支取 0、1,且字符只存在于叶节点上
- 不等长编码的注意:要避免二义性!!!即任何字符的编码都不能是另一字符编码的前缀,否则无法解码。
- 哈夫曼编码举例