在此祝大家新年快乐,新的一年守住头发,不断进步!
一、哈夫曼树基本概念
(1)路径:从树中的一个结点到另一个结点之间的分支构成这两个结点之间的路径
(2)路径长度:路径上的分支数目称作路径长度。
(3)树的路径长度:从树根到每一结点的路径长度之和。
(4)权:将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
(5)结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积
(6)树的带权路径长度:树中所有叶子结点的带权路径长度之和。
看以下几个例子:
我们可以总结出:
- 哈夫曼树:最优二叉树(带权路径长度(WPL)最短的二叉树) ,带权路径长度最短是在“度相同”的树中比较而得的结果,因此有最优二叉树、最优三叉树之称。
- 满二叉树不一定是哈夫曼树
- 哈夫曼树中权越大的叶子离根越近
- 具有相同带权结点的哈夫曼树不唯一
根据权值越大的结点离根结点越近这个特点,哈夫曼最早给出了一个构造哈夫曼树的方法,称为哈夫曼算法
二、哈夫曼树的构造算法
典型的贪心算法:构造哈夫曼树时首先选择权值小的叶子结点。
哈夫曼算法(构造哈夫曼树的方法)
(1)根据n个给定的权值{w1,w2,w3…,wn}构成n棵二叉树的森林F={T1,T2,…,Tn},其中,Ti只有一个带权为wi的根结点。
- 构造森林全是根
(2)在F中选取两棵根结点的权值最小的数作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和
- 选用两小造新树
(3)在F中删除这两棵树,同时将新得到的二叉树加入森林中。
- 删除两小添新人
(4)重复(2)和(3),直到森林中只有一棵树为止,这棵树即为哈夫曼树。
- 重复2、3剩单根
例如:
(1)4个结点
(2)5个结点
新添加的结点都是度为2的结点。
总结:
(1)哈夫曼树的结点的度数为0或2,没有度为1的结点
(2)包含n个叶子结点的哈夫曼树中共有2n - 1个结点(每两个合并成为一个新结点,共合并n-1次)
(3)新生成的结点有两个孩子(度为2)
三、哈夫曼树构造算法的实现
- 采用顺序存储结构——一维结构数组
- 结构类型定义
typedef struct{
int weight;//结点的权值
int parent,lch,rch;//结点的双亲、左孩子、右孩子的下标
}HTNode,*HuffmanTree;//动态分配数组存储哈夫曼树
HuffmanTree H; //定义一个指针变量
例如,第一个节点权值为5,表示为H[i].weight=5
哈夫曼树中共有2n-1个结点,不使用0下标,数组大小为2n.
三、哈夫曼构造算法的实现
【算法步骤】
-
初始化HT[1,2…2n-1]:lch=rch=parent=0;
-
输人初始n个叶子结点:置HT[1,2…n]的weight值;
-
==进行以下n-1次合并,一次产生n-1个结点HT[i],i=n+1…2n-1;
a)在HT[1…i-1]中选两个未被选过的weight最小的两个结点HT[s1]和HT[s2],s1,s2为两个最小结点下标;
b)修改 HT[s1]和HT[s2]的parent值:HT[s1].parent=i;HT[s2].parent=i;
c)修改新产生的HT[i]:
HT[i].lch=s1;
HT[i].rch=s2;
HT[i].weight=HT[s1].weight+HT[s2].weight;
【算法实现】
#include<iostream>
using namespace std;
typedef struct{
int weight;//结点的权值
int parent,lch,rch;//结点的双亲、左孩子、右孩子的下标
}HTNode,*HuffmanTree;//动态分配数组存储哈夫曼树
void CreatHuffmanTree(HuffmanTree &HT,int n)
{//构造哈夫曼树HT
if(n<=1)
return ;
m=2*n-1; //数组共2n-1个元素
HT=new HTNode[m+1]; //0号单元未用,H[T]表示根结点
for(int i=1;i<m;i++)
{//将2n-1个元素的lch、rch、parent置为0
HT[i].lch=0;
HT[i].rch=0;
HT[i].parent=0;
}
for(int i=1;i<=n;i++)
{
cin>>HT[i].weight;//输入前n个元素的weight值
}
//初始化结束,下面开始建立哈夫曼树
for(int i=n+1;i<=m;i++)// 合并产生n-1个结点—构造哈夫曼树
{//通过n-1次选择、删除、合并来创建哈夫曼树
Select(HT,i-1,s1,s2);
//在HT[k](k>=1&&k<=i-1)中选择两个其双亲域为0
//且权值最小的结点,并返回它们在HT中的序号s1,s2;
HT[s1].parent=i;//将s1,s2的双亲域由0改为1
HT[s2].parent=i; //得到新结点i,从森林中删除s1,s2;
HT[i].lch=s1;
HT[i].rch=s2;//s1,s2分别作为i的左右孩子
HT[i].weight=HT[s1].weight+HT[s2].weight;
//i的权值为左右孩子权值之和
}
}
四、哈夫曼编码
前缀编码:如果在一个编码方案中,任一个编码都不是其他任何编码的前缀(最左子串),则称为前缀编码
问题引入:什么样的前缀编码能使电文总长最短?
——哈夫曼编码
方法:
1、统计字符集中每个字符在电文中出现的平均概率(概率越大,要求编码越短)。
2、利用哈夫曼树的特点:权越大的叶子离根越近;将每个字符的概率值作为权值,构造哈夫曼树,则概率越大的结点,路径越短。
3、在哈夫曼树的每个分支上标上0或1:
- 结点的左分支标0,右分支标1
- 把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码。
所以哈夫曼编码就是
对一颗具有n个叶子的哈夫曼树,若对树中的每个左分支赋予0,右分支赋予1,则从根到每个叶子的路径上,各分支的赋值分别构成一个二进制串,该二进制串就称为哈夫曼编码。
下面看两个问题:
- 为什么哈夫曼编码能够保证是前缀编码?
因为没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其它叶结点编码的前缀。
- 为什么哈夫曼编码能够保证字符编码总长最短?
因为哈夫曼树的带权路径长度最短,故字符编码的总长最短。
这就得出哈夫曼编码的两个性质:
- 哈夫曼编码是前缀编码
- 哈夫曼编码是最优前缀码
五、哈夫曼编码的算法实现
【算法步骤】
【算法描述】
#include<iostream>
using namespace std;
typedef char *HuffmanCode;//动态分配数组存储哈夫曼编码表
void CreateHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n)
{//从叶子到根逆向求每个字符的哈夫曼编码 ,存储在编码表HC中
HC=new char *[n+1]; //分配n个字符编码的头指针矢量
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--;
if(HT[f].lch==c)
cd[start]='0'; //结点c是f的左孩子,则生成代码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[statr]);//将求得的编码从临时空间cd复制到HC的当前行中
}
delete cd;//释放临时空间
}