算法原理

由上图来说明霍夫曼编码的原理。首先,假设我们要进行编码的字符序列为"AAAAAAABBCCCCDDDD",统计这些字符出现的频率,然后将这些字符按频率从低到高排序,并且存放在列表list1中。然后,从list1中取出频率最低的两个字符,分别作为左子树和右子树构成一个新的节点,并将新的节点放在list1中,同时删除刚刚从list1中取出的两个字符。以此类推,直到list1中只剩一个节点时停止,如此便可构成一棵霍夫曼树。其次,就可以用构造的霍夫曼树来完成霍夫曼编码。从根节点开始,给连接左子树的路径赋值为0,给连接*右子树的路径赋值为1,这样就可以完成编码。具体细节如上图所示。

代码

import functools

class Node:
    def __init__(self, data, left, right):
        self.data = data
        self.left = left
        self.right = right

def node_cmp(node1, node2):
    """
    此函数用于存储节点的列表自定义排序
    node1,node2: Node类型的节点
    """
    if node1.data[1] > node2.data[1]:
        return 1
    elif node1.data[1] < node2.data[1]:
        return -1
    return 0

def init(strList):
    char_frequence = {}
    for s in strList:
        if char_frequence.get(s) == None:
            char_frequence[s] = 1
        else:
            char_frequence[s] += 1
    nodeList = []       # 将{'A', 2},即字符+频率的数据,以节点的方式存储在列表中。
    for key in char_frequence:
        nodeList.append(Node((key, char_frequence[key]), None, None))
    nodeList = sorted(nodeList, key=functools.cmp_to_key(node_cmp)) # 将节点进行排序
    return nodeList

def create_Hoffman_Tree(nodeList):
    for n in nodeList:
        print(n.data)

    while len(nodeList) >= 2:
        new_node = Node( (nodeList[0].data[0]+nodeList[1].data[0], 
                        nodeList[0].data[1]+nodeList[1].data[1]), nodeList[0], nodeList[1] )
        nodeList.pop(0)
        nodeList.pop(0)     # 删除用过的前两个节点
        nodeList.append(new_node)   # 将新生成的节点添加进列表
        nodeList = sorted(nodeList, key=functools.cmp_to_key(node_cmp)) # 重新排序

    root = nodeList[0]      # 最终列表内只剩一个节点,即为霍夫曼树的根节点
    # print(root.right.right.right.data)
    # while root.right != None and root.left != None:
    return root

def traverse_tree(root, s, hoffman_code):
    """
    "先序"遍历霍夫曼树,找到叶子节点(即原字符),并进行编码
    root: 根节点
    s : 01编码序列
    hoffman_code:最终返回的霍夫曼编码结果
    """
    if root != None:
        if root.right == None and root.left == None:
            # print(root.data)
            # print(s)
            hoffman_code[root.data[0]] = s
            s = s[:-1]  # 
        s += "0"
        traverse_tree(root.left, s, hoffman_code)
        s = s[:-1]      # 左子树遍历完去遍历右子树时,需要将前一个添加的0,1删除
        s += "1"
        traverse_tree(root.right, s, hoffman_code)

if __name__ == '__main__':
    strList = input("请输入字符序列:").lower()
    print('字符序列长度:', len(strList))
    nodeList = init(strList)
    root = create_Hoffman_Tree(nodeList)
    hoffman_code = {}
    traverse_tree(root, "", hoffman_code)
    print(hoffman_code.items())

运行结果如下

在代码中有些需要注意的点,我在此说明一下。

首先,在获得输入字符序列的字符频率之后,直接将这些字符以及其频率都以Node数据类型(见代码第3行)存储在列表中,方便排序以及之后霍夫曼树的构造。

其次,在构造霍夫曼树的时候,每次选择已排序的节点列表的前两个节点作为左子树和右子树构成一个新的节点(或者说是新的二叉树)。之后要将用过的前两个节点删除,将新的节点添加进列表并排序。

最后,在进行编码的过程中,我采用了“先序”遍历的方式遍历整课棵霍夫曼树,找到叶子节点,并得到相应的编码结果。在代码中出现的 s 变量,即为每个叶子节点编码的结果。至于在代码中 s 变量添加0,1 或者删除最末端的0,1。这一点时根据递归算法的特性来进行选择的,在我编程的过程中也是试过很多错,最终找到一个正确的方式。这也许就是递归的魅力所在吧,简洁易懂的递归背后可能是十几次试错之后的结果。

END.

Last modification:June 28th, 2020 at 11:25 am
如果觉得我的文章对你有用,请随意赞赏