23.10.16 哈夫曼树及其应用

哈夫曼树又称最优二叉树,是n个带权叶子节点构成的所有二叉树中带权路径最短的二叉树。

6.4 哈夫曼树

6.4.1 哈夫曼树

对于哈夫曼树的学习,我们应该先了解一些相关概念:

  1. 路径:从树中一个结点到另一个结点之间的分支。
  2. 路径长度:路径上的分支数目。
  3. 树的路径长度:从树的根节点到每一个结点的路径长度之和。(ps:书上的定义是这么写的,但我感觉准确的说应该是到每一个叶子节点的路径长度之和。)在节点数目相同的二叉树中完全二叉树路径长度最短。
  4. 结点的权:在一些应用中,赋予树中结点一个有某种意义的实数。
  5. 结点的带权路径长度:从结点到树的根结点之间的路径长度与节点上权的乘积。
  6. 树的带权路径长度:树中所有叶子的带权路径长度之和,也称树的代价。
    公式为:WPL=\sum_{i=1}^{n}w_{i}l_{i},n表示叶子数目,wi表示叶子i的权值,li表示根到结点i之间的路径长度。

那么我们可以给哈夫曼树定义为:在权为w1,w2,w3...,wn的n个叶子构成的所有二叉树中,带权路径长度WPL最小(代价最小)的二叉树成为哈夫曼树,或称最优二叉树。

哈夫曼树有三个特点:

  1. 由n个带权叶子结点所构成的二叉树中,满二叉树或完全二叉树不一定是哈夫曼树(因为权值不一定相同);只有当叶子上的权值均相同时,满二叉树或者完全二叉树才是哈夫曼树。
  2. 在哈夫曼树中,权越大的叶子离根越近。
  3. 哈夫曼树的形态不唯一,其WPL最小。

哈夫曼算法的基本思想是:

  1. 根据给定的口个权值 w1,w2,…wn,构造包含n棵二叉树的森林F={T1,T2,...,Tn},其中每棵二叉树Ti中都只有一个带权wi的根结点,其左子树和右子树均为空。
  2. 在森林F中选出两棵根结点权值最小的树(当这样的树不止两棵树时,可以从中任选两棵),将这两棵树合并成一棵新树,为了保证新树仍是一棵二叉树,需要增加一个新结点作石新树的根,并将所选的两根树的根分划作为新树根的左右孩子(谁左谁有可以任意,将这两个孩子的权值之和作为新树根的权值。
  3. 在森林上中删除2.选中的那两棵根结点权值最小的二叉树,同时将新得到的二叉树加人森林F中。
  4. 重复2.和3.,直到森林F中只剩下一棵树为止。这棵树便是哈夫曼树。

图示该过程为:

可以看出:

  1. 在初始森林中的n棵二叉树,每一棵都有一个孤立的结点,它们既是根又是叶子;
  2. n个叶子的哈夫曼树要经过n-1次合并,产生n-1个新结点,共包含2n-1个结点;
  3. 在哈夫曼树中没有度数为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 哈夫曼编码

相关概念:

  1. 编码:数据压缩过程。
  2. 解码:数据还原过程。
  3. 等长编码:将给定大小为n的字符集C中的每个字符设置同样码长。
  4. 变长编码:将频度高的字符串设置较短,频度低的字符串设置较长。(注意这个概念,一会要看的哈夫曼编码就是变长编码)
  5. 前缀编码:对字符集进行编码时,字符集中任一字符的编码都不是其他字符编码的前缀。
  6. 最优前缀编码:平均码长或者文件总长最小的前缀编码。

那么我们可以利用哈夫曼树而得到一个最优前缀编码。

哈夫曼编码:将给定的字符和其权值作为叶子和其权值,构造一棵哈夫曼树,并将树中左分支和右分支分别标记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)。

小结

今天对哈夫曼树做了比较系统的复习,这本书中给的代码有很多变量不进行定义直接使用,为了下次能够看懂我自己对变量进行了定义,有可能会出现格式错误,如果发现的话也希望可以帮我指正,我对很多地方在书上原有的基础上增加了新的注释,也是希望能够让代码能够更加清晰吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值