Python实现霍夫曼树

Python实现霍夫曼树

霍夫曼树是一种特殊的二叉树,是一种带权路径长度最短的二叉树,又称为最优二叉树。

给定 N 个权值作为二叉树的 N 个叶节点的权值,构造一棵二叉树,若该二叉树的带权路径长度达到最小,则称该二叉树为霍夫曼树。

霍夫曼树中权值越大的节点离根越近。

霍夫曼树主要应用于信息编码和数据压缩领域,是现代压缩算法的基础。

一、霍夫曼树的相关术语

霍夫曼树要满足带权路径长度最小,那就要知道什么是权值?什么是路径?什么是带权路径长度?

1. 路径

在一棵树中,从一个节点往下可以到达子节点或子孙节点的通路,称为路径。

Python实现霍夫曼树_第1张图片

2. 节点的权值

在具体的应用场景中,二叉树的每个节点对应着具体的业务含义,每个节点有不同的权重,节点的权重值被称为节点的权值。

如下图中节点C的权值为5。

Python实现霍夫曼树_第2张图片

3. 节点的路径长度

根节点处于二叉树的第1层,则二叉树中第L层的节点的路径长度为 L-1 。

如上图中节点 F 处于二叉树的第三层,则 F 的路径长度为 3-1 = 2 。

节点的路径长度也可以这么理解,如果每个节点到达它的子节点的路径记为一个路径单元,则从根节点开始,到达某节点的路径单元数称为该节点的路径长度。

4. 节点的带权路径长度

节点的权值与节点的路径长度的乘积称为该节点的带权路径长度。

如上图中节点 D 的权值为 18、路径长度为 2,则节点 D 的带权路径长度为 18*2 = 36。

5. 树的带权路径长度

树的所有叶节点的带权路径长度之和,称为树的带权路径长度,记为WPL(Weighted Path Length of Tree)。

如上图中的二叉树的带权路径长度为 WPL = 18*2 + 7*2 + 6*2 + 17*2 = 96

二、霍夫曼树的构造过程

给定 N 个权值作为二叉树的 N 个叶节点的权值,构造一棵二叉树,可以构建出多种不同结构的二叉树,不同结构的二叉树的带权路径长度不一定相等。只有当二叉树的带权路径长度最小时,二叉树才是霍夫曼树。

如给定 3,5,7,13 四个权值作为叶节点的权值,构造四个叶节点的二叉树可以有很多种不同的结构,下面例举出了其中两种,左边的二叉树的带权路径长度为 3*3 + 5*2 + 7*2 + 13*2 = 59,右边的二叉树的带权路径长度为 13*1 + 7*2 + 3*3 + 5*3 = 51。根据霍夫曼树的特性,权值越大的节点离根越近,也就是说权值越大的节点路径越短,下图中右边的二叉树就是一棵霍夫曼树,树的带权路径长度已经达到了最小。

Python实现霍夫曼树_第3张图片

那么,怎么根据给定的叶节点权值构建一棵霍夫曼树呢?在构造霍夫曼树前,先推导一些霍夫曼树的一般属性。

1. 要保证构造出来的二叉树是霍夫曼树,就要让权值大的节点的路径尽量短,反过来,权值小的节点就只能处于二叉树的更高层,把路径短的位置让给权值大的节点。从局部看,只要保证每个节点的路径都不大于权值比它小的节点即可,所以可以使用贪婪算法。

2. 霍夫曼树中不会存在只有一个子节点的节点。假设霍夫曼树中存在只有一个子节点的节点,如果删除该节点,它的子树中的所有叶节点的路径长度都可以减1,可以构造出带权路径长度更小的二叉树,这与霍夫曼树的定义矛盾,所以假设不成立。

3. 如果霍夫曼树只有两个叶节点,则两个叶节点的路径相等,都为 1。

根据这些属性,开始构造霍夫曼树,步骤如下:

1. 将有 N 个叶节点的树看成 N 棵树的森林(每棵树仅有一个根节点)。

以 3,5,7,13 为例。

Python实现霍夫曼树_第4张图片

