文章目录
- 哈夫曼树(最优二叉树)
- 哈夫曼树构造算法
- 哈夫曼构造算法的实现
- 哈夫曼编码
- 文件的编码和译码
哈夫曼树(最优二叉树)
定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
路径:从树中一个结点到另一个结点之间得分支构成这两个结点间的路径。
结点的路径长度:两结点间路径上的分支数。
树的路径长度:从树根到每一个结点的路径长度之和。记作:TL
结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树
权(weight):将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积。
树的带权路径长度:树中所有叶子结点的带权路径长度之和。
因为构造这种树的算法是由哈夫曼教授于1952年提出的,所以被称为哈夫曼树,相应的算法称为哈夫曼算法。
- 1.满二叉树不一定是哈夫曼树
2.哈夫曼树中权越大的叶子离根越近
3.具有相同带权结点的哈夫曼树不唯一
哈夫曼树构造算法
哈夫曼算法:
1.根据 n 个给定的权值{W1,W2,…,Wn}构成 n 棵二叉树的森林 F={T1,T2,…,Tn},其中 Ti 只有一个带权为 Wi 的根结点
- 构造森林全是根
2.在F中选取两棵根结点的权值最小的数作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。
- 选用两小造新树
3.在 F 中删除这两棵树,同时将新得到的二叉树加入森林中
- 删除两小添新人
4.重复②和③,直到森林中只有一棵树为止,这棵树即为哈夫曼树
- 重复2、3单根
哈夫曼算法口诀:
1.构造森林全是根;
2.选用两小造新树;
3.删除两小添新人;
4.重复2、3剩单根。
下面给个例子:
特点:
1.包含n个叶子结点的哈夫曼树中共有2n-1个结点
2.哈夫曼树的结点的度数为0或2,没有度为1的结点。
3.包含n棵树的森林要经过n-1次合并才能形成哈夫曼树,共产生n-1个新结点
总结:
1、在哈夫曼算法中,初始时有 n 棵二叉树,要经过n-1次合并最终形成哈夫曼树
2、经过 n-1 次合并产生 n-1 个新结点,且这 n-1 个新结点都是具有两个孩子的分支结点
可见,哈夫曼树中共有n+n-1 = 2n - 1个结点,且其所有的分支结点的度均不为1。
哈夫曼构造算法的实现
①我们采用顺序存储结构——一维结构数组
②结点类型定义
typedef struct
{
int weight;
int parent,lch,rch;
}HTNode,*HuffmanTree;
定义数组 HuffmanTree H;
例,有n = 8,权值为W={7,19,2,6,32,3,21,10},构造哈夫曼树
【步骤】
- 初始化 HT[1…2n-1] :lch=rch=parent=0;
- 输入初始 n 个叶子结点:置 HT[1…n]的weight值
(HT就是上图左边的结构数组) - 进行以下n-1次合并,依次产生n-1次个结点HT[i],i=n+1…2n-1:
a)在HT[1…i-1]中选两个未被选过(从parent==0的结点中选)的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].weight = HT[s1].weight + HT[s2].weight;
- HT[i].lch = s1; HT[i].rch =s2;
void CreateHuffmanTree(HuffmanTree &HT,int n)//构造哈夫曼树HT
{
if(n<=1) return;
m = 2*n - 1; //数组共2n-1个元素
HT = new HTNode[2*n]; //0号单元未用,HT[m]表示根结点
for(int i = 1; i <= m; ++i) //将2n-1个元素的 lch、rch、parent置为0
{
HT[i].parent = 0;
HT[i].lchild = 0;
HT[i].rchild = 0;
}
for(int i =1; i <= n ; ++i) //输入前n个元素的weight值
{
cin >> HT[i].weight;
}
for(int i = n + 1; i <= m; ++i) //合并产生n-1个结点——构造Huffman树
{
int s1,s2;
select(HT,i-1, s1, s2); //在HT[k](1≤k ≤i-1)中选择两个其双亲域为0,
//且权值最小的结点,并返回它们在HT中的序号s1和s2
HT[s1].parent = i;
HT[s2].parent = i; //表示从F中删除s1,s2
HT[i].lchild = s1;
HT[i].rchild = s2; //s1,s2分别作为i的左右孩子
HT[i].weight = HT[s1].weight + HT[s2].weight; //i的权值为左右孩子权值之和
}
}
哈夫曼编码
哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)。
怎么样的前缀码能使得电文总长最短?哈夫曼编码
方法:
- 统计字符集中每个字符在电文中出现的平均概率(概率越大,要求编码越短)。
- 利用哈夫曼树得特点:权越大得叶子离根越近;将每个字符得概率值最为权值,构造哈夫曼树。则概率越大的结点,路径越短。
- 在哈夫曼树的每个分支上标上0或1:
结点的左分支标0,右分支标1
把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码。
两个问题:
-
为什么哈夫曼编码能够保证是前缀编码?
答:因为没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其他叶结点编码的前缀 -
为什么哈夫曼树编码能够保证字符编码总长最短?
答:因为哈夫曼树的带权路径最短,故字符编码的总长最短
性质1:哈夫曼编码是前缀码
性质2:哈夫曼编码是最优前缀码
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].lchild == c) cd[start] ='0'; //结点c是f的左孩子,则生成代码0
else cd[start] ='1'; //结点c是f的右孩子,则生成代码1
c = f;f =HT[f].parent; //继续向上回溯
} //求出第i个字符的编码
HC[i] =new char[n-start]; //为第i个字符串编码分配空间
strcpy(HC[i], &cd[start]); // 将求得的编码从临时空间cd复制到HC的当前中
}
delete cd; //释放临时空间
}
文件的编码和译码
-
编码:
① 输入各字符及其权值
② 构造哈夫曼树——HT[i]
③ 进行哈夫曼编码——HC[i]
④ 查 HC[i],得到各字符的哈夫曼编码 -
解码:
① 构造哈夫曼树
② 依次读入二进制码
③ 读入0,则走向左孩子;读入1,则走向右孩子
④ 一旦到达某叶子时,即可译出字符
⑤ 然后再从根出发继续译码
学习笔记内容来自:青岛大学——王卓老师