哈夫曼树又称最优二叉树,是n个带权叶子节点构成的所有二叉树中带权路径最短的二叉树。
6.4 哈夫曼树
6.4.1 哈夫曼树
对于哈夫曼树的学习,我们应该先了解一些相关概念:
- 路径:从树中一个结点到另一个结点之间的分支。
- 路径长度:路径上的分支数目。
- 树的路径长度:从树的根节点到每一个结点的路径长度之和。(ps:书上的定义是这么写的,但我感觉准确的说应该是到每一个叶子节点的路径长度之和。)在节点数目相同的二叉树中完全二叉树路径长度最短。
- 结点的权:在一些应用中,赋予树中结点一个有某种意义的实数。
- 结点的带权路径长度:从结点到树的根结点之间的路径长度与节点上权的乘积。
- 树的带权路径长度:树中所有叶子的带权路径长度之和,也称树的代价。
公式为:,n表示叶子数目,wi表示叶子i的权值,li表示根到结点i之间的路径长度。
那么我们可以给哈夫曼树定义为:在权为w1,w2,w3...,wn的n个叶子构成的所有二叉树中,带权路径长度WPL最小(代价最小)的二叉树成为哈夫曼树,或称最优二叉树。
哈夫曼树有三个特点:
- 由n个带权叶子结点所构成的二叉树中,满二叉树或完全二叉树不一定是哈夫曼树(因为权值不一定相同);只有当叶子上的权值均相同时,满二叉树或者完全二叉树才是哈夫曼树。
- 在哈夫曼树中,权越大的叶子离根越近。
- 哈夫曼树的形态不唯一,其WPL最小。
哈夫曼算法的基本思想是:
- 根据给定的口个权值 w1,w2,…wn,构造包含n棵二叉树的森林F={T1,T2,...,Tn},其中每棵二叉树Ti中都只有一个带权wi的根结点,其左子树和右子树均为空。
- 在森林F中选出两棵根结点权值最小的树(当这样的树不止两棵树时,可以从中任选两棵),将这两棵树合并成一棵新树,为了保证新树仍是一棵二叉树,需要增加一个新结点作石新树的根,并将所选的两根树的根分划作为新树根的左右孩子(谁左谁有可以任意,将这两个孩子的权值之和作为新树根的权值。
- 在森林上中删除2.选中的那两棵根结点权值最小的二叉树,同时将新得到的二叉树加人森林F中。
- 重复2.和3.,直到森林F中只剩下一棵树为止。这棵树便是哈夫曼树。
图示该过程为:
可以看出:
- 在初始森林中的n棵二叉树,每一棵都有一个孤立的结点,它们既是根又是叶子;
- n个叶子的哈夫曼树要经过n-1次合并,产生n-1个新结点,共包含2n-1个结点;
- 在哈夫曼树中没有度数为1的结点。
哈夫曼树的存储表示:
图示:
代码:
typedef struct{
int weight;//权值
int parent,lchild,rchild;//双亲,左、右孩子在数组中的下标
}HTNode;//定义结点
typedef HTNode *HuffmanTree;//定义树
构造哈夫曼树:
代码:
//初始化哈夫曼树
void InitHuffmanTree(HuffmanTree &HT,int m){
HT=new HTNode[m];//为HT分配m个HTNode空间
for(int i=0;i<m;i++){
HT[i].weight=0;
HT[i].lchild=-1;
HT[i].rchild=-1;
HT[i].parent=-1;
} //初始化每个结点:权值为0,左、右孩子和双亲结点的下标为-1
}
//在HT[0...i]中找到两个权值最小结点,s1最小,s2次小
void SelectMin(HuffmanTree HT,int i,int &s1,int &s2){
int k=0;
while(HT[k].parent!=-1) k++;//找到第一个有双亲结点的结点,用k记录下标
s1=k;//将k赋值给s1
for(j=0;j<=i;++j)
if((HT[j].parent==-1)&&(HT[j].weight<HT[s1].weight))
s1=j;
/*寻找为根节点的结点(因为在创造树时会将原叶子结点保留,
若不进行此步操作下次找出的还是原来的s1,s2)并且其权重小
于s1,则将其下标赋值给s1*/
k=0;
while((HT[k].parent!=-1)||(k==s1)) k++;
s2=k;
for(j=0;j<=i;++j)
if((HT[j].parent==-1)&&(HT[j].weight<HT[s2].weight)&&(j!=s1))
s2=j;//对s2操作同理,但要避开s1
}
void CreatHuffmanTree(HuffmanTree &HT,int n){
if(n<1)
Error("Parameter Error!");//如果要求构造没有叶子的哈夫曼树那肯定是要报错的
int m=2*n-1;//若有n个叶子结点则总共有2*n-1个结点
InitHuffmanTree(HT,m);//初始化,创建结点数为m的哈夫曼树
for(int i=0;i<n;i++){
int w;
cin>>w;
HT[i].weight=w;
}//为每一个叶子结点输入权值
for(i=n;i<m;++i){
SelectMin(HT,i-1,s1,s2);//在HT[0,...,i-1]中寻找两个最小值(既根结点权值最小的两棵树)
HT[s1].parent=i;
HT[s2].parent=i;
HT[i].lchild=s1;
HT[i].rchild=s2;
HT[i].weight=HT[s1].weight+HT[s2].weight; //创立新的结点HT[i]作为s1,s2的根结点
}
} //构造有n个结点的哈夫曼树
时间复杂度O(n^2)。
6.4.2 哈夫曼编码
相关概念:
- 编码:数据压缩过程。
- 解码:数据还原过程。
- 等长编码:将给定大小为n的字符集C中的每个字符设置同样码长。
- 变长编码:将频度高的字符串设置较短,频度低的字符串设置较长。(注意这个概念,一会要看的哈夫曼编码就是变长编码)
- 前缀编码:对字符集进行编码时,字符集中任一字符的编码都不是其他字符编码的前缀。
- 最优前缀编码:平均码长或者文件总长最小的前缀编码。
那么我们可以利用哈夫曼树而得到一个最优前缀编码。
哈夫曼编码:将给定的字符和其权值作为叶子和其权值,构造一棵哈夫曼树,并将树中左分支和右分支分别标记0和1;从根到叶子路径分支上的二进制组成字符串,作为该叶子所表示字符的编码。
哈夫曼编码的存储表示:
因为各字符串的编码长度不等,所以按照编码的长度动态分配空间。
typedef char **HuffmanCode;//动态分配数组存储哈夫曼表
/*使用两重指针 ** 的目的是为了表示一个指向指针数组的指针
例:char *HuffmanCode[256] 可以表示一个包含 256 个指针的数组,
每个指针可以指向不同字符的编码。
char **HuffmanCode 表示一个指向指针数组的指针。*/
构造哈夫曼编码:
代码:
void HUffmanCoding(HuffmanTree HT,HuffmanCode &HC,int n){
HC=new (char*)[n];//分配n个字符编码的头指针数组
char cd=new char[n];//分配求解编码时需要的辅助空间
cd[n-1]='\0';//编码结束符
for(int i=0;i<n;++i){//逐个叶子结点HT[i]进行编码
int start=n-1;//用start记录cd的起始位置(最开始时候指示cd的结束位置)
int c=i;//用c记录i,从HT[c]开始上溯
while((int f=HT[c].parent)>=-1){//从叶子结点一直上溯到根结点
cd[--start]=(HT(f).lchild==c)?'0':'1';
/*先将start前移一位,如果该结点为其双亲结点的左孩子,
则此时ch[start]=0,若为其双亲的右孩子 则此时ch[start]=1
一直上溯到根节点,start就前移到cd的起始位置了*/
c=f;//将c移到其双亲结点
}
HC[i]=new(char*)[n-start];//为第i个字符编码分配空间
StrCopy(HC[i],cd,start);//从cd第start个起开始复制编码到HC
}
delete cd[];//释放刚才所用的辅助空间
}
时间复杂度O(n^2)。
小结
今天对哈夫曼树做了比较系统的复习,这本书中给的代码有很多变量不进行定义直接使用,为了下次能够看懂我自己对变量进行了定义,有可能会出现格式错误,如果发现的话也希望可以帮我指正,我对很多地方在书上原有的基础上增加了新的注释,也是希望能够让代码能够更加清晰吧。