【数据结构】哈夫曼树与哈夫曼编码的Python实现及应用

最近数据结构课布置的作业:
用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%。

看来就算对结点数很少的数据,哈弗曼编码还是能压缩的。

最后有一个问题:为啥哈弗曼树就是最优树?据说要归纳证明,很麻烦。

你可能感兴趣的:(数据结构学习)