數(shù)據(jù)結(jié)構(gòu)(五):哈夫曼樹(Huffman Tree)

哈夫曼樹

哈夫曼樹(或者赫夫曼樹、霍夫曼樹),指的是一種滿二叉樹,該類型二叉樹具有一項(xiàng)特性,即樹的帶權(quán)路徑長(zhǎng)最小,所以也稱之為最優(yōu)二叉樹。

節(jié)點(diǎn)的帶權(quán)路徑長(zhǎng)指的是葉子節(jié)點(diǎn)的權(quán)值與路徑長(zhǎng)的乘積,樹的帶權(quán)路徑長(zhǎng)即為樹中所有葉子節(jié)點(diǎn)的帶權(quán)路徑長(zhǎng)度之和。由此可知,若葉子節(jié)點(diǎn)的權(quán)值都是已知的,則二叉樹的構(gòu)造過(guò)程中,使得權(quán)值越大的葉子節(jié)點(diǎn)路徑越小,則整棵樹的帶權(quán)路徑長(zhǎng)最小。

編碼與解碼

數(shù)據(jù)在計(jì)算機(jī)上是以二進(jìn)制表達(dá)的,即計(jì)算機(jī)只識(shí)別二進(jìn)制序列,所以所有字符內(nèi)容都需要完成與二進(jìn)制的轉(zhuǎn)換,才能在計(jì)算機(jī)中存儲(chǔ)和呈現(xiàn)為我們看到的內(nèi)容。并且這種轉(zhuǎn)換方式必須是一致的,即字符內(nèi)容若以方式 A 轉(zhuǎn)換為二進(jìn)制序列,則二進(jìn)制序列同樣需要以方式 A 轉(zhuǎn)換為字符內(nèi)容,否則會(huì)產(chǎn)生所謂的亂碼現(xiàn)象。這種字符到二進(jìn)制的轉(zhuǎn)換即為編碼,二進(jìn)制到字符的轉(zhuǎn)換即為解碼,編碼和解碼需要使用相同的映射規(guī)則,才不會(huì)產(chǎn)生亂碼。

哈夫曼編碼

構(gòu)造哈夫曼樹的目的是為了完成哈夫曼編碼,哈夫曼編碼是一種變長(zhǎng)、極少多余編碼方案。相對(duì)于等長(zhǎng)編碼,將文件中每個(gè)字符轉(zhuǎn)換為固定個(gè)數(shù)的二進(jìn)制位,變長(zhǎng)編碼根據(jù)字符使用頻率的高低,使用了不同長(zhǎng)度的二進(jìn)制位與不同字符進(jìn)行映射,使得頻率高的字符對(duì)應(yīng)的二進(jìn)制位較短,頻率低的字符對(duì)應(yīng)的二進(jìn)制位較長(zhǎng)。使得源文件利用哈夫曼編碼后的二進(jìn)制序列大小,相對(duì)于原編碼方案能夠有較大縮小,如此即完成了文件的壓縮。

哈夫曼編碼能夠用于實(shí)現(xiàn)文件的無(wú)損壓縮,自然保證了文件解壓縮過(guò)程的正確性,即二進(jìn)制序列向字符的映射過(guò)程不會(huì)發(fā)生錯(cuò)亂。解碼過(guò)程的正確性通過(guò)哈夫曼樹的結(jié)構(gòu)可以得到證明,以哈夫曼樹中的每個(gè)葉子節(jié)點(diǎn)作為一個(gè)字符,則從根節(jié)點(diǎn)到每個(gè)葉子的路徑都是唯一的,即不存在一個(gè)葉子節(jié)點(diǎn)的路徑是另一個(gè)葉子節(jié)點(diǎn)的路徑前綴。滿足該特性的編碼稱之為前綴編碼,所以哈夫曼編碼中能夠?qū)崿F(xiàn)二進(jìn)制到字符的正確映射。

哈夫曼樹的構(gòu)造

哈夫曼樹是一棵滿二叉樹,樹中只有兩種類型的節(jié)點(diǎn),即葉子節(jié)點(diǎn)和度為 2 的節(jié)點(diǎn),所以樹中任意節(jié)點(diǎn)的左子樹和右子樹同時(shí)存在。構(gòu)建步驟如下:

  1. 對(duì)字符集合按照字符頻率進(jìn)行升序排序,并構(gòu)建一顆空樹;
  2. 遍歷字符集合,將每一個(gè)字符添加到樹中,添加規(guī)則為:
    【1】若樹為空,則作為根節(jié)點(diǎn);
    【2】若字符頻率不大于根節(jié)點(diǎn)頻率,則字符作為根節(jié)點(diǎn)的左兄弟,形成一個(gè)新的根節(jié)點(diǎn),頻率值為左、右子節(jié)點(diǎn)之和;
    【3】若字符頻率大于根節(jié)點(diǎn)頻率,則字符作為根節(jié)點(diǎn)的右兄弟,形成一個(gè)新的根節(jié)點(diǎn),頻率值為左右子節(jié)點(diǎn)之和。