2. 从森林中选出根节点权值最小的两棵树,分别作为新树的左右子树(这样构造新树满足霍夫曼树),且新树的根节点权值为其左右子树根结点的权值之和。然后将被合并的两棵树从森林中删除,将新树添加到森林中。

从中选出最小的 3 和 5,合并成一棵霍夫曼树,然后将新树添加到森林中。

Python实现霍夫曼树_第5张图片

3. 重复步骤 2 ,直到森林中只剩一棵树为止,最后的树即为霍夫曼树。

为了保证霍夫曼树的结构唯一,本文中每次合并时都将根节点权值小的树作为左子树,根节点权值大的树作为右子树。这个可以自己定,因为只要树的带权路径长度达到了最小,不管什么结构,都是霍夫曼树,霍夫曼树不是唯一的。

继续选出最小的 7 和 8,合并。
Python实现霍夫曼树_第6张图片

最终得到的霍夫曼树结构如下。

Python实现霍夫曼树_第7张图片

现在验证一下,树的带权路径长度为 WPL = 13*1 + 7*2 + 3*3 + 5*3 = 51,权值越大的节点路径越短,所以这是一棵霍夫曼树。

三、Python实现霍夫曼树

1. 代码准备

# coding=utf-8
class Node(object):
    def __init__(self, data):
        self.data = data
        self.parent = None
        self.left_child = None
        self.right_child = None
        self.is_in_tree = False


class HuffmanTree(object):
    """霍夫曼树"""
    def __init__(self):
        self.__root = None
        self.prefix_branch = '├'
        self.prefix_trunk = '|'
        self.prefix_leaf = '└'
        self.prefix_empty = ''
        self.prefix_left = '─L─'
        self.prefix_right = '─R─'

    def is_empty(self):
        return not self.__root

    @property
    def root(self):
        return self.__root

    @root.setter
    def root(self, value):
        self.__root = value if isinstance(value, Node) else Node(value)

    def show_tree(self):
        if self.is_empty():
            print('空二叉树')
            return
        print('-' * 20)
        print(self.__root.data)
        self.__print_tree(self.__root)
        print('-' * 20)

    def __print_tree(self, node, prefix=None):
        if prefix is None:
            prefix, prefix_left_child = '', ''
        else:
            prefix = prefix.replace(self.prefix_branch, self.prefix_trunk)
            prefix = prefix.replace(self.prefix_leaf, self.prefix_empty)
            prefix_left_child = prefix.replace(self.prefix_leaf, self.prefix_empty)
        if self.has_child(node):
            if node.right_child is not None:
                print(prefix + self.prefix_branch + self.prefix_right + str(node.right_child.data))
                if self.has_child(node.right_child):
                    self.__print_tree(node.right_child, prefix + self.prefix_branch + ' ')
            else:
                print(prefix + self.prefix_branch + self.prefix_right)
            if node.left_child is not None:
                print(prefix + self.prefix_leaf + self.prefix_left + str(node.left_child.data))
                if self.has_child(node.left_child):
                    prefix_left_child += '  '
                    self.__print_tree(node.left_child, self.prefix_leaf + prefix_left_child)
            else:
                print(prefix + self.prefix_leaf + self.prefix_left)

    def has_child(self, node):
        return node.left_child is not None or node.right_child is not None

先创建一个节点类 Node,用于创建霍夫曼树的节点,这里要注意一点,因为在构造霍夫曼树时,要不断从一个森林中选根节点最小的两棵树进行合并,所以在节点里添加一个标志位,is_in_tree,如果为 True 则表示该树已经合并到霍夫曼树中了,不会重复取。

提前实现一个霍夫曼树的类 HuffmanTree ,先准备了一个按树形结构打印霍夫曼树的方法 show_tree() 。

