Huffman算法(结合离散数学细致分析!)

摘要:

认识Huffman码的发展过程,学习Huffman算法思想,探索Huffman算法在数据压缩问题上的运用。

1、背景介绍

随着科技的发展,计算机成为了当今信息时代的产物。计算机的使用难免会存在信息的传输,由于计算机主要以二进制编码来进行交流,因此将信息编码成二进制位串的形式是必须的。如何将信息编码成合适的二进制位串使得数据的传输效率变得更高,对此人们进行了相当长的历史研究,从莫尔斯码到前缀码的最优前缀码,再到Huffman编码。目前对数据的压缩传输主要使用Huffman算法思想。

2、概念与术语

最优前缀码算法的输入、输出

输入: 一个符号集 S = { x 1 , x 2 , ⋯   , x n > } S=\left\{ x_{1},x_{2}, \cdots ,x_{n} >\right \} S={x1,x2,,xn>} 和每个符号出现的频率 f x i f_{x_{i}} fxi
输出: 每个符号集中元素所对应的前缀码 γ j ( x i ) \gamma_{j} \left( x_{i} \right) γj(xi),频率高的符号二进制编码长度相对较短,而频率高的符号二进制编码长度相对较长,并且任何二进制编码的前缀不会与其它编码匹配;

前缀码、最优前缀码的定义与例子

前缀码: 是一种编码方式,使得符号集 S = { x 1 , x 2 , ⋯   , x n } S=\left\{ x_{1},x_{2}, \cdots ,x_{n} \right \} S={x1,x2,,xn} 中每个符号 x i x_{i} xi 都有一个独一无二的二进制编码,这个编码不是其它任何一个编码的前缀,用于解决在计算机或其它设备之间传输或储存数据时候,莫尔斯码中因为“暂停”而产生的不确定性问题。

例如:

符号 x i x_{i} xi 概率 f x i f_{x_{i}} fxi 前缀码 γ 1 ( x i ) \gamma_{1} \left( x_{i} \right) γ1(xi)
A 0.05 01
B 0.2 10
C 0.25 001
D 0.1 110
E 0.4 111

最优前缀码: 是一种前缀码,通过使用最短的二进制编码来表示一个符号集 S = { x 1 , x 2 , ⋯   , x n } S=\left\{ x_{1},x_{2}, \cdots ,x_{n} \right \} S={x1,x2,,xn}。尽可能的将出现频率比较高的符号用较短的编码表示,而出现频率比较低的符号用比较长的编码表示,使得出来到平均编码长度 A B L ( γ j ) ABL\left( \gamma_{j} \right) ABL(γj) 最小。

例如:

符号 x i x_{i} xi 概率 f x i f_{x_{i}} fxi 前缀码 γ 1 ( x i ) \gamma_{1} \left( x_{i} \right) γ1(xi) 最优前缀码 γ 2 ( x i ) \gamma_{2} \left( x_{i} \right) γ2(xi)
A 0.05 01 1100
B 0.2 10 111
C 0.25 001 10
D 0.1 110 01
E 0.4 111 0

