<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 20930字,需瀏覽 42分鐘

           ·

          2024-05-07 22:32

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

          Huffman樹

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

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

          其中涉及到的概念如下:

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

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

          我們來看個例子:

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

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

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

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

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

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

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

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

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

          from binary_tree import BTree

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

          while len(tree_list) != 1:
              # 每個節(jié)點的數(shù)值
              values = [tree.data for tree in tree_list]
              # 從Node_list取兩個節(jié)點數(shù)值最小的節(jié)點,并刪除這兩個節(jié)點
              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é)點
              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樹的一個重要應用是Huffman編碼,而Huffman編碼常用于數(shù)據(jù)解壓縮。

          Huffman coding

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

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

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

          首先我們對二叉搜索樹(BST)的Python實現(xiàn)中的二叉樹進行改成,新增獲取葉子結(jié)點方法以及二叉樹可視化方法修改(將標簽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é)點
              @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進行二叉樹的可視化
              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']

                  # 繪制以某個節(jié)點為根節(jié)點的二叉樹
                  def print_node(node, node_tag):
                      # 節(jié)點顏色
                      color = sample(colors,1)[0]
                      if node.left is not None:
                          left_tag = str(uuid.uuid1())            # 左節(jié)點的數(shù)據(jù)
                          self.dot.node(left_tag, str(node.left.data), style='filled', color=color)    # 左節(jié)點
                          label_string = '0' if label else ''    # 是否在連接線上寫上標簽,表明為左子樹
                          self.dot.edge(node_tag, left_tag, label=label_string)   # 左節(jié)點與其父節(jié)點的連線
                          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 ''  # 是否在連接線上寫上標簽,表明為右子樹
                          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é)點標簽
                      self.dot.node(root_tag, str(self.data), style='filled', color=sample(colors,1)[0])     # 創(chuàng)建根節(jié)點
                      print_node(self, root_tag)

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

          Huffman編碼示例:

          from binary_tree import BTree
          from collections import Counter

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


          # 返回一個數(shù)組的最小值,及其對應的下標
          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:
              # 每個節(jié)點的數(shù)值
              values = [tree.data for tree in tree_list]

              # 從Node_list取兩個節(jié)點數(shù)值最小的節(jié)點,并刪除這兩個節(jié)點
              _, 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é)點
              tree_list.append(root)
              values.append(root)

          root.print_tree(label=True)

          # print(root.leaves)

          # Huffman Coding
          queue = [root]

          # node_dict: 節(jié)點及其對應的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é)點及其對應的Huffman編碼
          for node in root.leaves:
              print(node.data, '--->', node_dict[node])

          # code_dict: 單個字符及其對應的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樹

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

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

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

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

          from math import floor, log2

          # 對原字符串進行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
          # 計算壓縮比
          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.

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

          Huffman decoding

          有了Huffman編碼,自然也需要對編碼后的字符串進行解碼。Huffman解碼的思想是十分簡單的,因為是前綴編碼,對于編碼后的字符串后,從頭開始遍歷,直到碰到葉子結(jié)點暫停,這樣解碼出一個字符,如此循環(huán),直到編碼后字符串結(jié)尾。

          我們寫一個示例的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),對于長字符串的解碼,效率是很低的。我們在本文的txt文件壓縮實戰(zhàn)部分來演示如何使用bitarray模塊來提升Huffman解碼效率,從而提升文件的編/解碼效率。

          其它用途

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

          • 判別條件

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

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

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

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

          • word2vec

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

          這是Huffman樹在NLP領域中的一個精彩應用!

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

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

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

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

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


          # 返回一個數(shù)組的最小值,及其對應的下標
          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:
              # 每個節(jié)點的數(shù)值
              values = [tree.data for tree in tree_list]

              # 從Node_list取兩個節(jié)點數(shù)值最小的節(jié)點,并刪除這兩個節(jié)點
              _, 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é)點
              tree_list.append(root)
              values.append(root)

          # Huffman Coding
          queue = [root]

          # node_dict: 節(jié)點及其對應的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: 單個字符及其對應的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

          # 對原字符串進行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變量只顯示了前幾個字符):

          原始文件中的前100個字符: 凡人修仙傳 第二千三百零一章-第二千三百五十章 第兩千三百零一章 再見靈王 話音剛落,他也不等一干空魚人再說什么,就一張口,噴出那顆山海珠。 此珠方一飛出,立刻迎風滴溜溜的轉(zhuǎn)動而起,一抹艷麗霞光從上面
          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對test2.bin進行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個字符:'''.join(dec)[:100])

          輸出結(jié)果為:

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

          與原始文件一致。

          總結(jié)

          總結(jié)下,本文主要介紹了Huffman樹和Huffman編碼的原理和Python代碼,并介紹了他們的用途,最后再介紹了txt文件壓縮實戰(zhà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
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  狠狠狠狠狠狠狠狠狠狠狠狠狠狠狠狠 | 欧美淫淫| 蜜桃91精品秘 成人取精库 | 人人爱人人摸人人操 | 天天操天天曰 |