第16章《贪心算法》:赫夫曼树,python实现

贪心算法

求解最优化问题的算法通常需要经过一系列的步骤,在每个步骤都面临多种选择。对于许多最优化问题,使用动态规划算法来求最优解有些杀鸡用牛刀了,可以使用更简单,更高效的算法。贪心算法(greedy algorithm)就是这样的算法,它在每一步都做出当时看起来最佳的选择,也就是说,它总是做出局部最优的选择。

贪心选择性质

我们可以通过做出局部最优(贪心)选择来构造全局最优解,当进行选择时,我们直接做出当前问题中看来最优的选择,而不必考虑子问题的解,一个贪心算法通常是自顶向下的,进行一次又一次选择,将给定问题实例变得更小。

赫夫曼编码

赫夫曼编码可以很有效地压缩数据。假定我们希望压缩一个10万个字符的数据文件,如下图给出了文件中所出现的字符和它们出现的频率,也就是说文件中只出现6个不同的字符,其中字符 a a a出现了45000次。
在这里插入图片描述
如果用用定长编码,指定一个3位的码字,我们可以将文件编码为300000位的长度,但使用上表所示的变长编码,仅用224000位编码文件。 ( 45 ∗ 1 + 13 ∗ 3 + 12 ∗ 3 + 16 ∗ 3 + 9 ∗ 4 + 5 ∗ 4 ) ∗ 1000 = 224000 (45* 1+13*3+12*3+16*3+9*4+5*4)*1000=224000 (451+133+123+163+94+54)1000=224000对应的二叉树表示如下,每个叶节点标记了一个字符及其出现的频率。每个内部节点标记了其子树中叶节点的频率之和。(a)对应定长编码 a = 000 , . . . , f = 101 a=000,...,f=101 a=000,...,f=101的二叉树,(b)对应最优前缀码 a = 0 , b = 101 , . . . , f = 1100 a=0,b=101,...,f=1100 a=0,b=101,...,f=1100的二叉树。

在这里插入图片描述
给定一棵对应的qian’zhui前缀码的树 T T T,我们可以容易地计算出编码一个文件需要多少个二进制位,对于字母表 C C C中的每个字符 c c c,令属性 c . f r e q c.freq c.freq表示 c c c在文件中出现的频率,令 d T ( c ) d_T(c) dT(c)表示 c c c的叶节点在树中的深度。则编码文件需要 B ( T ) = ∑ c ∈ C c . f r e q . d T ( c ) B(T)=\sum_{c \in C}c.freq.d_T(c) B(T)=cCc.freq.dT(c)个二进制位,我们将 B ( T ) B(T) B(T)定义为 T T T的代价。

构造赫夫曼编码

赫夫曼设计了一个贪心算法来构造最优前缀码,被称为赫夫曼编码(Huffman code)。我们假定 C C C是一个 n n n个字符的集合,而其中每个字符 c ∈ C c \in C cC都是一个对象,其属性 c . f r e q c.freq c.freq给出了字符的出现频率。算法自底向上地构造出对应的最优编码的二叉树 T T T。它从 ∣ C ∣ |C| C个叶节点开始,执行 ∣ C ∣ − 1 |C|-1 C1个“合并”操作创建出最终的二叉树。如下图,赫夫曼算法执行过程:
在这里插入图片描述
python实现赫夫曼编码如下:

# -*-coding:utf8 -*-
import sys

#构造节点
class Node(object):
	def __init__(self, key, code=0, parent=None,lchild=None, rchild=None):
		self.key = key
		self.code = code
		self.parent = parent
		self.lchild = lchild
		self.rchild = rchild


class HuffmanTree(object):
	def __init__(self,root=None):
		self.huffman_tree = []
		self.root = root
	#找两个key值最小的节点
	def find2node(self, flag):
		mini = []
		for i in range(len(self.huffman_tree)):
			if i in flag:
				continue
			if len(mini) < 2:
				mini.append(i)
			else:
				if self.huffman_tree[i].key < max([self.huffman_tree[mini[0]].key, self.huffman_tree[mini[1]].key]):
					pos = 0 if self.huffman_tree[mini[0]].key > self.huffman_tree[mini[1]].key else 1
					mini[pos] = i
		# let lchird.key < rchild.key
		if self.huffman_tree[mini[0]].key > self.huffman_tree[mini[1]].key:
			temp = mini[0]
			mini[0] = mini[1]
			mini[1] = temp
		#return sorted(mini)
		return mini
 

	def build(self, C):
		#flag记录已经合并的节点
		flag = []		
		#初始化各关键字key节点
		n = len(C)
		for key in C:
			self.huffman_tree.append(Node(key))
		#构建hufuman,n个值,需要n-1次合并操作
		for i in range(n,2*n-1):
			mini = self.find2node(flag)
			print(str(mini[0])+'\t'+str(mini[1]))		
			#合并的节点记录,下次合并不需要考虑		
			flag.append(mini[0])
			flag.append(mini[1])
			#构建新的Node节点,key为两个子节点key相加
			key = self.huffman_tree[mini[0]].key + self.huffman_tree[mini[1]].key
			node = Node(key,lchild=self.huffman_tree[mini[0]],rchild=self.huffman_tree[mini[1]])
			#新节点加入
			self.huffman_tree.append(node)
			self.huffman_tree[mini[0]].parent = node
			self.huffman_tree[mini[1]].parent = node
			self.huffman_tree[mini[1]].code = 1	

		#记录root节点
		print(node.key)
		self.root = node  
		
	#中序遍历
	def inorder_tree_walk(self, tree):
		if tree is not None:
			self.inorder_tree_walk(tree.lchild)
			print(tree.key, end=" ")
			self.inorder_tree_walk(tree.rchild)

	#获取节点的编码
	def huffman_code(self, cur):
		path = []
		while cur.parent!=None:
			#path结果保存从根节点搜索路径的编码结果
			path.insert(0,cur.code)
			cur = cur.parent
		return path	
				 
if __name__=='__main__':
	tree =  HuffmanTree()
	C = [5,9,12,13,16,45]
	tree.build(C)
	tree.inorder_tree_walk(tree.root)
	print()
	#打印每个叶子节点的赫夫曼编码
	for i in range(len(C)):
		path = tree.huffman_code(tree.huffman_tree[i])
		print('key='+str(tree.huffman_tree[i].key))
		print(path)	

具体代码,可参考github地址算法导论各章节算法python实现

猜你喜欢

转载自blog.csdn.net/BGoodHabit/article/details/106068573