哈夫曼树的定义
结点的权
在许多应用中,树中结点常常被赋予一个表示某种意义的数值,称为该结点的权。
路径长度(Path Length)
对于树中的两个结点,它们之间的路径长度是指连接这两个结点的边的数量。换句话说,路径长度是测量从一个结点到另一个结点所需的步数。
结点的路径长度(Path Length of a Node)
对于树中的一个结点,其路径长度是指从该结点到树根之间的路径的长度。这是通过计算连接该结点到树根的路径上的边的数量来得到的。
结点的带权路径长度(Weighted Path Length of a Node)
对于树中的每个结点,其带权路径长度是指从该结点到树根之间的路径长度乘以该结点的权值。如果一个结点距离树根的步数为d,该结点的权值为w,则该结点的带权路径长度为w*d。
树的带权路径长度(Weighted Path Length of a Tree)
树的带权路径长度是指树中所有叶子结点的带权路径长度之和。每个叶子结点的带权路径长度是其到树根的路径长度乘以其权值,然后将所有叶子结点的带权路径长度相加得到整个树的带权路径长度。
哈夫曼树
在含有n个带权叶结点的二叉树中, 其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树。例如,图1中的2棵二叉树都有6个叶子结点a,b,c,d,e,f,分别带权1,2,3,4,5,6。
它们的带权路径长度分别为
(a)WPL= 1×2+2×2+3×3+4×3+5×3+6×3=60。
(b)WPL= 1×4+2×4+3×3+4×2+5×2+6×2=51。
图1的(b)树的WPL最小。可以验证,它恰好为哈夫曼树。
哈夫曼树的构造
在讲解哈夫曼树构造之前,先对上述图1带权叶结点的二叉树进行改造,非叶结点的权值等于其两个子结点的权值之和。图1的(b)树对非叶结点添加权值之后如下图2所示。
给定n个权值分别为W1,W2,…,Wn的结点,构造哈夫曼树的算法描述如下:
1)将这n个结点分别作为n棵仅含一个结点的二叉树, 构成森林F。
2)构造一个新结点,从F中选取两棵根结点权值最小的二叉树作为新结点的左、 右子树,并且将新结点的权值置为左、 右子树上根结点的权值之和。
3)从F中删除刚才选出的两棵二叉树, 同时将新得到的二叉树树加入F中。
4)重复步骤2)和3),直至F中只剩下一棵树为止。
例如, 权值{1,2,3,4,5,6}的哈夫曼树的构造过程如图3所示。
哈夫曼编码
在数据通信中,若对每个字符用相等长度的二进制位表示,称这种编码方式为固定长度编码。若允许对不同字符用不等长的二进制位表示,则这种编码方式称为可变长度编码。可变长度编码比固定长度编码要好得多,其特点是对频率高的字符赋以短编码,而对频率较低的字符则赋以较长一些的编码,从而可以使字符的平均编码长度减短,起到压缩数据的效果。哈夫曼编码是一种被广泛应用而且非常有效的数据压缩编码。
若没有一个编码是另一个编码的前缀, 则称这样的编码为前缀编码。举例: 设计字符A,B,C和D对应的编码0,10,110,和111是前缀编码。因为没有一个编码是其他编码的前缀。 对于111101100码串,做如下解码操作。
从左到右扫描码串,如果当前长度的编码在编码表中找不到,则增加长度。
- 从左到右扫描 ‘1’,1在编码表中找不到,继续累计扫描。
- 接着扫描 ‘1’,11在编码表中找不到,继续累计扫描。
- 扫描 ‘1’,111在编码表中找到为D。
- 扫描 ‘1’,1在编码表中找不到,继续累计扫描。
- 扫描 ‘0’,10在编码表中找到为B。
- 扫描 ‘1’,1在编码表中找不到,继续累计扫描。
- 扫描 ‘1’,11在编码表中找不到,继续累计扫描。
- 扫描 ‘0’,110在编码表中找到为C。
- 扫描 ‘0’,0在编码表中找到为A。
最终,我们得到解码后的字符串,这里是 ‘DBCA’。
在这个过程中,关键是要确保没有一个编码是另一个编码的前缀,这样我们就能够准确地解码。
对于字符串序列“abbcccddddeeeeeffffff”,如何得到此序列的哈夫曼编码?
-
统计字符频率并赋予权值:
- 字符 ‘a’ 出现1次,权值为1
- 字符 ‘b’ 出现2次,权值为2
- 字符 ‘c’ 出现3次,权值为3
- 字符 ‘d’ 出现4次,权值为4
- 字符 ‘e’ 出现5次,权值为5
- 字符 ‘f’ 出现6次,权值为6
-
构建小顶堆:
- 因为在后续构造哈夫曼树时需要从二叉树构成的森林中选权值最小和次小的二叉树组成新的二叉树。使用小顶堆可以很方便的从中取出最小的元素。关于小顶堆的相关介绍请参考文章Python中的heapq模块详解
- 初始时,构建包含字符及其频率的结点,每个结点都是一棵二叉树,这6棵二叉树构成森林F。用这6棵二叉树构建一个小顶堆。堆顶元素的二叉树具有最小的频率。
-
构造哈夫曼树:
- 从小顶堆中取出堆顶元素记为二叉树A,A是频率最小的二叉树。再从小顶堆中取出堆顶元素记为二叉树B,B是频率次小的二叉树。取出的A和B是6棵二叉树中频率最小的两棵。新建一个新结点,将A作为新结点的左子树,B作为新结点的右子树,新结点的频率为两个左右子树的频率之和。将以新结点为根的新二叉树重新插入小顶堆中。此时小顶堆中的二叉树减少为5个。
- 重复上述步骤,直到小顶堆中只剩下一个元素,这个二叉树即为哈夫曼树。
-
分配编码:
- 从哈夫曼树的根开始,沿着路径向下遍历每个叶子结点。在遍历的过程中,每次向左移动就给路径添加一个’0’,每次向右移动就给路径添加一个’1’。当到达叶子结点时,记录下该叶子结点对应的字符及其路径,这个路径即为该字符的哈夫曼编码。
- 重复上述步骤,对每个叶子结点都分配编码,直到所有叶子结点都被处理完。
-
得到哈夫曼编码表:
- 将每个字符及其对应的哈夫曼编码记录在一个表中,这个表就是最终的哈夫曼编码表。
对于给定的字符串序列,得到的哈夫曼编码如下:
- ‘a’ 的编码为 ‘1110’
- ‘b’ 的编码为 ‘1111’
- ‘c’ 的编码为 ‘110’
- ‘d’ 的编码为 ‘00’
- ‘e’ 的编码为 ‘01’
- ‘f’ 的编码为 ‘10’
这就是根据字符频率构建哈夫曼树并分配编码的过程。哈夫曼编码的特点是没有任何一个字符的编码是另一个字符编码的前缀,确保了在解码时能够唯一确定每个字符的编码。
以下是python语言编写的构建哈夫曼编码的代码。
import heapq
class HuffmanNode:
def __init__(self, char, frequency):
self.char = char
self.frequency = frequency
self.left = None
self.right = None
# 重写此函数是因为在heapq模型中构建小顶堆时需要比较对象之间的大小,因此需要重写lt函数,表示什么情况下哈夫曼结点对象小于另一个。
def __lt__(self, other):
return self.frequency < other.frequency
# 打印哈夫曼结点,第二参数表示本结点,第三参数表示本结点的父结点的频率值,第三参数表示本结点是父结点的左孩子还是右孩子。
def print_huffman_node(self, root, parent_frequency, is_left_or_right_child):
if root is not None:
if root.char is not None:
print(f"Character: {root.char}, \tfrequency: {root.frequency}, \tparent_frequency: {parent_frequency}, \tis_left_or_right_child: {is_left_or_right_child}")
else:
print(f"Character: None, \tfrequency: {root.frequency}, \tparent_frequency: {parent_frequency}, \tis_left_or_right_child: {is_left_or_right_child}")
self.print_huffman_node(root.left, root.frequency, "left")
self.print_huffman_node(root.right, root.frequency, "right")
def build_huffman_tree(data):
frequency = {}
for char in data:
if char in frequency:
frequency[char] += 1
else:
frequency[char] = 1
heap = [HuffmanNode(char, freq) for char, freq in frequency.items()]
heapq.heapify(heap)
count = 0
print(f"\nthis is {count} result")
for i in heap:
i.print_huffman_node(i, None, None)
print(f'Huffman Node {i.frequency} print end')
while len(heap) > 1:
node1 = heapq.heappop(heap)
node2 = heapq.heappop(heap)
merged_node = HuffmanNode(None, node1.frequency + node2.frequency)
merged_node.left = node1
merged_node.right = node2
# merged_node.print_huffman_node(merged_node, None, None)
heapq.heappush(heap, merged_node)
count += 1
print(f"\nthis is {count} result")
for i in heap:
i.print_huffman_node(i, None, None)
print(f'Huffman Node {i.frequency} print end')
return heap[0]
def print_huffman_codes(root, code=""):
if root is not None:
if root.char is not None:
print(f"Character: {root.char}, frequency: {root.frequency}, Code: {code}")
print_huffman_codes(root.left, code + "0")
print_huffman_codes(root.right, code + "1")
# 示例用法
data = "abbcccddddeeeeeffffff"
root_node = build_huffman_tree(data)
print("huffman tree:")
root_node.print_huffman_node(root_node, None, None)
print("huffman code:")
print_huffman_codes(root_node)
在构造哈夫曼树的过程中,小顶堆的元素变化如下所示。
哈夫曼编码如下所示。