最近数据结构课布置的作业:
用python实现哈夫曼树与哈夫曼编码,并撰写该大作业的实验报告。报告要求说明算法原理、算法的实现、测试案例说明、测试结果与分析。测试案例要求来源于日常生活或专业应用。
看了一些文章发现编码有点奇奇怪怪,于是就按自己的理解写了一遍。过程中曾试图用树遍历的方式进行哈夫曼编码,但是好像挺麻烦,还不如给结点加个parent属性,自底向上编码。
简单回顾下思路——
首先是树的构建,从一堆叶结点中找出俩权值最小的,同时创建新结点,新结点是俩叶结点的父结点,权值为俩权之和。然后从这堆叶结点中删去这俩,加入新结点,重复操作,直到只剩俩结点,然后将这俩结点连到树的根结点上,至此树的创建完毕。
其次是编码,从这堆叶结点里再次取出一个又一个叶结点p,初始编码code=’’,并且检测p.parent是不是root,如果不是,那么就检测p是p.parent的左还是右子结点,左就code = ‘0’ +code,右就code = ‘1’ + code,然后p = p.parent,继续重复上述检测;如果p.parent是root,那么输出code。
最后是应用,我用来存储DNA或RNA序列,具体可以看代码。
代码如下——
class node(object): #创建霍夫曼树结点类
def __init__(self):
self.name = None
self.weight = None
self.leftchild = None
self.rightchild = None
self.parent = None
class code_node(object): #创建编码结点类
def __init__(self, code, name, weight):
self.code = code
self.name = name
self.weight = weight
class HaffumanTree(object): #哈夫曼树类
def __init__(self):
self.root = node()
def buildtree(self, name_set, weight_set): #创建哈夫曼树
self.nodes = [] #以降序存储新结点
while len(name_set) > 0:
max_weight = max(weight_set)
max_pos = weight_set.index(max_weight)
max_name = name_set[max_pos]
newnode = node()
newnode.name = max_name
newnode.weight = int(max_weight) #权值为整型
self.nodes.append(newnode)
name_set.remove(max_name)
weight_set.remove(max_weight) #创建新结点,填入name和weight属性,且按降序入列
self.nodes_copy = self.nodes[:]
#备份self.nodes,用于自底向上编码,不能使用self.nodes_copy = self.nodes,这会导致两者始终同步
while len(self.nodes) > 2:
l = self.nodes.pop()
r = self.nodes.pop()
newnode = node()
newnode.weight = l.weight + r.weight
newnode.leftchild = l
newnode.rightchild = r
l.parent = newnode
r.parent = newnode
self.insert(newnode) #将新结点根据weight大小插入self.nodes
left = self.nodes.pop()
right = self.nodes.pop()
self.root.weight = left.weight + right.weight
self.root.leftchild = left
self.root.rightchild = right
left.parent = self.root
right.parent = self.root #列表中最后两个结点连接Haffuman树的根节点
def insert(self, newnode):
for i in range(len(self.nodes)):
if self.nodes[i].weight < newnode.weight:
self.nodes.insert(i, newnode)
return
self.nodes.append(newnode)
def make_code(self): #对不同字符编码
self.codes = []
while len(self.nodes_copy) > 0:
code = ''
precode_node = self.nodes_copy.pop()
t = precode_node
while t.parent != self.root: #自底向上编码
p = t.parent
if t == p.leftchild:
code = '0' + code
elif t == p.rightchild:
code = '1' + code
t = t.parent
if t == self.root.leftchild:
code = '0' + code
elif t == self.root.rightchild:
code = '1' + code
self.codes.append(code_node(code, precode_node.name, precode_node.weight)) #编码结点存入编码库self.nodes
def print_code_table(self): #输出字符与编码对应表
print ("编码表:")
for i in range(len(self.codes)):
print(self.codes[i].name+'->'+self.codes[i].code)
def data_preprocessing(self): #输入DNA或RNA序列,并进行预处理
print ("请输入数字要压缩序列类型:1-DNA 2-RNA")
option = int(input("<<"))
_data = input("请输入序列信息:")
self.data = list(_data) #数据转列表,单个字符存储
self.base_num = [0 for i in range(4)] #按ATGC或AUGC顺序存储各碱基数目进入num中
self.base_name = []
if option == 1:
if 'U' in self.data:
print("序列非DNA序列")
return self.data_preprocessing()
else:
for i in range(len(self.data)):
if self.data[i] == 'A':
self.base_num[0] += 1
elif self.data[i] == 'T':
self.base_num[1] += 1
elif self.data[i] == 'G':
self.base_num[2] += 1
elif self.data[i] == 'C':
self.base_num[3] += 1
self.base_name = ['A', 'T', 'G', 'C']
elif option == 2:
if 'T' in self.data:
print("序列非RNA序列")
return self.data_preprocessing()
else:
for i in range(len(self.data)):
if self.data[i] == 'A':
self.base_num[0] += 1
elif self.data[i] == 'U':
self.base_num[1] += 1
elif self.data[i] == 'G':
self.base_num[2] += 1
elif self.data[i] == 'C':
self.base_num[3] += 1 #统计序列中各碱基的数目
self.base_name = ['A', 'U', 'G', 'C']
# self.base_name 即为 name_set,self.base_num 为 weight_set
def data_make_code(self): #制作编码并输出编码表
self.buildtree(self.base_name, self.base_num)
self.make_code()
self.print_code_table()
def compress(self): #压缩
self.compress_data = []
for i in range(len(self.data)):
for j in range(len(self.codes)):
if self.data[i] == self.codes[j].name:
self.compress_data.append(self.codes[j].code)
def print_compress(self): #输出压缩码
print ("压缩码为:")
p = ''.join(self.compress_data)
print (p)
def uncompress(self): #解压缩
print("请输入数字选择要解压缩序列类型:1-DNA 2—RNA")
un_option = int(input("<<"))
print("请输入各碱基的编码")
reference = [] #碱基编码对照表
reference_DNA_base_name = ['A', 'T', 'G', 'C']
reference_RNA_base_name = ['A', 'U', 'G', 'C']
if un_option == 1:
a = input("A:")
t = input("T:")
g = input("G:")
c = input("C:")
reference = [a, t, g, c]
if un_option == 2:
a = input("A:")
u = input("U:")
g = input("G:")
c = input("C:")
reference = [a, u, g, c]
print("请输入要解压的编码串")
uncompress_code = input()
result = ''
while len(uncompress_code) > 0:
for i in range(len(reference)):
if self.cmp_ok(uncompress_code, reference[i]) == True:
h = len(reference[i])
uncompress_code = uncompress_code[h:]
if un_option == 1:
result = result + reference_DNA_base_name[i]
elif un_option == 2:
result = result + reference_RNA_base_name[i]
print ("解压结果为")
print (result)
def cmp_ok(self, a, b): #从头比较,判断b是否为a的子字符串
l = len(b)
k = 0
if len(a) < len(b):
return False
while k < l:
if a[k] == b[k]:
k += 1
else:
return False
return True
if __name__ == '__main__':
htree = HaffumanTree() #创建Haffuman空树
htree.data_preprocessing() #输入序列信息并预处理
htree.data_make_code() #制作编码并输出编码表
htree.compress() #压缩
htree.print_compress() #输出压缩码
htree.uncompress() #解压缩
因为就是交个作业,所以就懒得再修了,测试结果如下图
结果分析——
原始长度 | 使用哈夫曼编码的压缩码长度 | 使用定长编码的压缩码长度 | |
---|---|---|---|
DNA序列 | 305个字符 | 451个字符 | 610个字符 |
原始数据存储大小为305 * 8b = 2440b;使用哈夫曼编码压缩后大小为451b,压缩率18.48%,使用定长编码压缩后大小为610b,压缩率25.0%。
看来就算对结点数很少的数据,哈弗曼编码还是能压缩的。
最后有一个问题:为啥哈弗曼树就是最优树?据说要归纳证明,很麻烦。