主要内容
基本概念
1)路径:由一个结点到另一个结点之间的所有分支共同构成。
2)路径长度:结点之间的分支数目。
3)树的路径长度:从树的根结点到其他所有结点的路径长度之和。
4)权:赋予某一实体的值。在数据结构中,实体包括结点和边,所以对应有结点权和边权。
5)结点的带权路径长度:结点与树的根结点之间的路径长度与结点权的乘积。
6)树的带权路径长度:所有叶结点的带权路径长度之和。
7)哈夫曼树(Huffman Tree):树的带权路径长度最小的树。
构造思路
假设有8个结点n1~n8。
权值越大的结点应该离树的根结点越近,因而先找到树中最小的两个结点n1和n2,并把它们作为最后一层的叶结点。
最小的两个结点作为新结点n9的左、右子树根结点后,又从n3~n9选出最小的两个结点n4和n6,并把它们作为新结点n10的左、右子树根结点。
不断重复上面的操作,直至n1~n8都成为哈夫曼树的叶结点。
最终得到的哈夫曼树没有度为1的结点,因此一棵有n个叶结点的哈夫曼树,一共有2n-1个结点,可以存储在一个大小为2n-1的一维数组中。
为了方便表示,增加一个单元的数组长度,从1号位置开始使用,所以数组长度为2n。
将叶结点n1~n8存储在前面的1~8号位置,其余结点存储在9~2n-1号位置。
存储结构
哈夫曼树是特殊的、带权路径长度最小的二叉树,当然可以使用传统的二叉链表存储。
但在树的存储结构篇里已经提过,顺序存储结构仅适用于完全二叉树,而二叉链表存在许多空指针域,再加上哈夫曼树的构造具有规律,而且结点数目确定,所以可以用一个确定的一维数组存储。
除了0号位置空出来,其余每个位置存放一个结点。每个数组元素包括四个信息:权值、父结点编号、左子树根结点编号和右子树根结点编号。
typedef struct
{
int weight;
int parent;
int lchild, rchild;
} HFNode, *HFBiTree; /*数组长度根据给定结点数n来确定*/
构造算法
void CreatHuffmanTree(HFBiTree &HT, int n)
{
if(n <= 1) return ERROR; /*结点数不足,报错*/
int m = 2*n-1; /*哈夫曼树结点数*/
HT = new HFNode[m+1]; /*分配数组空间*/
/*-------初始化-------*/
for(int i = 1; i <= m; i++)
{
HT[i].parent = HT[i].lchild = HT[i].rchild = 0;
}
for(int i = 1; i <= n; i++)
cin>>HT[i];
/*-----构建哈夫曼树-----*/
for(int i = n+1; i <= m; i++)
{
Select(HT, i-1, s1, s2);
/*从HT的1~i-1号位置中选出两个权值最小的结点,并返回它们的编号s1和s2*/
HT[s1].parent = HT[s2].parent = i;
HT[i].lchild = s1;
HT[1].rchild = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
}
哈夫曼编码的引入
哈夫曼树在通信、编码和数据压缩等技术领域有着广泛的应用,其中哈夫曼编码是构造通信码的一个典型应用。
在数据通信、数据压缩时,需要将数据文件转换成由二进制字符0/1组成的二进制串,这个过程称为编码。
编码的方案一般有三种:
等长编码方案 | 不等长编码方案 | 哈夫曼编码方案 | |||
字符 | 编码 | 字符 | 编码 | 字符 | 编码 |
a | 00 | a | 0 | a | 0 |
b | 01 | b | 01 | b | 10 |
c | 10 | c | 010 | c | 110 |
d | 11 | d | 111 | d | 111 |
为什么要引入哈夫曼编码?
1)为了使频率高的字符尽可能采用更短的编码,而频率低的字符可以采用稍长的编码,构造一种不等长编码以获得更好的空间效率。这也是文件压缩技术的核心思想。
2)但不等长编码不能随意设计,任何一个字符的编码都不可以是另一个字符的编码的前缀,不合理的编码将有可能导致在解码时出现二义性。
为了合理地设计不等长编码,同时考虑各个字符的使用频率,哈夫曼编码出现了。
在哈夫曼树中,权值越大的结点离根结点越近,路径长度越短,这与使用频率越高,编码越短的思想相同。
在哈夫曼树中,根结点到任何一个叶结点的路径都不可能是到另一个叶结点路径的一部分,这与任何一个字符的编码都不可以是另一个字符的编码的前缀的条件相同。
因此我们可以根据每个字符出现的概率,构造出一棵哈夫曼树。
求哈夫曼编码
设左分支为0,右分支为1。
我们已经知道由根结点到叶结点的路径可以求出一个字符的哈夫曼编码。但是考虑到哈夫曼树结点的存储结构,每个结点的信息包括:权值、父结点编号、左子树根结点编号和右子树根结点编号。
如果由根结点遍历到叶结点,访问下一个结点时总是得考虑左、右两个方向,显然,这样的求解效率很低。
反过来,如果从叶结点遍历到根结点,访问一下个结点,即父结点时,只需要考虑一个方向,显然这样的求解目的性更强,效率也更高。
typedef char **HFCode; /*动态分配数组存储哈夫曼编码表(指针数组)*/
void CreatHuffmanCode(HFBiTree &HT, HFCode &HC, int n)
{
/*从叶结点回溯到根结点求每个字符的哈夫曼编码,存储在编码表HC中*/
HC = new char *[n+1]; /*0号单元不用,分配存储n个字符编码的编码表空间*/
char *cd = new char[n]; /*分配临时存放每个字符编码的动态数组空间*/
cd[n-1] = '\0'; /*编码结束符*/
for(int i = 1; i <= n; i++) /*逐个字符求哈夫曼编码*/
{
/*哈夫曼编码是从根结点开始到叶结点,但因为算法中从叶结点向上回溯,所以编码也要从后往前写*/
start = n-1; /*start开始时指向最后,即编码结束符位置*/
int c = i, f = HT[i].parent; /*f是结点c的父结点编号*/
while(f != 0) /*从叶结点向上回溯,直到根结点(f == 0)*/
{
start--; /*回溯一次,start指向前一个位置*/
if(HT[f].lchild == c) cd[start] = '0'; /*左分支,代码0*/
else cd[start] = '1'; /*右分支,代码1*/
c = f; f = HT[f].parent; /*继续往上回溯*/
} /*求出第i个字符的编码*/
HC[i] = new char[n-start]; /*为第i个字符编码分配空间*/
strcpy(HC[i], &cd[start]); /*将求得的编码从临时空间cd复制到HC的当前行中*/
}
delete cd; /*释放临时空间*/
}