哈夫曼树与哈夫曼编码

哈夫曼树与哈夫曼编码


术语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