構(gòu)造示例

這里自然不可能以所有字符集作示例,假設(shè)字符集范圍為 A~J
字符集合為:contentArr = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']
對(duì)應(yīng)的頻率為:valueArr = [5, 3, 4, 0, 2, 1, 8, 6, 9, 7]

step 1:

對(duì)字符集合按照頻率進(jìn)行排序,這里使用插入排序算法進(jìn)行排序。

算法示例:

# synchronise sort the valueArr and contentArr
def insertionSort(valueArr, contentArr):
    for i in range(1, len(valueArr)):  # iteration times
        tmpValue = valueArr[i]
        tmpContent = contentArr[i]
        while i > 0 and tmpValue < valueArr[i - 1]:
            valueArr[i] = valueArr[i - 1]
            contentArr[i] = contentArr[i - 1]
            i = i - 1
        valueArr[i] = tmpValue
        contentArr[i] = tmpContent

排序后字符集合和對(duì)應(yīng)的頻率為:

contentArr = ['D', 'F', 'E', 'B', 'C', 'A', 'H', 'J', 'G', 'I']
valueArr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

step 2:

遍歷將集合中元素添加到樹中,其中定義如下:

樹節(jié)點(diǎn)定義為:

# tree node definition
class Node(object):
    def __init__(self, value, content=None, lchild=None, rchild=None):
        self.lchild = lchild
        self.rchild = rchild
        self.value = value
        self.content = content

樹定義為:

# tree definition
class Tree(object):
    def __init__(self, root=None):
        self.root = root
        self.codeMap = {}

    # merge two nodes and return one root node
    def acceptNewNode(self, value, content):
        if not self.root:
            self.root = Node(value, content)
        else:
            newNode = Node(value, content)
            newRoot = Node(self.root.value + value)
            lchild, rchild = (self.root, newNode) if self.root.value < value else (newNode, self.root)
            newRoot.lchild, newRoot.rchild = lchild, rchild
            self.root = newRoot

樹結(jié)構(gòu)中定義的 acceptNewNode 方法,用于向樹中添加新字符,其中 value 表示新字符的頻率,content 表示字符體。

第一個(gè)元素 D,頻率為 0

第二個(gè)元素 F,頻率為 1

第三個(gè)元素 E,頻率為 2

...
...
...

第十個(gè)元素 I,頻率為 9

哈夫曼樹編解碼

哈夫曼樹構(gòu)造完成之后,以 0 表示左分支,1 表示右分支,則樹中每個(gè)字符都有唯一的二進(jìn)制映射。這里借用哈希表結(jié)構(gòu),將字符與對(duì)應(yīng)的二進(jìn)制序列存儲(chǔ)為鍵值對(duì),來(lái)演示編碼過(guò)程;利用二進(jìn)制序列在二叉樹中查找具體的字符,來(lái)演示解碼過(guò)程。

構(gòu)造哈希表

首先根據(jù)哈夫曼樹,生成哈希表,有點(diǎn)類似于前序遍歷:

# initialize the huffman tree code map
def initializeCodeMap(node, byteArr, codeMap):
    if node.lchild:
        byteArr.append('0')
        initializeCodeMap(node.lchild, byteArr, codeMap)
        byteArr.append('1')
        initializeCodeMap(node.rchild, byteArr, codeMap)
        byteArr.pop() if len(byteArr) > 0 else None  # in case only the root node left
    else:
        codeMap[node.content] = ''.join(byteArr)
        byteArr.pop()

代碼中以 codeMap 作為存儲(chǔ)鍵值對(duì)的哈希表, 以 byteArr 存儲(chǔ)二進(jìn)制路徑信息。因?yàn)楣蚵鼧涫菨M二叉樹,節(jié)點(diǎn)的左子樹存在則右子樹同時(shí)存在,所以判斷左子樹是否存在即可判斷是否為葉子節(jié)點(diǎn)。每個(gè)左葉子節(jié)點(diǎn)訪問(wèn)結(jié)束則記錄鍵值對(duì)到 codeMap 中,并將路徑 byteArr 回退到父節(jié)點(diǎn),開(kāi)始訪問(wèn)右子樹;每個(gè)右葉子節(jié)點(diǎn)訪問(wèn)結(jié)束則記錄鍵值對(duì)到 codeMap 中,并將路徑 byteArr 回退到父節(jié)點(diǎn)的父節(jié)點(diǎn),訪問(wèn)其右子樹。