设: ∣ γ j ( x i ) ∣ |\gamma_{j} \left( x_{i} \right)| γj(xi) γ j ( x i ) \gamma_{j} \left( x_{i} \right) γj(xi) 的长度,
编码长度 = ∑ x ∈ S n f x i ∣ γ j ( x i ) ∣ = n ∑ x ∈ S f x i ∣ γ j ( x i ) ∣ A B L ( γ j ) = ∑ x ∈ S f x i ∣ γ j ( x i ) ∣ \begin{align*} \text{编码长度}&=\sum_{x\in S}^{}n f_{x_{i}}|\gamma_{j} \left( x_{i} \right)|\\ &=n\sum_{x\in S}^{} f_{x_{i}}|\gamma_{j} \left( x_{i} \right)|\\ ABL\left( \gamma_{j} \right)&=\sum_{x\in S}^{} f_{x_{i}}|\gamma_{j} \left( x_{i} \right)| \end{align*} 编码长度ABL(γj)=xSnfxiγj(xi)=nxSfxiγj(xi)=xSfxiγj(xi)
A B L ( γ 1 ) = 0.05 × 2 + 0.2 × 2 + 0.25 × 3 + 0.1 × 3 + 0.4 × 3 = 2.75 A B L ( γ 2 ) = 0.05 × 4 + 0.2 × 3 + 0.25 × 2 + 0.1 × 4 + 0.4 × 1 = 2.10 \begin{align*} ABL\left( \gamma_{1} \right)&=0.05\times 2+0.2\times 2+0.25\times 3+0.1\times 3+0.4\times 3=2.75\\ ABL\left( \gamma_{2} \right)&=0.05\times 4+0.2\times 3+0.25\times 2+0.1\times 4+0.4\times 1=2.10 \end{align*} ABL(γ1)ABL(γ2)=0.05×2+0.2×2+0.25×3+0.1×3+0.4×3=2.75=0.05×4+0.2×3+0.25×2+0.1×4+0.4×1=2.10
∵ A B L ( γ 1 ) > A B L ( γ 2 ) \because ABL\left( \gamma_{1} \right)>ABL\left( \gamma_{2} \right) ABL(γ1)>ABL(γ2)
∴ \therefore 可得出最优前缀码在表示符号时产生的二进制编码长度会比较短。

Huffman算法的思想

Huffman算法: 通过Huffman树的建立来获取Huffman编码。
Huffman编码: 是一种前缀码,也是最优前缀码,可以形象的通过Huffman树查找制定符号得到,向左孩子查找一次编码一个0,向右孩子查找一次编码一个1。
Huffman树: 又叫做最优树,是一种带权路径长度最短的树,符号结点都为叶子结点,且权值越大的结点越靠近树根结点。
Huffman树构建方法: 保持符号作为树的叶子结点,每次选取权值最小的结点作为孩子结点生成双亲结点,双亲结点可为两个孩子权值之和,再选择剩下的结点中包含新生成的双亲结点的两个最小权值结点,并重复上述过程。

3、算法设计

生成Huffman树的伪代码:
CreateHuffmanTree(TreeHead,Symbol,Frequence)
    create an priority queue Q by CrePriQueue(Symbol,Frequence)
    while Q has more than one element
        remove the two nodes t1 and t2 with the lowest frequence from Q
        insert t1 and t2 into tree by TreeHead
        create a new tree node t with t1 and t2 as its left and right children
        frequence[t1] + frequence[t2] as its frequence
        insert t into Q
    return the head node of the tree : TreeHead
生成Huffman编码的伪代码:
CreateHuffmanCode(HuffManTreeHead,Symbol)
    SymbolLen = length of Symbol
    create a two-dimensional array HC to store the Huffman codes
    create a one-dimensional array Code to store temporary Huffman codes
    for i=1 to SymbolLen
        c = i
        f = fatherNode
        while fatherNode is not NULL
            if Symbol[i] is fatherNode the left child
                append "0" to Code
                c = node of left child
            else
                append "1" to Code
                c = node of right child
        HC[i] = Code
        reset the array Code
    return HC

4、算法时间复杂度分析

Huffman算法时间复杂度

Huffman编码总时间复杂度: 主要由构建优先队列,优先队列的插入提取,构建Huffman树,求Huffman编码分别对于给定的符号种类数量 n n n 的时间复杂度决定,主要为 O ( n log ⁡ 2 n ) O\left(n \log_{2}{n} \right) O(nlog2n)
构建优先队列: 通常使用堆来实现,也可以使用排序来实现,时间复杂度为 O ( n log ⁡ 2 n ) O\left(n \log_{2}{n} \right) O(nlog2n)
优先队列取出和插入: 优先队列的取出和插入Huffman树的时间复杂度为 O ( log ⁡ 2 n ) O\left( \log_{2}{n} \right) O(log2n)
构建Huffman树: 从优先队列中反复取出最小权值的两个结点组成一个新的结点,对于优先队列中的 n n n 个结点时间复杂度为 O ( n log ⁡ 2 n ) O\left(n \log_{2}{n} \right) O(nlog2n)
求Huffman编码:通过从最小的叶子结点开始回溯到根节点,求出每个叶子结点符号所对应的Huffman编码,时间复杂度为 O ( n log ⁡ 2 n ) O\left(n \log_{2}{n} \right) O(nlog2n)
算法拓展 :总的来说算法的时间复杂度为 O ( n log ⁡ 2 n ) O\left(n \log_{2}{n} \right) O(nlog2n),在细节部分的算法时间复杂度仍可继续探讨;由于斐波拉契堆和二项队列在提取和插入最小值时候时间复杂度为 O ( 1 ) O(1) O(1),在构建优先队列时候能够优化算法,但是对于大多数数据集其性能差异并不显著。

