【数据结构】哈夫曼树

来源:互联网 发布:金正恩的发型 知乎 编辑:程序博客网 时间:2024/05/22 16:05

1、背景知识

1、路径和路径长度

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

2、结点的权及带权路径长度

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

3、树的带权路径长度

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

4、哈夫曼树

若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

2、哈夫曼算法

为了构造权值集合为{w1, w2, …, wn}的哈夫曼树,哈夫曼提出了一个构造算法,这个算法就是哈夫曼算法。基本思路如下:
(1)根据给定的n个权值{w1, w2, …, wn},构造具有n棵扩充二叉树的森林F = {T1, T2, T3, …, Tn},其中每棵扩充二叉树Ti只有一个带权值wi的根结点,其左右子树均为空。
(2)重复以下步骤,直到F中只剩下一棵树为止。
①在F中选取两棵根结点的权值最小的子树分别作为左右子树构造一棵新的二叉树。置新的二叉树的根结点的权值为其左右子树上根结点权值之和。
②在F中删去这两棵二叉树。
③把新的二叉树加入F。

最后得到的就是哈夫曼树。

下图是哈夫曼树构造的图解过程:

这里写图片描述

注:从哈夫曼算法可知,实际上构造出来的哈夫曼树可以不止一棵。

3、哈夫曼树的结点表示

这里用C++中的结构类型描述哈夫曼树中的结点

template<typename T>struct HTNode{    T data;                 //结点值    double weight;          //权值    int parent;             //双亲结点    int rchild, lchild;     //左右孩子结点};

此处将lchild、rchild、parent依旧称为”指针”,他们实际上是三个整形指示器,分别指示结点在数组中的下标。因为C++语言数组的下界是0,故用-1来表示空指针。设置 parent 域有两个作用,其一是便于查找某个结点的双亲,其二是可通过判断parent的值是否为-1来区分根结点和非根结点。

4、算法实现

哈夫曼算法的初始状态是一个初始森林,共有n课二叉树。同时每一次的合并,都会产生一个新结点,合并 n - 1 次共产生 n - 1 个新结点,他们都是具有两个孩子的分支结点。所以最终求得的哈夫曼树共有2n-1个结点。因此可以用大小为2n-1的一维数组来存放哈夫曼树中的结点,其存储结构如下:

const int N = 50;           //叶子的最多数目const int M = 2 * N - 1;    //树中结点的总数template <typename T>class huffmantree{public:    huffmantree(HTNode<T> ht[], int n);    ~huffmantree();private:    HTNode<T> HT[M + 1];};

用ht[]数组存放哈夫曼树,对于具有 n 个叶子结点的哈夫曼树,总共有 2n - 1 个结点。其算法的基本思路是,n 个叶子结点(存放在 ht[0] ~ ht[n - 1] 中)只有data和weight域值,先将所有 2n - 1个结点的parent、rchild、lchild域置初值为 -1 。处理每个非叶子结点ht[i](存放在 ht[n] ~ ht[2n - 2]中)如下:从 ht[0] ~ ht[i - 2] 中找出根结点(即其parent域为-1)最小的两个结点ht[lnode]和ht[rnode],将他们作为 ht[i]的左右子树,ht[lnode]和ht[rnode]的双亲结点置为ht[i],并且ht[i].weight = ht[lnode].weight + ht[rnode].weight。如此操作直到所有2n - 1 个非叶子结点处理完毕。具体算法如下:

template<typename T>huffmantree::huffmantree(HTNode<T> ht[], int n) {    int i, k, lnode, rnode;    int min1, min2;    for (i = 0; i < 2 * n - 1; ++i)        ht[i].parent = ht[i].lchild = ht[i].rchild = -1;    for (i = n; i < 2 * n - 1; ++i) {               //构造哈夫曼树        min1 = min2 = 32767;        lnode = rnode = -1;        for (k = 0; k <= i - 1; ++k) {            if (ht[k].parent == -1) {               //只在尚未构造二叉树的结点中查找                if (ht[k].weight < min1) {                    min2 = min1;                    rnode = lnode;                    min1 = ht[k].weight;                    lnode = k;                } else if (ht[k].weight < min2) {                    min2 = ht[k].weight;                    rnode = k;                }            }        }        ht[lnode].parent = ht[rnode].parent = i;        ht[i].weight = ht[lnode].weight + ht[rnode].weight;        ht[i].lchild = lnode;        ht[i].rchild = rnode;    }    for (i = 0; i < 2 * n - 1; ++i) {        HT[i].data = ht[i].data;        HT[i].weight = ht[i].weight;        HT[i].parent = ht[i].parent;        HT[i].rchild = ht[i].rchild;        HT[i].lchild = ht[i].lchild;    }}

5、哈夫曼编码

哈夫曼树最经典的应用是在通信领域。经哈夫曼编码的信息消除了冗余数据,极大地提高了通信信道的传输效率。

1、介绍

哈夫曼编码是根据每个符号出现的频率,将其存储的定长编码转换成变长编码。为出现概率较高的字符指定较短的码字,而为出现概率较低的字符指定较长的码字,可以明显提高传输的平均性能。

表:哈夫曼编码

符号 概率 定长编码 变长编码 a 0.12 000 1111 b 0.40 001 0 c 0.15 010 110 d 0.08 011 1110 e 0.25 100 10

表中的两种二进制编码可以用二叉树表示,如图,其中,二叉树的叶结点标记字符,由根结点沿着二叉树路径下行,左分支标记为0,右分支标记为1,则每条从根结点到叶结点的路径唯一表示了该叶结点的二进制编码。

这里写图片描述

图:两种编码方案的二叉树示意图

平均编码长度最小的编码称为最优编码。哈夫曼树的加权路径长度就是相应编码的平均编码长度。我们称这样得到的编码为哈夫曼编码。根据前面介绍的哈夫曼树的性质,我们知道哈夫曼编码是最优的二进制编码。

2、讨论实现哈夫曼编码的算法

实现哈夫曼编码的算法可以分为两个部分:
(1)构造哈夫曼树,此算法在前面已给出。
(2)在哈夫曼树上求叶结点的编码。

求哈夫曼编码,实质上就是在已建立的哈夫曼树中,从叶结点开始,沿叶结点的双亲链域回退到根结点,每回退一步,就走过了哈夫曼树的一个分支,从而得到一位哈夫曼编码值,由于一个字符的哈夫曼编码是从根结点到相应叶结点所经过的路径上各分支所组成的”0”、”1”序列,因此先得到的分支代码为所求的编码的低位码,后得到的分支代码为所求编码的高位码,可以设置数组HuffmanCode用来存放各个字符的哈夫曼信息。

3、代码实现

//接着上面的哈夫曼树类定义struct HCode{    char cd[n];         //n为层数减一    int start;          //cd[start] ~ cd[n]存放当前结点的哈夫曼码};template <typename T>void huffmantree<T>::CreateHCode(HCode hcd[], int n) {    int i, f, c;    HCode hc;    for (i = 0; i < n; ++i) {               //根据哈夫曼树求哈夫曼编码        hc.start = n;        c = i;        f = HT[i].parent;        while (f != -1) {                   //循环直到树根结点            if (HT[f].lchild == c)          //处理左孩子结点                hc.cd[hc.start--] = '0';            else                hc.cd[hc.start--] = '1';    //处理右孩子结点            c = f;            f = HT[f].parent;        }        hc.start++;                         //start指向哈夫曼编码的最开始字符        hcd[i] = hc;    }}
1 0
原创粉丝点击