1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        Huffman樹與Huffman編碼的Python實(shí)現(xiàn)

        共 20930字,需瀏覽 42分鐘

         ·

        2024-05-07 22:32

        在之前的文章二叉樹的Python實(shí)現(xiàn)二叉搜索樹(BST)的Python實(shí)現(xiàn)中,筆者分別介紹了二叉樹與二叉搜索樹的Python實(shí)現(xiàn)。本文將在此基礎(chǔ)上,介紹Huffman樹和Huffman編碼的實(shí)現(xiàn)。

        Huffman樹

        首先,我們先來看下什么是

        給定N個(gè)權(quán)值作為N個(gè)葉子結(jié)點(diǎn),構(gòu)造一棵二叉樹,若該樹的帶權(quán)路徑長度達(dá)到最小,稱這樣的二叉樹為最優(yōu)二叉樹,也稱為哈夫曼樹(Huffman Tree)。哈夫曼樹是帶權(quán)路徑長度最短的樹,權(quán)值較大的結(jié)點(diǎn)離根較近。

        其中涉及到的概念如下:

        • 樹的帶權(quán)路徑長度(WPL):樹中所有葉子結(jié)點(diǎn)的帶權(quán)路徑長度之和

        • 結(jié)點(diǎn)的帶權(quán)路徑長度:是指該結(jié)點(diǎn)到樹根之間的路徑長度與該結(jié)點(diǎn)上權(quán)的乘積

        我們來看個(gè)例子:

        葉子結(jié)點(diǎn)為a,b,c,d,e,圖中這顆樹的帶權(quán)路徑長度為

        4 * 2 + 4 * 2 + 2 * 3 + 3 * 3 + 7 *2 = 45

        那么,如何來構(gòu)建Huffman樹呢?

        Huffman樹的構(gòu)造(Huffman Algorithm)算法如下:

        1. 根據(jù)給定的n個(gè)權(quán)值{w1,w2,…,wn}構(gòu)成二叉樹集合F={T1,T2,…,Tn},其中每棵二叉樹Ti中只有一個(gè)帶權(quán)為wi的根結(jié)點(diǎn),其左右子樹為空;

        2. 在F中選取兩棵根結(jié)點(diǎn)權(quán)值最小的樹作為左右子樹構(gòu)造一棵新的二叉樹,且置新的二叉樹的根結(jié)點(diǎn)的權(quán)值為左右子樹根結(jié)點(diǎn)的權(quán)值之和;

        3. 在F中刪除這兩棵樹,同時(shí)將新的二叉樹加入F中;

        4. 重復(fù)2、3, 直到F只含有一棵樹為止(得到哈夫曼樹)。

        利用我們之前實(shí)現(xiàn)的二叉樹代碼(參考文章 二叉搜索樹(BST)的Python實(shí)現(xiàn) 中的二叉樹實(shí)現(xiàn)代碼),我們接著使用Python代碼來實(shí)現(xiàn)Huffman樹

        from binary_tree import BTree

        values = [719263232110]
        tree_list = [BTree(i) for i in values]

        while len(tree_list) != 1:
            # 每個(gè)節(jié)點(diǎn)的數(shù)值
            values = [tree.data for tree in tree_list]
            # 從Node_list取兩個(gè)節(jié)點(diǎn)數(shù)值最小的節(jié)點(diǎn),并刪除這兩個(gè)節(jié)點(diǎn)
            min_value_tree = tree_list.pop(values.index(min(values)))
            values.pop(values.index(min(values)))
            sec_min_value_tree = tree_list.pop(values.index(min(values)))
            values.pop(values.index(min(values)))

            # 創(chuàng)建二叉樹
            root_value = min_value_tree.data + sec_min_value_tree.data
            root = BTree(root_value)
            root.left = min_value_tree
            root.right = sec_min_value_tree

            # 在原來的Node_list中添加新的root節(jié)點(diǎn)
            tree_list.append(root)

        root.print_tree(label=True)
        # 前序遍歷,中,左,右
        root.pre_order()
        print()
        # 中序遍歷,左,中,右
        root.in_order()
        print()
        # 后序遍歷,左,右,中
        root.post_order()
        print()
        # 層次遍歷
        print(root.level_order())

        構(gòu)建的Huffman樹如下:

        huffman tree

        輸出結(jié)果為:

        100 40 19 21 60 28 11 5 2 3 6 17 7 10 32 
        19 40 21 100 2 5 3 11 6 28 7 17 10 60 32 
        19 21 40 2 3 5 6 11 7 10 17 28 32 60 100 
        [[100], [4060], [19212832], [1117], [56710], [23]]

        Huffman編碼

        Huffman樹的一個(gè)重要應(yīng)用是Huffman編碼,而Huffman編碼常用于數(shù)據(jù)解壓縮。

        Huffman coding

        在實(shí)際使用Huffman數(shù)進(jìn)行編碼過程中,我們可以考慮對(duì)基本的文字單位(比如字母、漢字或者NLP中的token)按出現(xiàn)次數(shù)作為結(jié)點(diǎn)權(quán)重,去構(gòu)建一顆Huffman樹。此時(shí),在這棵樹中,以左子樹路徑為0,以右子樹路徑為1。

        這樣我們就能得到葉子結(jié)點(diǎn)的編碼,此時(shí)該編碼為前綴編碼(任何一個(gè)字符的編碼都不能是另一個(gè)字符編碼的前綴)。

        我們來看下面的例子(以字符串ABCACCDAEAE為例):

        首先我們對(duì)二叉搜索樹(BST)的Python實(shí)現(xiàn)中的二叉樹進(jìn)行改成,新增獲取葉子結(jié)點(diǎn)方法以及二叉樹可視化方法修改(將標(biāo)簽L, R改為0, 1),如下:

        # @file: binary_tree.py
        class BTree(object):
            # 初始化
            def __init__(self, data=None, left=None, right=None):
                self.data = data    # 數(shù)據(jù)域
                self.left = left    # 左子樹
                self.right = right  # 右子樹
                self.dot = Digraph(comment='Binary Tree')

            ...

            # 獲取葉子節(jié)點(diǎn)
            @property
            def leaves(self):
                if self.data is None:
                    return []
                if self.left is None and self.right is None:
                    return [self]
                return self.left.leaves + self.right.leaves

            # 利用Graphviz進(jìn)行二叉樹的可視化
            def print_tree(self, save_path='./Binary_Tree.gv', label=False):

                # colors for labels of nodes
                colors = ['skyblue''tomato''orange''purple''green''yellow''pink''red']

                # 繪制以某個(gè)節(jié)點(diǎn)為根節(jié)點(diǎn)的二叉樹
                def print_node(node, node_tag):
                    # 節(jié)點(diǎn)顏色
                    color = sample(colors,1)[0]
                    if node.left is not None:
                        left_tag = str(uuid.uuid1())            # 左節(jié)點(diǎn)的數(shù)據(jù)
                        self.dot.node(left_tag, str(node.left.data), style='filled', color=color)    # 左節(jié)點(diǎn)
                        label_string = '0' if label else ''    # 是否在連接線上寫上標(biāo)簽,表明為左子樹
                        self.dot.edge(node_tag, left_tag, label=label_string)   # 左節(jié)點(diǎn)與其父節(jié)點(diǎn)的連線
                        print_node(node.left, left_tag)

                    if node.right is not None:
                        right_tag = str(uuid.uuid1())
                        self.dot.node(right_tag, str(node.right.data), style='filled', color=color)
                        label_string = '1' if label else ''  # 是否在連接線上寫上標(biāo)簽,表明為右子樹
                        self.dot.edge(node_tag, right_tag, label=label_string)
                        print_node(node.right, right_tag)

                # 如果樹非空
                if self.data is not None:
                    root_tag = str(uuid.uuid1())                # 根節(jié)點(diǎn)標(biāo)簽
                    self.dot.node(root_tag, str(self.data), style='filled', color=sample(colors,1)[0])     # 創(chuàng)建根節(jié)點(diǎn)
                    print_node(self, root_tag)

                self.dot.render(save_path)      # 保存文件為指定文件

        Huffman編碼示例:

        from binary_tree import BTree
        from collections import Counter

        # string字符串至少含有兩個(gè)不同的字符
        string = 'ABCACCDAEAE'
        counter_dict = Counter(string)
        print('各字符出現(xiàn)次數(shù):', counter_dict)


        # 返回一個(gè)數(shù)組的最小值,及其對(duì)應(yīng)的下標(biāo)
        def min_and_index(array):
            f_minimum = float('inf')
            flag = None
            for i in range(len(array)):
                if array[i] < f_minimum:
                    f_minimum = array[i]
                    flag = i

            return f_minimum, flag


        # create Huffman Tree
        tree_list = [BTree(i) for i in counter_dict.values()]

        while len(tree_list) != 1:
            # 每個(gè)節(jié)點(diǎn)的數(shù)值
            values = [tree.data for tree in tree_list]

            # 從Node_list取兩個(gè)節(jié)點(diǎn)數(shù)值最小的節(jié)點(diǎn),并刪除這兩個(gè)節(jié)點(diǎn)
            _, index = min_and_index(values)
            min_value_node = tree_list.pop(index)
            values.pop(index)
            _, index = min_and_index(values)
            sec_min_value_node = tree_list.pop(index)
            values.pop(index)

            # 創(chuàng)建二叉樹
            root_value = min_value_node.data + sec_min_value_node.data
            root = BTree(root_value)
            root.left = min_value_node
            root.right = sec_min_value_node

            # 在原來的Node_list中添加新的root節(jié)點(diǎn)
            tree_list.append(root)
            values.append(root)

        root.print_tree(label=True)

        # print(root.leaves)

        # Huffman Coding
        queue = [root]

        # node_dict: 節(jié)點(diǎn)及其對(duì)應(yīng)的Huffman編碼組成的字典
        node_dict = {root: ''}
        while queue:
            node = queue.pop(0)
            if node.left is not None:
                queue.append(node.left)
                node_dict[node.left] = node_dict[node] + '0'
            if node.right is not None:
                queue.append(node.right)
                node_dict[node.right] = node_dict[node] + '1'

        # 葉子節(jié)點(diǎn)及其對(duì)應(yīng)的Huffman編碼
        for node in root.leaves:
            print(node.data, '--->', node_dict[node])

        # code_dict: 單個(gè)字符及其對(duì)應(yīng)的Huffman編碼
        code_dict = {}
        for char in counter_dict.keys():
            for node in root.leaves:
                if counter_dict[char] == node.data and node_dict[node] not in code_dict.values():
                    code_dict[char] = node_dict[node]
                    break

        print(code_dict)

        輸出的結(jié)果為:

        各字符出現(xiàn)次數(shù): Counter({'A'4'C'3'E'2'B'1'D'1})
        2 ---> 00
        1 ---> 010
        1 ---> 011
        3 ---> 10
        4 ---> 11
        {'A''11''B''010''C''10''D''011''E''00'}

        構(gòu)建的Huffman樹如下圖:

        構(gòu)建的Huffman樹

        由上可知,對(duì)于各個(gè)字符的編碼方式為:A=11, B=010, C=10, D=011, E=00.

        觀察上述的編碼方式可知,這種編碼方式是前綴編碼(每個(gè)字母都是葉子結(jié)點(diǎn),沒有一個(gè)葉子結(jié)點(diǎn)在另一個(gè)葉子結(jié)點(diǎn)的路徑上),因此可以用來做字符串的編解碼。而且在滿足前綴編碼的前提下,Huffman編碼的編碼效率是最高的,即所用的0-1字符數(shù)最少,這就是Huffman編碼的魅力。

        這里,讓我們停下腳步來想一想字符串編解碼的問題。一種常見的編解碼方式為等長編碼,比如對(duì)于字符串ABCACCDAEAE,共5個(gè)字母,因此如果使用0-1編碼的話,每個(gè)字符需使用3位編碼,此時(shí)整個(gè)字符串需用11 * 3 = 33位編碼,這種編碼方式效率并不是最高的。

        我們來看看Huffman編碼的結(jié)果:

        from math import floor, log2

        # 對(duì)原字符串進(jìn)行Huffman編碼
        coded_string = ''
        for char in string:
            coded_string += code_dict[char]

        print('壓縮后的字符串為%s.' % coded_string)

        # 一般方法的編碼位數(shù)
        if bin(len(counter_dict)).count('1') == 1 or len(counter_dict) == 1:
            bites = floor(log2(len(counter_dict)))
        else:
            bites = floor(log2(len(counter_dict))) + 1

        # print(bites)
        # 一般方法的編碼的總位數(shù)
        original_code_bites = len(string) * bites
        # 計(jì)算壓縮比
        ratio = original_code_bites/len(coded_string)

        print('一般方法編碼后的字符串長度為%s.' % original_code_bites)
        print('Huffman編碼后的字符串長度為%s.' % len(coded_string))
        print('壓縮比為%s.' % round(ratio, 3))

        輸出結(jié)果為:

        {'A''11''B''010''C''10''D''011''E''00'}
        壓縮后的字符串為110101011101001111001100.
        一般方法編碼后的字符串長度為33.
        Huffman編碼后的字符串長度為24.
        壓縮比為1.375.

        而對(duì)于字符串'BCDEFGhHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'+'A'*10000000,其壓縮比達(dá)到驚人的6.0。

        Huffman decoding

        有了Huffman編碼,自然也需要對(duì)編碼后的字符串進(jìn)行解碼。Huffman解碼的思想是十分簡單的,因?yàn)槭乔熬Y編碼,對(duì)于編碼后的字符串后,從頭開始遍歷,直到碰到葉子結(jié)點(diǎn)暫停,這樣解碼出一個(gè)字符,如此循環(huán),直到編碼后字符串結(jié)尾。

        我們寫一個(gè)示例的Huffman解碼的Python代碼:

        code_dict = {'A''11''B''010''C''10''D''011''E''00'}
        secret_string = "110101011101001111001100"

        # Huffman Decoding
        decoded_string = ''
        while secret_string:
            for char, code in code_dict.items():
                if secret_string.startswith(code):
                    decoded_string += char
                    secret_string = secret_string[len(code):]
                    break

        print(decoded_string)

        輸出結(jié)果為ABCACCDAEAE

        注意觀察,這樣的解碼代碼效率是比較低的,使用了雙重循環(huán),對(duì)于長字符串的解碼,效率是很低的。我們?cè)诒疚牡?code style="font-size: inherit;line-height: inherit;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(248, 35, 117);background: rgb(248, 248, 248);">txt文件壓縮實(shí)戰(zhàn)部分來演示如何使用bitarray模塊來提升Huffman解碼效率,從而提升文件的編/解碼效率。

        其它用途

        Huffman樹和Huffman編碼還有其他用途,其中之一便是剛才講述過的文件編解碼。其它用途列舉如下:

        • 判別條件

        對(duì)于如下的分類判別樹(if-else條件):

        如果學(xué)生的總成績數(shù)據(jù)有10000條,則5%的數(shù)據(jù)需 1 次比較,15%的數(shù)據(jù)需 2 次比較,40%的數(shù)據(jù)需 3 次比較,40%的數(shù)據(jù)需 4 次比較,因此 10000 個(gè)數(shù)據(jù)比較的次數(shù)為:

        10000 (5%+2×15%+3×40%+4×40%)=31500次

        而如果使用Huffman數(shù),則可將判別次數(shù)減少至22000次,這樣可以提升判別樹的效率。

        • word2vec

        在NLP中的word2vec算法(一種用于獲取詞向量的算法)中,對(duì)從隱藏層到輸出的softmax層這里的計(jì)算量進(jìn)行了改進(jìn)。為了避免要計(jì)算所有詞的softmax概率,word2vec采樣了霍夫曼樹來代替從隱藏層到輸出softmax層的映射。

        這是Huffman樹在NLP領(lǐng)域中的一個(gè)精彩應(yīng)用!

        txt文件壓縮實(shí)戰(zhàn)

        ok,講了這么多,我們來看看Huffman編碼在文件壓縮領(lǐng)域中的作用(也是它的主戰(zhàn)場)了。

        在這里,我們使用Python中的bitarray模塊來提升Huffman編/解碼的效率。

        import json
        from binary_tree import BTree
        from collections import Counter
        from bitarray import bitarray

        # string字符串至少含有兩個(gè)不同的字符
        with open('test2.txt''r'as f:
            string = f.read()
        print('原始文件中的前100個(gè)字符:', string[:100])
        counter_dict = Counter(string)
        print(counter_dict)


        # 返回一個(gè)數(shù)組的最小值,及其對(duì)應(yīng)的下標(biāo)
        def min_and_index(array):
            f_minimum = float('inf')
            flag = None
            for i in range(len(array)):
                if array[i] < f_minimum:
                    f_minimum = array[i]
                    flag = i

            return f_minimum, flag


        # create Huffman Tree
        tree_list = [BTree(i) for i in counter_dict.values()]

        while len(tree_list) != 1:
            # 每個(gè)節(jié)點(diǎn)的數(shù)值
            values = [tree.data for tree in tree_list]

            # 從Node_list取兩個(gè)節(jié)點(diǎn)數(shù)值最小的節(jié)點(diǎn),并刪除這兩個(gè)節(jié)點(diǎn)
            _, index = min_and_index(values)
            min_value_node = tree_list.pop(index)
            values.pop(index)
            _, index = min_and_index(values)
            sec_min_value_node = tree_list.pop(index)
            values.pop(index)

            # 創(chuàng)建二叉樹
            root_value = min_value_node.data + sec_min_value_node.data
            root = BTree(root_value)
            root.left = min_value_node
            root.right = sec_min_value_node

            # 在原來的Node_list中添加新的root節(jié)點(diǎn)
            tree_list.append(root)
            values.append(root)

        # Huffman Coding
        queue = [root]

        # node_dict: 節(jié)點(diǎn)及其對(duì)應(yīng)的Huffman編碼組成的字典
        node_dict = {root: ''}
        while queue:
            node = queue.pop(0)
            if node.left is not None:
                queue.append(node.left)
                node_dict[node.left] = node_dict[node] + '0'
            if node.right is not None:
                queue.append(node.right)
                node_dict[node.right] = node_dict[node] + '1'

        # code_dict: 單個(gè)字符及其對(duì)應(yīng)的Huffman編碼
        code_dict = {}
        for char in counter_dict.keys():
            for node in root.leaves:
                if counter_dict[char] == node.data and node_dict[node] not in code_dict.values():
                    code_dict[char] = node_dict[node]
                    break

        # 對(duì)原字符串進(jìn)行Huffman編碼
        coded_string = ''
        for char in string:
            coded_string += code_dict[char]

        # write the result to a file
        with open('test2.bin''wb'as f:
            bitarray(coded_string).tofile(f)

        # write the code_dict to a json file
        with open('code_dict.json''w'as f:
            json.dump(code_dict, f)

        輸出結(jié)果為(由于篇幅原因,counter_dict變量只顯示了前幾個(gè)字符):

        原始文件中的前100個(gè)字符: 凡人修仙傳 第二千三百零一章-第二千三百五十章 第兩千三百零一章 再見靈王 話音剛落,他也不等一干空魚人再說什么,就一張口,噴出那顆山海珠。 此珠方一飛出,立刻迎風(fēng)滴溜溜的轉(zhuǎn)動(dòng)而起,一抹艷麗霞光從上面
        Counter({',': 8278, '一': 5472, '的': 5226, '。': 4718, ' ': 3260, '了': 2659, '不': 1929, '道': 1545, '是': 1543, '在': 1362, '這': 1287, '人': 1284, '”': 1283, '大': 1270})

        原始文件test2.txt大小為453K,而編碼后的test2.bin文件大小為166K,壓縮效果較好。如果我們使用Linux中的tar命令: tar -zcvf test2.tar.gz test2.txt,壓縮后的test2.tar.gz文件大小為183K。

        接下來,我們使用code_dict.json對(duì)test2.bin進(jìn)行Huffman解碼,示例代碼如下:

        import json
        from bitarray import bitarray

        # read json file
        with open('code_dict.json''r'as f:
            code_dict = json.loads(f.read())

        string = bitarray()
        with open('test2.bin''rb'as fh:
            string.fromfile(fh)

        secret_string = string[:len(string) - 8] + bitarray(string[len(string) - 8:].to01().strip('0'))
        new_code_dict = {key: bitarray(value) for key, value in code_dict.items()}

        # Huffman decoding effectively
        dec = secret_string.decode(new_code_dict)
        print('編碼文件解碼后的前100個(gè)字符:'''.join(dec)[:100])

        輸出結(jié)果為:

        編碼文件解碼后的前100個(gè)字符: 凡人修仙傳 第二千三百零一章-第二千三百五十章 第兩千三百零一章 再見靈王 話音剛落,他也不等一干空魚人再說什么,就一張口,噴出那顆山海珠。 此珠方一飛出,立刻迎風(fēng)滴溜溜的轉(zhuǎn)動(dòng)而起,一抹艷麗霞光從上面

        與原始文件一致。

        總結(jié)

        總結(jié)下,本文主要介紹了Huffman樹和Huffman編碼的原理和Python代碼,并介紹了他們的用途,最后再介紹了txt文件壓縮實(shí)戰(zhàn)。

        參考文獻(xiàn)

        1. 霍夫曼編碼: https://zh.wikipedia.org/wiki/%E9%9C%8D%E5%A4%AB%E6%9B%BC%E7%BC%96%E7%A0%81

        2. 哈夫曼(huffman)樹和哈夫曼編碼: https://www.cnblogs.com/kubixuesheng/p/4397798.html

        3. word2vec原理(二) 基于Hierarchical Softmax的模型: https://www.cnblogs.com/pinard/p/7243513.html

        4. Decoding a Huffman code with a dictionary: https://stackoverflow.com/questions/33089660/decoding-a-huffman-code-with-a-dictionary


        瀏覽 46
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            renrencao | 三上悠亚在线观看一区二区三区 | 青青操成人娱乐网站 | 99精品这里只有精品 | 日日射影院 | 三上悠亚ssni | 91啦丨porny丨国产 | 大鸡巴视频在线播放 | A片黄色 成人午夜视频二区三区 | 日韩 中文字幕 |