編碼與解碼

構(gòu)造完成哈希表后,編碼 encode 過(guò)程只需要根據(jù)字符取二進(jìn)制序列即可。解碼 decode 過(guò)程就是根據(jù)二進(jìn)制序列,不斷在二叉樹中查找字符而已,找到字符后則從根節(jié)點(diǎn)繼續(xù)查找下一個(gè)字符。

編碼與解碼函數(shù)體實(shí)現(xiàn)如下:

# tree definition
class Tree(object):

    # encode
    def encode(self, chars):
        bytes = ''
        for i in chars:  # get the mapped bytes
            bytes += self.codeMap.get(i.upper(), '###')
        return bytes

    # decode
    def decode(self, bytes):
        chars = ''
        tmpNode = self.root
        for i in bytes:
            if i == '0':
                tmpNode = tmpNode.lchild
            elif i == '1':
                tmpNode = tmpNode.rchild
            if not tmpNode.lchild:
                chars += tmpNode.content
                tmpNode = self.root
        return chars

代碼附錄

完整代碼如下:

# tree node definition
class Node(object):
    def __init__(self, value, content=None, lchild=None, rchild=None):
        self.lchild = lchild
        self.rchild = rchild
        self.value = value
        self.content = content

# tree definition
class Tree(object):
    def __init__(self, root=None):
        self.root = root
        self.codeMap = {}

    # initialize the huffman tree code map
    def initializeCodeMap(self):
        initializeCodeMap(self.root, [], self.codeMap)

    # encode
    def encode(self, chars):
        bytes = ''
        for i in chars:  # get the mapped bytes
            bytes += self.codeMap.get(i.upper(), '###')
        return bytes

    # decode
    def decode(self, bytes):
        chars = ''
        tmpNode = self.root
        for i in bytes:
            if i == '0':
                tmpNode = tmpNode.lchild
            elif i == '1':
                tmpNode = tmpNode.rchild
            if not tmpNode.lchild:
                chars += tmpNode.content
                tmpNode = self.root
        return chars

    # merge two nodes and return one root node
    def acceptNewNode(self, value, content):
        if not self.root:
            self.root = Node(value, content)
        else:
            newNode = Node(value, content)
            newRoot = Node(self.root.value + value)
            lchild, rchild = (self.root, newNode) if self.root.value < value else (newNode, self.root)
            newRoot.lchild, newRoot.rchild = lchild, rchild
            self.root = newRoot

# initialize the huffman tree code map
def initializeCodeMap(node, byteArr, codeMap):
    if node.lchild:
        byteArr.append('0')
        initializeCodeMap(node.lchild, byteArr, codeMap)
        byteArr.append('1')
        initializeCodeMap(node.rchild, byteArr, codeMap)
        byteArr.pop() if len(byteArr) > 0 else None  # in case only the root node left
    else:
        codeMap[node.content] = ''.join(byteArr)
        byteArr.pop()

# construct the huffman tree
def createHuffmanTree(valueArr, contentArr):
    insertionSort(valueArr, contentArr)  # synchronise sort the valueArr and contentArr
    hfTree = Tree()
    for i in range(len(valueArr)):  # construct the huffman tree
        hfTree.acceptNewNode(valueArr[i], contentArr[i])
    hfTree.initializeCodeMap() # initialize the huffman tree code map
    return hfTree

# synchronise sort the valueArr and contentArr
def insertionSort(valueArr, contentArr):
    for i in range(1, len(valueArr)):  # iteration times
        tmpValue = valueArr[i]
        tmpContent = contentArr[i]
        while i > 0 and tmpValue < valueArr[i - 1]:
            valueArr[i] = valueArr[i - 1]
            contentArr[i] = contentArr[i - 1]
            i = i - 1
        valueArr[i] = tmpValue
        contentArr[i] = tmpContent

演示示例如下:

if __name__ == '__main__':
    valueArr = [5, 3, 4, 0, 2, 1, 8, 6, 9, 7]
    contentArr = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']
    huTree = createHuffmanTree(valueArr, contentArr)
    chars = 'hIebAhcHdfGc'
    bytes = huTree.encode(chars)
    print(chars.lower(),'encode =',bytes)
    
    chars = huTree.decode(bytes)
    print(bytes,'decode =',chars.lower())

示例輸出為:

hiebahchdfgc encode = 11100111111111111110111101110111110111011111110011111110110111110
11100111111111111110111101110111110111011111110011111110110111110 decode = hiebahchdfgc

github 鏈接:哈夫曼樹

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。