5、算法的性质及其证明

定理4.27:从 T T T 构成的 S S S 的编码是一个前缀码。

反证法:
         ~~~~~~~~         假设: ∃ x 0 ∈ S \exists x_{0}\in S x0S T T T 构成的 S S S 的编码,不是一个前缀码。
         ∵ T ~~~~~~~~\because T         T 是一棵二叉树,不存在环,
         ∴ ~~~~~~~~\therefore          T T T 中查找到 x 0 x_{0} x0 的路径必然是某个 S S S 中的符号往下查找的必经之路,
         ∵ S ~~~~~~~~\because S         S 中的符号都为 T T T 的叶子结点,任意一个符号被找到后往下不再有路可寻,
         ∴ ~~~~~~~~\therefore          与所设命题相互矛盾,原命题成立!

定理4.28:与最优前缀码对应的二叉树是满的。

反证法:
         ~~~~~~~~         假设:最优前缀码的二叉树 T T T 中存在一个结点 u u u 只有一个孩子结点 v v v
         ~~~~~~~~         当结点 u u u 为树根结点时,将 u u u 结点的位置与以结点 v 为根的子树位置交换,则结点 u u u 以 结点 v v v 为根的子树的平均编码位数会变小;
         ~~~~~~~~         当结点 u u u 不为树根结点时,假设其双亲结点为 w w w ,则将 u u u 结点的位置与以结点 v v v 为根的子树位置交换,使得结点 v 作为结点 w w w 的孩子结点,同样使得结点 u u u 以 结点 v v v 为根的子树的平均编码位数会变小;
         ~~~~~~~~         因此可知与所设命题相互矛盾,原命题成立!

命题4.29:假设 u u u v v v T ∗ T^{*} T 的树叶,使得 d e p t h ( u ) < d e p t h ( v ) depth\left(u\right)depth(u)<depth(v),进一步假设在 T ∗ T^{*} T 对应于最优前缀码的标记中,树叶 u u u y ∈ S y\in S yS 标记且树叶 v v v z ∈ S z\in S zS 标记,那么 f y ≥ f x f_{y}\ge f_{x} fyfx

反证法:
         ~~~~~~~~         假设:最优前缀码的二叉树 T T T 中结点 u u u 和结点 v v v 对应的频率为 f y < f x f_{y}< f_{x} fy<fx
         ~~~~~~~~         当交换在结点 u u u 和结点 v v v的标记得到的编码后, f y f_{y} fy 的乘数由 d e p t h ( u ) depth\left(u\right) depth(u) 变为 d e p t h ( v ) depth\left(v\right) depth(v) f z f_{z} fz 的乘数由 d e p t h ( v ) depth\left(v\right) depth(v) 变为 d e p t h ( u ) depth\left(u\right) depth(u)
         ~~~~~~~~         可得到每个符号平均位数的表达式变化为:
A B L ( T ∗ ) = ∑ x ∈ S f x d e p t h ( x ) + f y d e p t h ( v ) − f y d e p t h ( u ) + f z d e p t h ( u ) − f z d e p t h ( v ) = ∑ x ∈ S f x d e p t h ( x ) + [ d e p t h ( v ) − d e p t h ( u ) ] ( f y − f z ) \begin{align*} ABL\left( T^{*} \right)&=\sum_{x\in S}^{}f_{x}depth\left(x\right)+f_{y}depth\left(v\right)-f_{y}depth\left(u\right)+f_{z}depth\left(u\right)-f_{z}depth\left(v\right)\\ &=\sum_{x\in S}^{}f_{x}depth\left(x\right)+\left[depth\left(v\right)-depth\left(u\right)\right]\left(f_{y}-f_{z}\right) \end{align*} ABL(T)=xSfxdepth(x)+fydepth(v)fydepth(u)+fzdepth(u)fzdepth(v)=xSfxdepth(x)+[depth(v)depth(u)](fyfz)

         ∵ f y − f z < 0 ~~~~~~~~\because f_{y}-f_{z}<0         fyfz<0,交换后使得平均编码长度比原来的最优前缀码二叉树 T T T 更小了,
         ∴ ~~~~~~~~\therefore         与所设命题相互矛盾,原命题成立!

