哈夫曼樹與哈夫曼編碼

哈夫曼樹與哈夫曼編碼


術語node

i)路徑和路徑長度算法

在一棵樹中,從一個結點往下能夠達到的孩子或孫子結點之間的通路,稱爲路徑。 路徑中分支的數目稱爲路徑長度。若規定根結點的層數爲1,則從根結點到第L層結點的路徑長度爲L-1。數組

ii)結點的權及帶權路徑長度app

若對樹中的每一個結點賦給一個有着某種含義的數值,則這個數值稱爲該結點的權。 結點的帶權路徑長度爲:從根結點到該結點之間的路徑長度與該結點的權的乘積。編碼

iii)樹的帶權路徑長度加密

樹的帶權路徑長度:全部葉子結點的帶權路徑長度之和,記爲WPL。spa


先了解一下哈夫曼樹,以後再構造一棵哈夫曼樹,最後分析下哈夫曼樹的原理。code

1)哈夫曼樹

哈夫曼樹是這樣定義的:給定n個帶權值的節點,做爲葉子節點,構造一顆二叉樹,使樹的帶權路徑長度達到最小,這時候的二叉樹就是哈夫曼樹,也叫最優二叉樹。blog

哈夫曼樹具備以下性質:排序

1)帶權路徑長度最短

2)權值較大的結點離根較近


2)構造哈夫曼樹

構造哈夫曼樹的步驟以下:

假設有n個權值,則構造出的哈夫曼樹有n個葉子結點。 n個權值分別設爲 w一、w二、…、wn,則哈夫曼樹的構造規則爲:

1) 將w一、w二、…,wn當作是有n 棵樹的森林(每棵樹僅有一個結點);

2) 在森林中選出兩個根結點的權值最小的樹合併,做爲一棵新樹的左、右子樹, 且新樹的根結點權值爲其左、右子樹根結點權值之和

3)從森林中刪除選取的兩棵樹,並將新樹加入森林

4)重複2)、3)步,直到森林中只剩一棵樹爲止,該樹即爲所求得的哈夫曼樹

根據如上規則,能夠循序漸進的寫出代碼,Go 語言的描述以下:

package main

import (
    "fmt"
    "errors"
    "os"
)

type BNode struct {
    key string
    value float64
    ltree, rtree *BNode
}

func getMinNodePos(treeList []*BNode) (pos int, err error) {
    if len(treeList) == 0 {
        return -1, errors.New("treeList length is 0")
    }
    pos = -1
    for i, _ := range treeList {
        if pos < 0 {
            pos = i
            continue
        }
        if treeList[pos].value > treeList[i].value {
            pos = i
        }
    }

    return pos, nil
}

func get2MinNodes(treeList []*BNode) (node1, node2 *BNode, newlist []*BNode) {
    if len(treeList) < 2 {
    }
    pos, err := getMinNodePos(treeList)
    if nil != err {
        return nil, nil, treeList
    }
    node1 = treeList[pos]
    newlist = append(treeList[:pos], treeList[pos + 1 :]...)

    pos, err = getMinNodePos(newlist)
    if nil != err {
        return nil, nil, treeList
    }
    node2 = newlist[pos]
    newlist = append(newlist[:pos], newlist[pos + 1 :]...)

    return node1, node2, newlist
}

func makeHuffmanTree(treeList []*BNode) (tree *BNode, err error) {
    if len(treeList) < 1 {
        return nil, errors.New("Error : treeList length is 0")
    }
    if len(treeList) == 1 {
        return treeList[0], nil
    }
    lnode, rnode, newlist := get2MinNodes(treeList)

    newNode := new(BNode)
    newNode.ltree = lnode
    newNode.rtree = rnode

    newNode.value = newNode.ltree.value + newNode.rtree.value
    newNode.key = newNode.ltree.key + newNode.rtree.key;

    newlist = append(newlist, newNode)

    return makeHuffmanTree(newlist)
}

func main() {
    keyList   := []byte    {'A',  'B', 'C',  'D',  'E', 'F',  'G',  'H'}
    valueList := []float64 {0.12, 0.4, 0.29, 0.90, 0.1, 1.1, 1.23, 0.01}

    treeList := []*BNode   {}
    for i, x := range keyList {
        n := BNode{key:string(x), value:valueList[i]}
        treeList = append(treeList, &n)
    }

    tree, err := makeHuffmanTree(treeList)
    if nil != err {
        fmt.Println(err.Error())
    }

    //TODO you can make it yourself
    //showTree(tree)
}

獲得的哈夫曼樹以下:

其中的橙色結點都是數據中的權值結點。

計算一下這棵樹的帶權路徑長度:

WPL=0.9x2 + 0.4x3 + 0.01x6 + 0.01x6 + 0.1x6 + 0.12x5 + 0.29x4 + 1.1x2 + 1.23x2 = 10.08

