前言
哈夫曼树也叫最优二叉树,它可以用于数据压缩,实现编码和解码的过程。下文将描述哈夫曼树的构建方法以及其编码思想的应用。
基础知识
- 路径:两个节点之间的线。
- 某个节点的路径长度:该节点与根节点之间的路径数量。
- 树的路径长度:从根节点到每个节点的路径长度之和。
上图树的路径长度为0+1+1+2+2+3+3+4+4=20
4. 节点的带权路径长度:从该节点到树根之间的路径长度与节点上权的乘积。
5.树的带权路径长度:树中所有叶子节点的带权路径长度之和。、
哈夫曼树的定义
哈夫曼树:带权路径最短的二叉树。
哈夫曼树的构建
要让二叉树的带权路径最短,我们要让权值大的节点靠近根节点,权值小的节点作叶子结点。
哈夫曼算法(构造哈夫曼树的算法)
- 构造森林全是根:根据n个给定权值{W1,W2,.....,Wn}构成n棵二叉树的森林F={T1,T2,...,Tn}其中Ti只有一个带权为Wi的根节点。
- 选用两小造新树:在F中选取两棵根节点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根节点的权值为其左右子树上根节点的权值之和。
- 删除两小添新树:在F中删除这两棵树,同时将上一步得到的新二叉树加入森林中。
- 重复(2)、(3),直到森林中只有一棵树为止,这棵树即为哈夫曼树。
总结
- 哈夫曼算法中,初始有n棵二叉树,经过n-1次合并最终形成哈夫曼树。
- 经过n-1次合并产生n-1个新结点,且这n-1个新结点都是具有两个孩子的分支节点。
- 可见:哈夫曼树中共有n+n-1=2n-1个结点,且起所有分支节点的度均不唯一
度的定义是指该节点的子节点的个数。
哈夫曼树算法的实现
void CreatHuffmanTree(HuffmanTree HT,int n)
{
if(n<=1)
return;
int m = 2*n-1; //数组共2n-1个元素
HT = new HTNode[m+1]; //0号单元未用,HT[m]表示根节点
for(int i=1;i<m;i++) //将2n-1个元素的lch、rch、parent置为零
{
HT[i].lch=0;
HT[i].rch=0;
HT[i].parent=0;
}
for(int i=1;i<=n;i++) //输入前n个元素的weight值
cin>>HT[i].weight;
/*****初始化结束,下面开始建立哈夫曼******/
for(int i=n+1;i<=m;i++)
{
int s1,s2;
Select(HT,i-1,s1,s2); //调用Select函数选出权值最小的两个根
HT[s1].parent=i;HT[s2].parent=i; //改变parent,删除这两个结点
HT[i].lch=s1;HT[i].rch=s2;
HT[i].weight=HT[s1].weight+HT[s2].weight; //选用两小造新树
}
}
Select函数:
void Select(HuffmanTree HT,int n,int& s1,int& s2)
{
for(int i=1;i<=n;i++) //找到第一个没有用双亲的根节点
{
if(HT[i].parent==0)
{
s1 = i;
break;
}
}
for(int i=1;i<=n;i++)
{
if(HT[i].parent==0&&HT[i].weight<HT[s1].weight) //找第一个权值最小的根节点
{
s1 = i;
}
}
for(int i=1;i<=n;i++)
{
if(HT[i].parent==0&&i!=s1)
{
s2 = i;
break;
}
}
for(int i=1;i<=n;i++)
{
if(HT[i].parent==0&&i!=s1&&HT[i].weight<HT[s2].weight) //找第二个权值最小且与s1不同的根节点
{
s1 = i;
}
}
}
哈夫曼编码
在远程通讯中,要将待传字符转换为二进制的字符串
我们根据字符的出现频率,决定字符的编码长度。比如出现次数多的字符采用尽可能短的编码。生成的这种长度不等的编码串可以一定程度上节约空间。这就是哈夫曼编码思想。
哈夫曼编码设计
关键:要设计长度不等的编码,则必须使任一字符的编码都不是另一字符编码的前缀(称为前缀编码)。
方法:
- 统计字符集中每个字符在电文中出现的平均频率(概率越大,要求编码越短)。
- 利用哈夫曼树的特点:权越大的叶子离根越近;将每个字符的概率值作为权值,构造哈夫曼树。则概率越大的结点路径越短。
- 在哈夫曼树的每个分支上标0或1:结点的左分支标0,有右分支标1。
- 把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码。
保证是前缀编码:由于所有字符都是作为叶子结点,说明没有一个字符是另一个字符的祖先,所以每个叶子结点的编码就不可能是其他叶子结点的编码前缀。
保证字符编码总长最短:因为哈夫曼树的带权路径长度最短,所以字符编码长度最短。
结论:哈夫曼编码是最优前缀码!
哈夫曼编码的算法实现
void CreatHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n)
//从叶子到根逆向求每个字符的哈夫曼编码,存储在编码表HC中
{
HC = new char*[n+1]; //分配n个字符编码的头指针矢量
char* cd=new char[n]; //分配临时存放编码的动态数组空间
cd[n-1]='\0';
for(int i=1;i<=n;i++) //逐个字符求哈夫曼编码
{
int start=n-1;
int c=i;
int f=HT[i].parent;
while(f!=0) //从叶子结点开始向上回溯,直到根节点
{
--start; //回溯一次start向前指一个位置
if(HT[f].lch==c) //结点c是f的左孩子,生成0
cd[start]='0';
else cd[start]='1'; //结点c是f的右孩子,生成代码1
c=f;
f=HT[f].parent; //继续向上回溯
}
HC[i]=new char[n-start]; //为第i个字符串编码分配空间
strcpy(HC[i],&cd[start]); //将求得的编码从临时空间cd复制到HC的当前行中
}
delete []cd; //释放临时空间
}