本文主要介紹Huffman編碼、Huffman樹、和如何借助Python實現Huffman編碼樹對文件進行壓縮和解壓縮。下文目錄:
- 什么是Huffman編碼;
- 如何通過Huffman樹創建Huffman編碼;
- Python實現Huffman編碼對文件進行壓縮和解壓縮
一、什么是Huffman編碼
百科給的定義如下:
哈夫曼編碼(Huffman Coding),又稱霍夫曼編碼,是一種編碼方式,哈夫曼編碼是可變字長編碼(VLC)的一種。Huffman于1952年提出一種編碼方法,該方法完全依據字符出現概率來構造異字頭的平均長度最短的碼字,有時稱之為最佳編碼,一般就叫做Huffman編碼(有時也稱為霍夫曼編碼)
如上,Huffman編碼就是一種效率很高的編碼方式,在理解Huffman編碼之前,我們先來了解一下下面兩種編碼方式:
1.定長編碼方式
例如ASCII碼,8-bit定長編碼,使用8位(一個字節)代表一個字符,比如tea就一定得需要 3x8 = 24位去表示該自字符串,一個含有n個字符的字符串就得需要 nx8 位去表示該字符串;
這樣的編碼沒有考慮到一些字符出現的頻率會高于一些其它的字符,比如在英文26個字母e的出現頻率最高,而z出現的頻率最低,此時我們使用較短位數的編碼來表示e,代價是表示z字符可能需要稍長的編碼,但是這并不妨礙我們達到壓縮的效果:
例如: 使用7位的編碼表示e,9位的編碼表示z,其它字符的編碼不變,由于e出現的頻率比z的高,假設一篇文章當中e出現了10000次,而z出現的次數是10次,那么相比于定長編碼,就節省了 10000 - 10 = 9990位的空間。(這個例子主要展示定長碼的缺點,以及變長碼可以給我們帶來的好處)
2.變長編碼方式
上面提到了,變長編碼方式可以給我帶來節省空間的好處,但是對于使用變長碼編碼的文件,如何去解析該文件得到原文件內容呢,又可能會出現什么問題呢?
舉個例子:
使用 0 表示 e,1 表示 a,01表示t,那么 tea就被編碼成0101,但是我們解析的時候,0101就可以解析成為 tt,eat,eaea,為了解決這個問題,科學家們又引入一個新的概念,那就是前綴碼(Prefix codes 是屬于變長編碼范圍的)
前綴碼定義:任何一個字符的編碼都不能是其它任何字符的編碼的前綴
對于上面的例子,使用前綴碼的意思就是 e 不能用0去表示,應為 0是 t對應的編碼 01 的前綴,如果我們使用 001 去表示e,1 表示 a,01表示t,那么 tea就被編碼成為 010011,并且在解析的時候,我們就只能解析成為 tea。
Huffman編碼就是一種能夠使用最短的位數來編碼被編碼文件的前綴碼
如何構造這樣的前綴碼?
二、借助Huffman樹創建Huffman編碼
Huffman編碼將給字母分配編碼。每個字母的編碼的長度取決于在被壓縮文件中對應字母的出現頻率,我們稱之為權重(weight)。每個字母的Huffman編碼是從稱為Huffman編碼樹的滿二叉樹(所有節點要么有左右兩個子孩子,要么就沒有子孩子)中得到的。Huffman編碼樹的每一個葉節點對應于一個字母,葉節點的權重 (weight)就是它對應的字母出現的頻率。使用權重的目的是建立的Huffman編碼樹有最小外部路徑權重。
下圖1將給大家解釋一下什么是最小外部路徑權重,并且Huffman編碼的過程:
那么接下來,我們將解釋圖1 中的第一步構建Huffman編碼樹的過程:
- 創建n個初始化的Huffman樹,每個樹只包含單一的葉節點,葉節點紀錄對應的字母和該字母出現的頻率(weight);
- 按照weight從小到大對其進行所有的Huffman樹進行排序,取出其中weight最小的兩棵樹,構造一個新的Huffman樹,新的Huffman樹的weight等于兩棵子樹的weight之和,然后再加入到原來的Huffman樹數組當中;
- 反復上面的2中的操作,直到該數組當中只剩下一棵Huffman樹,那么最后剩下來的那棵Huffman樹就是我們構造好的Huffman編碼樹;
下面幾個圖將展示對應上圖例子的一個構造Huffman編碼樹的過程,如圖 2和圖 3所示:
得到上面的Huffman編碼樹之后,就可以得到每個字符對應的編碼了,方法就是:從根節點找到該葉節點,如果向左子樹前進一步,那么code + = '0',如果向右子樹前進了一步,那么code+= '1',等到達該葉節點,code對應的內容,就是該葉節點對應字符的編碼
自此,你已經知道了如何使用Huffman編碼樹如何給字符分配編碼,并且也知道了如何去構造這樣的Huffman編碼樹,那么接下來就借助Python來實現它吧!
三、Python實現Huffman編碼對文件進行壓縮和解壓縮
文件壓縮的思路如圖 4所示:
文件解壓縮的思路如圖 5所示:
Python代碼實現:
step1:實現Huffman編碼樹及其構造方法,代碼如下:
#-*- coding:utf-8 -*-
#copyright@zhanggugu
import six
import sys
class HuffNode(object):
"""
定義一個HuffNode虛類,里面包含兩個虛方法:
1. 獲取節點的權重函數
2. 獲取此節點是否是葉節點的函數
"""
def get_wieght(self):
raise NotImplementedError(
"The Abstract Node Class doesn't define 'get_wieght'")
def isleaf(self):
raise NotImplementedError(
"The Abstract Node Class doesn't define 'isleaf'")
class LeafNode(HuffNode):
"""
樹葉節點類
"""
def __init__(self, value=0, freq=0,):
"""
初始化 樹節點 需要初始化的對象參數有 :value及其出現的頻率freq
"""
super(LeafNode, self).__init__()
# 節點的值
self.value = value
self.wieght = freq
def isleaf(self):
"""
基類的方法,返回True,代表是葉節點
"""
return True
def get_wieght(self):
"""
基類的方法,返回對象屬性 weight,表示對象的權重
"""
return self.wieght
def get_value(self):
"""
獲取葉子節點的 字符 的值
"""
return self.value
class IntlNode(HuffNode):
"""
中間節點類
"""
def __init__(self, left_child=None, right_child=None):
"""
初始化 中間節點 需要初始化的對象參數有 :left_child, right_chiled, weight
"""
super(IntlNode, self).__init__()
# 節點的值
self.wieght = left_child.get_wieght() + right_child.get_wieght()
# 節點的左右子節點
self.left_child = left_child
self.right_child = right_child
def isleaf(self):
"""
基類的方法,返回False,代表是中間節點
"""
return False
def get_wieght(self):
"""
基類的方法,返回對象屬性 weight,表示對象的權重
"""
return self.wieght
def get_left(self):
"""
獲取左孩子
"""
return self.left_child
def get_right(self):
"""
獲取右孩子
"""
return self.right_child
class HuffTree(object):
"""
huffTree
"""
def __init__(self, flag, value =0, freq=0, left_tree=None, right_tree=None):
super(HuffTree, self).__init__()
if flag == 0:
self.root = LeafNode(value, freq)
else:
self.root = IntlNode(left_tree.get_root(), right_tree.get_root())
def get_root(self):
"""
獲取huffman tree 的根節點
"""
return self.root
def get_wieght(self):
"""
獲取這個huffman樹的根節點的權重
"""
return self.root.get_wieght()
def traverse_huffman_tree(self, root, code, char_freq):
"""
利用遞歸的方法遍歷huffman_tree,并且以此方式得到每個 字符 對應的huffman編碼
保存在字典 char_freq中
"""
if root.isleaf():
char_freq[root.get_value()] = code
print ("it = %c and freq = %d code = %s")%(chr(root.get_value()),root.get_wieght(), code)
return None
else:
self.traverse_huffman_tree(root.get_left(), code+'0', char_freq)
self.traverse_huffman_tree(root.get_right(), code+'1', char_freq)
def buildHuffmanTree(list_hufftrees):
"""
構造huffman樹
"""
while len(list_hufftrees) >1 :
# 1. 按照weight 對huffman樹進行從小到大的排序
list_hufftrees.sort(key=lambda x: x.get_wieght())
# 2. 跳出weight 最小的兩個huffman編碼樹
temp1 = list_hufftrees[0]
temp2 = list_hufftrees[1]
list_hufftrees = list_hufftrees[2:]
# 3. 構造一個新的huffman樹
newed_hufftree = HuffTree(1, 0, 0, temp1, temp2)
# 4. 放入到數組當中
list_hufftrees.append(newed_hufftree)
# last. 數組中最后剩下來的那棵樹,就是構造的Huffman編碼樹
return list_hufftrees[0]
上面代碼詳細展示了如何構造Huffman編碼樹,代碼當中的注釋已經足夠詳細,并且算法在第二部分已經進行了詳細的講解,在此就不贅述!
step2:壓縮文件函數compress() 的實現,其主要思路就如圖 4所示,其代碼參考github 工程:
step3:解壓縮文件函數decompress() 的實現,其主要思路就如圖 5所示,其代碼參考github 工程:
對代碼中解壓縮和壓縮對齊處理方法說明:
測試:
原則上,該程序可以對任意格式的文件進行壓縮,我自己也試過將xsl,word的文件進行壓縮,大概可以得到30%左右的空間節省
四、總結
看完本文,希望你對Huffman編碼方法能夠有一個清晰的了解,并且知道如何使用Python實現Huffman編碼樹并且對文件進行解壓縮。
完整源碼請參考github工程
參考資料:
[1] - Data Structures and Algorithm Analysis in C++ Third Edition by Clifford A. Shaffer (<<數據結構與算法分析 C++版第三版>> 作者Clifford A. Shaffer , 電子工業出版社 )
聲明:
- 聯系作者,新浪微博私信 @谷谷_z
- 如果在文章當中發現有描述錯誤的地方,還請您不吝指出,萬分感謝!
- 此文章系本人原創作品,轉發請注明出處!