計算好了,可是這個帶權路徑是最小的嗎?下面就看一下理論依據。


3)哈夫曼樹的證實

設有t片葉子,權值分別爲W1,W2,W3,...,Wt。 定義二叉樹的權值爲W(T)=∑Wi*L(vi),其中Vi是帶權爲Wi的葉子, L(vi)是葉子Vi的路徑長度,接下來咱們就求W(T)的最小值。

1)權值最小的葉子節點距離樹根節點的距離不比其它葉子節點到樹根結點的距離近

不失通常性,咱們不妨設W1≤W2≤W3≤...≤Wt,而且W1和W2的葉子是兄弟。 先隨意給出一棵符合條件的二叉樹,再逐步把它調整到最佳。 設S是非葉子結點中路徑最長的一點,假設S的兒子不是V1和V2,而是其餘的Vx和Vy, 那麼L(Vx)≥L(V1),L(Vx)≥L(V2),L(Vy)≥L(V1), L(Vy)≥L(V1),注意到Vx,Vy≥V1,V2, 因此咱們交換Vx和V1,Vy和V2,固定其餘的量不變,則咱們獲得的二叉樹的權值差爲 [V1L(Vx)+ V2L(Vy)+ VxL(V1)+ VyL(V2)]- [V1L(V1)+ V2L(V2)+ VxL(Vx)+ VyL(Vy)]=(V1- Vx)(L(Vx)- L(V1))+(V2-Vy)(L(Vy)-L(V2))≤0,因此調整後權值減少了。 故S的兒子一定爲v1和v2。

2)哈夫曼樹是最優的

設Tx是帶權W1,W2,W3,...,Wt的二叉樹,在Tx中用一片葉子代替W1,W2這兩片樹葉和它們的雙親組成的子樹,並對它賦權值爲W1+W2,設Tx'表示帶權W1+W2,W3,W4,...,Wt的二叉樹,則顯然有W(Tx)=W(Tx')+W1+W2,因此若Tx是最優樹,則Tx'也是最優樹,因此逐步往下調整能夠把帶有t個權的最優樹簡化到t-1個,再從t-1個簡化到t-2個,...,最後降到帶有2個權的最優樹


4)哈夫曼編碼

哈夫曼編碼是可變字長編碼(VLC)的一種,Huffman於1952年提出的編碼方法, 該方法徹底依據字符出現機率來構造異字頭的平均長度最短的碼字, 有時稱之爲最佳編碼,通常就叫作Huffman編碼。

1951年,哈夫曼和他在MIT信息論的同窗須要選擇是完成學期報告仍是期末考試。 導師Robert M. Fano給他們的學期報告的題目是,尋找最有效的二進制編碼。 因爲沒法證實哪一個已有編碼是最有效的,哈夫曼放棄對已有編碼的研究, 轉向新的探索,最終發現了基於有序頻率二叉樹編碼的想法, 並很快證實了這個方法是最有效的。因爲這個算法,學生終於青出於藍, 超過了他那曾經和信息論創立者香農共同研究過相似編碼的導師。 哈夫曼使用自底向上的方法構建二叉樹, 避免了次優算法Shannon-Fano編碼的最大弊端──自頂向下構建樹。

1952年,David A. Huffman在麻省理工攻讀博士時發表了《一種構建極小多餘編碼的方法》 (A Method for the Construction of Minimum-Redundancy Codes)一文, 它通常就叫作Huffman編碼。

Huffman在1952年根據香農(Shannon)在1948年和範若(Fano) 在1949年闡述的這種編碼思想提出了一種不定長編碼的方法, 也稱霍夫曼(Huffman)編碼。霍夫曼編碼的基本方法是先對圖像數據掃描一遍, 計算出各類像素出現的機率,按機率的大小指定不一樣長度的惟一碼字, 由此獲得一張該圖像的霍夫曼碼錶。編碼後的圖像數據記錄的是每一個像素的碼字, 而碼字與實際像素值的對應關係記錄在碼錶中。

哈夫曼樹就是爲生成哈夫曼編碼而構造的。哈夫曼編碼的目的在於得到平均長度最短的碼字, 因此下面我麼以一個簡單的例子來演示一下, 經過哈夫曼編碼先後數據佔用空間對比,來講明一下哈夫曼編碼的應用。

4.1)編碼

這裏有一片英文文章《If I Were a Boy Again》,咱們首先統計其中英文字符和標點符號出現的頻率。(按照字符在字母表中的順序排序)

字符 頻數 比例
換行 36 2.236
空格 271 16.832
" 4 0.248
, 21 1.304
. 15 0.932
; 2 0.124
F 1 0.062
I 23 1.429
L 1 0.062
N 1 0.062
T 1 0.062
W 1 0.062
a 98 6.087
b 23 1.429
c 31 1.925
d 38 2.360
e 143 8.882
f 36 2.236
g 25 1.553
h 43 2.671
i 80 4.969
k 11 0.683
l 69 4.286
m 31 1.925
n 89 5.528
o 109 6.770
p 20 1.242
q 1 0.062
r 80 4.969
s 67 4.161
t 105 6.522
u 45 2.795
v 16 0.994
w 34 2.112
y 39 2.422