下面根据霍夫曼树的构造过程,实现霍夫曼树的构造方法。

    def huffman(self, leavers):
        """构造霍夫曼树"""
        if len(leavers) <= 0:
            return
        if len(leavers) == 1:
            self.root = Node(leavers[0])
            return
        woods = list()
        for i in range(len(leavers)):
            woods.append(Node(leavers[i]))
        while len(woods) < 2*len(leavers) - 1:
            node1, node2 = Node(float('inf')), Node(float('inf'))
            for j in range(len(woods)):
                if node1.data > node2.data:
                    node1, node2 = node2, node1
                if woods[j].data < node1.data and woods[j].is_in_tree is False:
                    node1, node2 = woods[j], node1
                elif node1.data <= woods[j].data < node2.data and woods[j].is_in_tree is False:
                    node2 = woods[j]
            parent_node = Node(node1.data + node2.data)
            woods.append(parent_node)
            parent_node.left_child, parent_node.right_child = node1, node2
            self.root, node1.parent, node2.parent = parent_node, parent_node, parent_node
            node1.is_in_tree, node2.is_in_tree = True, True

huffman(leavers): 构造霍夫曼树。构造霍夫曼树时会给定 N 个权值,如果 N<=0,则直接返回。如果 N=1,则直接将该权值作为根节点的权值,即可构建出霍夫曼树。如果 N>=2,则先将这 N 个权值作为根节点的权值构建一个包含 N 棵树的森林,再从森林中选根节点权值最小的两棵树进行合并,一直循环直到只剩一棵树。

在这个方法中,有以下几点需要注意,否则很容易出错:

1. 代码里为了方便处理,并没有将被合并的树从列表 woods 中删除(删除操作很麻烦,尤其权值相等时),而是通过修改根节点的标志位 is_in_tree,如果 is_in_tree 为 True,表示该树已经被合并了,不会重复取。

那怎么判断霍夫曼树已经构造完成了呢,当 woods 中只剩一棵树的根节点的标志位 is_in_tree 为 False 时,但是,这样不好判断,每次合并后都要判断一遍 woods 中的根节点标志位。根据前面分析的属性,使用贪婪算法,每次合并树时,新的树都是一棵局部的霍夫曼树,并且霍夫曼树中不会存在只有一个子节点的节点。在构造霍夫曼树的过程中,每个节点都作为一棵树的根节点被添加到森林 woods 中了,所以 woods 的长度等于霍夫曼树的节点数,当 woods 的长度达到霍夫曼树的节点总数时,霍夫曼树就构造完成。

霍夫曼树中除了叶节点,其他节点都有两个子节点,根据二叉树的特性,当叶节点数为 N 时,有两个子节点的节点数为 N+1,所以有 N 个叶节点的霍夫曼树的节点数为 2*N + 1 。

2. 为了获取到权值最小的两个根节点,提前声明了两个变量 node1 和 node2 ,这两个变量可以提前赋值为两个权值很大的节点,建议直接使用正无穷大 float('inf'),如果一开始赋值为 woods 中的第一个和第二个,当第一个或第二个值就是最小值时,可能会每次循环都取到相同的值。

3. 在寻找权值最小的两个根节点值时,如果当前节点 node 的权值小于 node1 的权值,node.data < node1.data ,则将 node1 赋值给 node2,将 node 赋值给 node1,如果当前节点 node 的权值大于等于 node1 的权值同时小于 node2 的权值,node1.data <= node.data < node2.data,则将 node 赋值给 node2。这里很容易忽略第二种情况。

if __name__ == '__main__':
    tree = HuffmanTree()
    leavers = [11, 5, 7, 13, 17, 11]
    tree.huffman(leavers)
    tree.show_tree()

运行结果:

--------------------
64
├─R─39
| ├─R─22
| | ├─R─11
| | └─L─11
| └─L─17
└─L─25
  ├─R─13
  └─L─12
    ├─R─7
    └─L─5
--------------------

给定的 N 个叶节点权值为 [11, 5, 7, 13, 17, 11] ,霍夫曼树结构如下图。

Python实现霍夫曼树_第8张图片

该霍夫曼树的带权路径长度为 WPL = 13*2 + 17*2 + 5*3 + 7*3 + 11*3 + 11*3 = 162 。

 

Python实现霍夫曼树_第9张图片

 

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