定理4.30: w w w T ∗ T^{*} T 的一片树叶。

反证法:
         ~~~~~~~~         设:最优前缀码的二叉树 T ∗ T^{*} T 有最大深度的一片树叶 v v v,树叶 v v v 的双亲为 u u u,而 u u u 有另外一个孩子 w w w v v v 的兄弟结点。
         ~~~~~~~~         假设: w w w 不是 T ∗ T^{*} T 的一片树叶。
         ~~~~~~~~         就会使得 w w w 的子树中将会存在某片树叶 w ′ w^{'} w
         ~~~~~~~~          w ′ w^{'} w 的深度将会大于最大深度树叶结点 v v v
         ~~~~~~~~         因此可知与所设命题相互矛盾,原命题成立!

定理4.31:存在一个与树 T ∗ T^{*} T 对应的最优前缀码,其中两个最低频率的字母被指定为树叶,这两片树叶是 T ∗ T^{*} T 中的兄弟。

证明:
         ~~~~~~~~         设:最优前缀码的二叉树 T ∗ T^{*} T 有最大深度的一片树叶 v v v,树叶 v v v 的双亲为 u u u,而 u u u 有另外一个孩子 w w w v v v 的兄弟结点。
         ~~~~~~~~         由定理 4.30 4.30 4.30 可知 v v v w w w 是最大深度的兄弟树叶,然后通过一层一层的标记得到 T ∗ T^{*} T
         ~~~~~~~~         由定理 4.29 4.29 4.29 可知最后将到达包含 v v v w w w 的层,在这一层上的树叶将会是最低频率的字母;
         ~~~~~~~~         由于已经证明了在这层内部将这些字母分配给树叶顺序是互不相关的,
         ~~~~~~~~         因此存在最优标记, v v v w w w 得到全部字母中的两个最低频率的字母。
         ~~~~~~~~         因此,原命题成立!

定理4.32: A B L ( T ′ ) = A B L ( T ) − f ω ABL\left(T^{'}\right)=ABL\left(T\right)-f_{\omega } ABL(T)=ABL(T)fω

证明:
         ~~~~~~~~         设: y ∗ y^{*} y z ∗ z^{*} z S S S 中频率最小的两个字母,合并它们后产生一个新的结点,其频率为 f ω = f y ∗ + f z ∗ f_{\omega}=f_{y^{*}}+f_{z^{*}} fω=fy+fz 作为它们的双亲结点,并加入到 S S S,使得变为 S ′ S^{'} S T T T 变为 T ′ T^{'} T A B L ( T ) ABL\left(T\right) ABL(T) 为编码 S S S 中的字母使用的平均位数, A B L ( T ′ ) ABL\left(T^{'}\right) ABL(T) 为编码 S ′ S^{'} S 中的字母使用的平均位数; y ∗ y^{*} y z ∗ z^{*} z T T T 中的深度每个都比 ω \omega ω T ′ T^{'} T 中的深度大 1 1 1,则有:
A B L ( T ) = ∑ x ∈ S f x ⋅ d e p t h T ( x ) = f y ∗ ⋅ d e p t h T ( y ∗ ) + f z ∗ ⋅ d e p t h T ( z ∗ ) + ∑ x ≠ y ∗ , z ∗ f x ⋅ d e p t h T ( x ) = ( f y ∗ + f z ∗ ) ⋅ ( 1 + d e p t h T ′ ( ω ) ) + ∑ x ≠ y ∗ , z ∗ f x ⋅ d e p t h T ′ ( x ) = f ω ⋅ ( 1 + d e p t h T ′ ( ω ) ) + ∑ x ≠ y ∗ , z ∗ f x ⋅ d e p t h T ′ ( x ) = f ω + f ω ⋅ d e p t h T ′ ( ω ) + ∑ x ≠ y ∗ , z ∗ f x ⋅ d e p t h T ′ ( x ) = f ω + ∑ x ∈ S ′ f x ⋅ d e p t h T ′ ( x ) = f ω + A B L ( T ′ ) \begin{align*} ABL\left( T \right)&=\sum_{x\in S}^{}f_{x}\cdot depth_{T}\left(x\right)\\ &=f_{y^{*}}\cdot depth_{T}\left(y^{*}\right)+f_{z^{*}}\cdot depth_{T}\left(z^{*}\right)+\sum_{x\ne y^{*},z^{*}}^{}f_{x}\cdot depth_{T}\left(x\right)\\ &=\left(f_{y^{*}}+f_{z^{*}}\right)\cdot \left(1+depth_{T^{'}}\left(\omega \right)\right)+\sum_{x\ne y^{*},z^{*}}^{}f_{x}\cdot depth_{T^{'}}\left(x\right)\\ &=f_{\omega}\cdot \left(1+depth_{T^{'}}\left(\omega \right)\right)+\sum_{x\ne y^{*},z^{*}}^{}f_{x}\cdot depth_{T^{'}}\left(x\right)\\ &=f_{\omega}+ f_{\omega}\cdot depth_{T^{'}}\left(\omega \right)+\sum_{x\ne y^{*},z^{*}}^{}f_{x}\cdot depth_{T^{'}}\left(x\right)\\ &=f_{\omega}+ \sum_{x\in S^{'}}^{}f_{x}\cdot depth_{T^{'}}\left(x\right)\\ &=f_{\omega}+ABL\left( T^{'} \right) \end{align*} ABL(T)=xSfxdepthT(x)=fydepthT(y)+fzdepthT(z)+x=y,zfxdepthT(x)=(fy+fz)(1+depthT(ω))+x=y,zfxdepthT(x)=fω(1+depthT(ω))+x=y,zfxdepthT(x)=fω+fωdepthT(ω)+x=y,zfxdepthT(x)=fω+xSfxdepthT(x)=fω+ABL(T)
         ~~~~~~~~         因此,原命题成立!

定理4.33:对于给定字母表的 Huffman 码得到任意前缀码的字母平均位数的最小值。

反证法:
         ~~~~~~~~         假设:通过贪心算法产生的树 T T T 不是最优的,存在一颗被标记的二叉树 Z Z Z 使得 A B L ( Z ) < A B L ( Z ) ABL\left(Z\right)ABL(Z)<ABL(Z)
         ~~~~~~~~         由命题 4.31 4.31 4.31 可知这颗树 Z Z Z 存在且 y ∗ y^{*} y z ∗ z^{*} z 的树叶是兄弟;
         ~~~~~~~~         与命题 4.30 4.30 4.30 类似,通过从 Z Z Z 中 删除标记为 y ∗ y^{*} y z ∗ z^{*} z 的树叶,并把它们的双亲结点标记为 ω \omega ω 得到一颗树 Z ′ Z^{'} Z,他对于 S ′ S^{'} S 定义了一个前缀码;在 ω \omega ω 下面加上标记为 y ∗ y^{*} y z ∗ z^{*} z 的树叶使得 Z ′ Z^{'} Z 变成 Z Z Z,这与 T ′ T^{'} T 变成 T T T 也是雷同;
         ~~~~~~~~         由命题 4.32 4.32 4.32 可知: A B L ( Z ′ ) = A B L ( Z ) − f ω ABL\left( Z^{'} \right)=ABL\left( Z \right)-f_{\omega} ABL(Z)=ABL(Z)fω A B L ( T ′ ) = A B L ( T ) − f ω ABL\left( T^{'} \right)=ABL\left( T \right)-f_{\omega} ABL(T)=ABL(T)fω;
         ∵ ~~~~~~~~\because          A B L ( Z ) < A B L ( T ) ABL\left(Z\right)ABL(Z)<ABL(T),两边可同时减去 f ω f_{\omega} fω
\par          ∴ ~~~~~~~~\therefore          A B L ( Z ′ ) < A B L ( T ′ ) ABL\left(Z^{'}\right)ABL(Z)<ABL(T)
         ~~~~~~~~         由于上面说明了任意 T ′ T^{'} T 都与 Z ′ Z^{'} Z 一样,通过相同方法一层层的保持着作为 S ′ S^{'} S 前缀码的树 T ′ T^{'} T 的最优性,必定不存在有另外一种作为 S ′ S^{'} S 前缀码的树 Z ′ Z^{'} Z 编码平均位长比 T ′ T^{'} T 还要小;
         ~~~~~~~~         因此可知与所设命题相互矛盾,原命题成立!

6、算法实现与实例应用

以下通过C++来实现Huffman算法,用HT表来表示Huffman树,用HC表来表示Huffman编码表。
#include
#include
#include
using namespace std;
#define OK 1        //Algorithm implementation success
#define ERROR 0     //Algorithm implementation failure
#define OVERFLOW 2      //overflow
#define Status int  //Program state

typedef struct
{
    char symb=' ';  //Symbol of node
    int weight=0;     //The weight of the node
    int parent,lchild,rchild;   //The parent of the node, the left child and the right child
} HTNode,*HuffmanTree;  //Dynamically allocated arrays store Huffman trees

typedef char** HuffmanCode; //Dynamically allocated arrays store Huffman encoding tables

//Select two nodes whose parent domain is 0 and whose weight is the smallest, and return the serial numbers s1 and s2 in HT
void Select(HuffmanTree &HT, int n, int &s1, int &s2)
{
    //Find the first node whose parent domain is 0 and has the lowest weight
    int min;
    for (int i = 1; i <= n; i++)	//The first parent field of 0 is found, and the subscript is temporarily stored to min
    {
        if (HT[i].parent == 0)
        {
            min = i;
            break;
        }
    }

    for (int i = 1; i <= n; i++)
    {
        if (HT[i].parent == 0)
        {
            if (HT[i].weight < HT[min].weight)
            {
                min = i;
            }
        }
    }
    s1 = min;

    //Find the second node with the lowest weight and parent domain 0
    for (int i = 1; i <= n; i++)	//The first parent field of 0 is found, and the subscript is temporarily stored to min
    {
        if (HT[i].parent == 0 && i != s1)
        {
            min = i;
            break;
        }
    }

    for (int i = 1; i <= n; i++)
    {
        if (HT[i].parent == 0 && i != s1)
        {
            if (HT[i].weight < HT[min].weight)
            {
                min = i;
            }
        }
    }
    s2 = min;
}

Status CreateHuffmanTree(HuffmanTree &HT,int n)
{
    //Build the Huffman tree HT
    if(n<=1) return OK;
    int m=2*n-1;    //The number of nodes used by Huffman tree
    HT=new HTNode[m+1];     //Create space
    if(HT==NULL) return OVERFLOW;    //Space allocation failure
    for(int i=1; i<=m; ++i) //Initialization tree
    {
        HT[i].parent=0;
        HT[i].lchild=0;
        HT[i].rchild=0;
    }
    cout<<"Please enter"<>HT[i].symb>>HT[i].weight;      //Input data
    }
    //  for(int i=1;i<=n;++i) cout<>n;
    if(CreateHuffmanTree(HT,n)==1)      //Build the Huffman tree
        cout<<"Huffman Tree built successfully!"<

输入: A 5 B 20 C 25 D 10 E 40
输出:
HT表(Huffman树):

结点下标 符号 权值/频率 双亲 左孩子 右孩子
1 A 5 6 0 0
2 B 20 7 0 0
3 C 25 8 0 0
4 D 10 6 0 0
5 E 40 9 0 0
6 15 7 1 4
7 35 8 6 2
8 60 9 3 7
9 100 0 5 8

HC表(Huffman编码表):

结点下标 符号 Huffman编码
1 A 1100
2 B 111
3 C 10
4 D 1101
5 E 0

----以上为个人思考与见解,有误请指点,有想法也可联系交流!

               ~~~~~~~~~~~~~~               谢谢观看!

你可能感兴趣的:(算法分析,算法,c++,c语言,huffman,tree,霍夫曼树)