哈夫曼树与哈夫曼编码

哈夫曼树与哈夫曼编码



术语

i)路径和路径长度

在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。 路径中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。

ii)结点的权及带权路径长度

若对树中的每个结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。 结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。

iii)树的带权路径长度

树的带权路径长度:所有叶子结点的带权路径长度之和,记为WPL。



先了解一下哈夫曼树,之后再构造一棵哈夫曼树,最后分析下哈夫曼树的原理。

1)哈夫曼树

哈夫曼树是这样定义的:给定n个带权值的节点,作为叶子节点,构造一颗二叉树,使树的带权路径长度达到最小,这时候的二叉树就是哈夫曼树,也叫最优二叉树。

哈夫曼树具有如下性质:

1)带权路径长度最短

2)权值较大的结点离根较近


2)构造哈夫曼树

构造哈夫曼树的步骤如下:

假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:

1) 将w1、w2、…,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.xin3e.com/blog/p/lkvq


时间: 2024-07-30 13:43:55

哈夫曼树与哈夫曼编码的相关文章

Huffman tree(赫夫曼树、霍夫曼树、哈夫曼树、最优二叉树)

flyfish 2015-8-1 Huffman tree因为翻译不同所以有其他的名字 赫夫曼树.霍夫曼树.哈夫曼树 定义引用自严蔚敏<数据结构> 路径 从树中一个结点到另一个结点之间的分支构成两个结点之间的路径. 路径长度 路径上的分支数目称作路径长度. 树的路径长度 树的路径长度就是从根节点到每一结点的路径长度之和. 结点的带权路径长度 结点的带权路径长度就是从该结点到根节点之间的路径长度与结点上权的乘积. 树的带权路径长度 树的带权路径长度就是树中所有叶子结点的带权路径长度之和,通常记做

【数据结构】树与树的表示、二叉树存储结构及其遍历、二叉搜索树、平衡二叉树、堆、哈夫曼树与哈夫曼编码、集合及其运算

1.树与树的表示 什么是树? 客观世界中许多事物存在层次关系 人类社会家谱 社会组织结构 图书信息管理 分层次组织在管理上具有更高的效率! 数据管理的基本操作之一:查找(根据某个给定关键字K,从集合R 中找出关键字与K 相同的记录).一个自然的问题就是,如何实现有效率的查找? 静态查找:集合中记录是固定的,没有插入和删除操作,只有查找 动态查找:集合中记录是动态变化的,除查找,还可能发生插入和删除 静态查找--方法一:顺序查找(时间复杂度O(n)) int SequentialSearch(St

【算法总结】哈夫曼树和哈夫曼编码

一.哈夫曼树 1. 哈夫曼树也称最优二叉树. 叶子节点的权值是对叶子节点赋予的一个有意义的数值量. 设二叉树具有 n 个带权值的叶子结点,从根节点到各个叶子结点的路径长度与相应叶子结点权值的乘积之和叫做二叉树的带权路径长度. 给定一组具有确定权值的叶子结点,可以构造处不同的二叉树,将其中带权路径长度最小的二叉树称为哈夫曼树. 2. 基本思想: 初始化:由给定的 n 个权值 $\left\{ \omega_{1},\omega_{2},\cdots ,\omega_{n}\right\}$构造 n

《数据结构复习笔记》--哈夫曼树,哈夫曼编码

先来了解一下哈夫曼树. 带权路径长度(WPL):设二叉树有n个叶子结点,每个叶子结点带有权值 wk,从根结点到每个叶子结点的长度为 lk,则每个叶子结点的带权路径长度之和就是: 最优二叉树或哈夫曼树: WPL最小的二叉树. [例]有五个叶子结点,它们的权值为{1,2,3,4,5},用此权值序列可以构造出形状不同的多个二叉树. 其中结果wpl最小值的是:33=(1+2)*3+(3)*2+(4+5)*2: 哈夫曼树的构造: 每次把权值最小的两棵二叉树合并, 代码: typedef struct Tr

哈夫曼树以及哈夫曼编码的问题

今天看到一个哈夫曼编码的题目,给定一个字符串abcdabaa,问哈夫曼编码后的二进制串的总长度是多少,答案是14 对于哈夫曼树我是一点都不了解啊,所以一顿查找,总结出以下知识点,与大家分享:当然部分内容参考了下百度 哈夫曼树又称为最优二叉树,是一种带权路径最短的二叉树.哈夫曼树是二叉树的一种应用,在信息检索中很常用. 一些相关的概念: 1.节点之间的路径长度:从一个节点到另一个节点之间的分支数量称为两个节点之间的路径长度. 2.树的路径长度:从根节点到树中每一个节点的路径长度之和. 3.节点的带

哈夫曼树及哈夫曼编码

一,引言 如上图,是一个判断体重在什么范围内的判定树,例如,学校体检的时候,我们反复用这个算法,当你输入一个体重:200斤,然后程序就开始反复判断了,经过三次判断,它发现你过重,然后重启系统了,又来一个人,还是200斤,三次判断之后,又系统重启了-后面的200多个200多斤的盘子判断完了之后,来了个120的,终于是个比较正常的体重了,但是系统一判断完,系统还是重启,反复检查之后,发现你那台8086时代的电脑终于撑不住了~ 于是你改了下算法,换了一棵判定树,这次,先判断这个人是不是个200多斤的胖

哈夫曼树和哈夫曼编码

在一般的数据结构的书中,树的那章后面,著者一般都会介绍一下哈夫曼(HUFFMAN)树和哈夫曼编码.哈夫曼编码是哈夫曼树的一个应用.哈夫曼编码应用广泛,如JPEG中就应用了哈夫曼编码. 首先介绍什么是哈夫曼树. 哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树.所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的 路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数).树的带权路径长度记为WPL= (W1*L1+W2*L2+W3*L3+...+Wn*Ln),N个权值W

霍夫曼树及霍夫曼编码的C语言实现

从周五开始学习霍夫曼树,一直到今天终于完成,期间遇到了各种各样的棘手的问题,通过一遍遍在纸上分析每一步的具体状态得以解决.现在对学习霍夫曼树的过程加以记录 首先介绍霍夫曼树 霍夫曼树(Huffman Tree),又称最优二叉树,是一类带权路径长度最短的树.假设有n个权值{w1,w2,-,wn},如果构造一棵有n个叶子节点的二叉树,而这n个叶子节点的权值是{w1,w2,-,wn},则所构造出的带权路径长度最小的二叉树就被称为赫夫曼树. 这里补充下树的带权路径长度的概念.树的带权路径长度指树中所有叶

[数据结构] AVL树和AVL旋转、哈夫曼树和哈夫曼编码

1. AVL树 AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树.查找.插入和删除在平均和最坏情况下都是O(log n).增加和删除可能需要通过一次或多次树旋转来重新平衡这个树. 节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反).带有平衡因子1.0或 -1的节点被认为是平衡的.带有平衡因子 -2或2的节点被认为是不平衡的,并需要重新平衡这个树.平衡因子可以直接存储在每个节点中,或从可能存储在节点中的子树高度计算出来. 1.2AVL旋转 AVL树的基本操作一