接下來構造一棵哈夫曼樹:

咱們依然使用本文最開始使用的代碼進行哈夫曼樹的構造。 以每一個字符爲葉子節點,字符出現的次數爲權值,構造哈夫曼樹。

構造出的哈夫曼樹圖片有點兒大,這個頁面放不下,有興趣的同窗到這裏看看。

獲取葉節點的哈夫曼編碼的Go語言代碼以下:

//葉子結點的哈夫曼編碼存儲在map m裏面
func getHuffmanCode(m map[string]string, tree *BNode){
    if nil == tree {
        return
    }

    showHuffmanCode(m, tree, "")
}

func showHuffmanCode(m map[string]string, node *BNode, e string) {
    if nil == node {
        return
    }
    //左右子結點均爲nil,則說明此結點爲葉子節點
    if nil == node.ltree && nil == node.rtree {
        m[node.key] = e
    }

    //遞歸獲取左子樹上葉子結點的哈夫曼編碼
    showHuffmanCode(m, node.ltree, e + "0")

    //遞歸獲取右子樹上葉子結點的哈夫曼編碼
    showHuffmanCode(m, node.rtree, e + "1")
}

根據哈夫曼樹得出的每一個葉子節點的哈夫曼編碼以下(按照頻數排序):

字符 頻數 哈夫曼編碼
W 1 10110011110
F 1 10110011111
L 1 1011001001
N 1 1011001000
q 1 1011001010
; 2 1011001110
" 4 101100110
k 11 1011000
. 15 1110000
v 16 1110001
p 20 010100
, 21 010101
I 23 011110
b 23 011111
g 25 101101
m 31 101111
c 31 101110
w 34 111001
換行 36 111111
f 36 111110
d 38 00100
y 39 00101
h 43 01011
u 45 01110
s 67 11101
l 69 11110
r 80 0100
i 80 0011
n 89 0110
a 98 1000
t 105 1001
o 109 1010
e 143 000
空格 271 110

這裏頻數就是權值,能夠看到,權值越小的距離根結點越遠,編碼長度也就越大。

好比W在整篇文章中只出現了一次,頻數是1,權重很小,而它的編碼是10110011110,很大吧。

編碼替換

下一步開始進行數據壓縮,就是根據上表,把文章中出現的全部字符替換成對應的哈夫曼編碼。 不是以字符串形式的"010101",而是二進制形式的"010101",就是bit位操做, 不過這裏爲了簡便,就省略了bit操做的步驟,而是以01字符串來表示二進制的01 bit流。。

進行內容替換的Go語言代碼以下:

func HuffmanCode(m map[string]string, tree *BNode, strContent string) string {
    if nil == tree{
        return ""
    }
    strEncode := ""
    for _, v := range strContent {
        strEncode += m[string(v)]
    }

    return strEncode
}

下面是一些統計數據:

原文章內容:1610字節

壓縮後長度:886字節(885.375)

壓縮率:54.99%

固然,這只是內容的數據部分,咱們還須要存儲剛剛生成的"字符-編碼"對照表, 因此綜合的壓縮率不會這麼大。當前的程序是基礎的使用哈夫曼編碼進行數據壓縮的方法, 還能夠在基礎的方法之上進行改進,壓縮率會更大。

4.2)解碼

解碼是編碼的逆過程。讀取加密的數據流,當接到一個bit的時候, 將當前的bit數組去和"字符-編碼"表中的編碼進行比較,若是匹配成功, 則將其替換成編碼對應的字符,當前bit數組清空,繼續讀取字節流並記錄。

下面是一個段解碼的代碼片斷:

func HuffmanDecode(mapTable map[string]string, str string) {
    //把"字符-編碼"的map反轉一下,變成"編碼-字符"的map,便於查找比對。
    mapRTable := make(map[string]string)

    for k, v := range mapTable {
        mapRTable[v] = k
    }

    var strCode string
    getWord := func (b byte) (strWord string, r bool){
        strCode += string(b)
        strWord = mapRTable[strCode]
        if "" == strWord {
            return "", false
        }
        strCode = ""
        return strWord, true
    }

    strDecode := ""
    for _, v := range []byte(str) {
        //每讀取一個bit位都要進行一次搜索,目前效率有點兒低哈~.~
        if strWord, b := getWord(v); b {
            //若是匹配成功,則把匹配到的字符追加到結尾
            strDecode += strWord
        }
    }

    fmt.Printf("decode : [%s]\n", strDecode)
}

 


 

同步發表:http://www.fengbohello.top/blog/p/lkvq

相關文章
相關標籤/搜索