- 哈夫曼树中的查找算法(Select)
- 哈夫曼树的构建(HuffmanTree)
- 哈夫曼编码的构建(HuffmanCoding)
- 打印哈夫曼树表(Print)
- 打印权值及其编码(Inputcode)
什么是哈夫曼树?
- 当有 n 个结点(都做叶子结点且都有各自的权值)构建一棵树时,如果构建的这棵树的带权路径长度(WPL)最小,称这棵树为“最优二叉树”,有时也叫“赫夫曼树”或者“哈夫曼树”(HuffmanTree)。
- 权值可以理解为访问频率,权值越大访问频率越高,访问次数越多;而
带权路径长度=权值 x 该节点到树根的路径长度
——WPL。
- 总而言之,哈夫曼树所构建出来的树,会将最频繁被访问的结点放在最前面,不常访问的结点依次往后排,这样的话每次都能最快速的查找到最常被访问的结点。
如何构建哈夫曼树?
- 选取并合并:选取取值最小的两个结点合并成一棵树(根节点为权值之和)
- 删除并加入:从序列中删除上述选取的两个两个最小结点,加入新合并的树
- 重复:重复上述操作即可得到哈夫曼树
- 不难发现在②中有两个权值都为5的结点,此时选取任意一个都是可行的,所以哈夫曼树的构造是不唯一的,但是带权路径长度WPL是唯一的!
代码实现哈夫曼树的构造
- 首先我们要知道,我们的哈夫曼树使用的是结构体数组,每一个结点有4个域。
- 拥有n个叶子结点的哈夫曼树,共有2n-1个结点
typedef struct
{
int weight;//权值
int parent,rchild,lchild;//存储地址(下标)
}HTnode;//每一个域存储的都是地址(数组下标)
- 我们需要建立起哈夫曼树组HuffTree[2n-1] (如下图,哈夫曼数组用HT[ ]代替)
- 数组初始化:建立好有2n-1个单元格的哈夫曼数组后要对每个单元初始化。
初始化内容包括:
① 把每个结点的地址域(parent,lchild,rchild)赋值为-1
② 给每个节点填上(输入)对应的权值。
- 完成前两步的初始化才开始构建哈夫曼树: ① 选出权值最小的两个结点(select函数)②找父母 ③找孩子 (见下列代码第30-40行)
HTnode *HuffmanTree(HTnode *HuffTree,int n)
{
int i;//i处理a[0]-a[n]
int k;//k处理a[n+1]-a[2n-1]
//1.建立HuffTree数组( HuffTree=&HuffTree[0]).
if(n<=1)
{
return;
}
HuffTree=(HTnode *)malloc((2*n-1)*sizeof(HTnode));//建立哈夫曼树组,有2n-1个结点,每个节点有4个域
//2.给HuffTree数组的地址域初始化为-1.
for(i=0;i<2*n-1;i++)
{//对2n-1个结点进行初始化
HuffTree[i].parent=-1;
HuffTree[i].lchild=-1;
HuffTree[i].rchild=-1;
}
//3.将权值写入HuffTree数组中.
printf("Please Enter Weights in Turn:");
for(i=0;i<n;i++)
{
scanf("%d",&HuffTree[i].weight);
}
//4.构建哈夫曼树.
for(k=n;k<2*n-1;k++)
{
int s1,s2;//第一小,第二小的地址下标
select(HuffTree,k,&s1,&s2);//1.选择出权值第一小s1,和第二小s2(s1,s2是下标、地址,不是数值!)
HuffTree[k].weight=HuffTree[s1].weight+HuffTree[s2].weight;//2.权值加和
HuffTree[s1].parent=k;//3.s1,s2找父母(默认左孩子是第一小,右孩子是第二小)
HuffTree[s2].parent=k;
HuffTree[k].lchild=s1; //4.父母K找孩子
HuffTree[k].rchild=s2;
}
return HuffTree;
}
如何构造select函数,选出权值最小的两个结点?
- 可行的方法:① 排序:在权值数组w[ ]中,我们可以先对数组元素排序,然后取走第一小和第二小。② 升级版选择排序:我写了一个哈夫曼树中的查找算法(Select),是选择排序的升级版。
- 需要注意的问题:① 只对parent=-1的结点进行查找 ② 我们要查找的是第一小、第二小在哈夫曼树组中的下标,而不是数值!
void select(HTnode *HuffTree,int k,int *s1,int *s2)
{
int min1=9999,min2=9999;
int i;
int m1,m2;//用于储存坐标
for(i=0;i<k;i++)
{
if(HuffTree[i].parent==-1)//判断父母为-1的结点参与比较
{
if(HuffTree[i].weight<min2)//小于第二小
{
if(HuffTree[i].weight<min1)//不但小于第二小,还小于第一小
{
min2=min1;
min1=HuffTree[i].weight;
m1=i;//储存第一小的坐标 (地址)
}
else //小于第二小,但大于第一小
{
min2=HuffTree[i].weight;
m2=i;//储存第一小的坐标(地址)
}
}
}
}
*s1=m1;//将第一小的坐标带入指针中传出
*s2=m2;//将第二小的坐标带入指针中传出
}
哈夫曼编码
- 什么是哈夫曼编码?
- 哈夫曼编码是不等长编码。
- 哈夫曼编码就是在哈夫曼树的基础上构建的,这种编码方式最大的优点就是用最少的字符包含最多的信息内容。
- 哈夫曼编码的优越性在哪?
- 哈夫曼编码是前缀编码(前缀永不重叠),且是最优前缀编码!因为哈夫曼编码是前缀永不重叠的前缀编码,保证了编码的可读取性与可解码性,与此同时结合自身是不等长编码的特性,实现了与等长编码一样的编码和解码功能,但编码更短,数据量更少。
- 看完上图,你就能明白什么叫用最少的字符包含最多的信息。
如何手动构建哈夫曼编码?
- 手动构建哈夫曼编码非常简单,只需要构建出哈夫曼树,然后按"左0右1"(左1右0也是可以的)给每个左右孩子标记上,最后从根节点走向每一个叶子结点,就可以得到每个叶子节点的哈夫曼编码。
如何用程序构建哈夫曼编码?
- 用程序构建哈夫曼编码比我们手动构建哈夫曼编码复杂得多。
char **HuffmanCoding(HTnode *HuffTree,char **Huffcode,int n)//哈弗曼编码
{
char *temp;//临时存储,用于存放生成的编码
int start;//用于记录temp数组的下标
int i,pos,parent;//用于记录HuffTree数组的下标
Huffcode=(char **)malloc(n*sizeof(char *));//建立哈夫曼编码数组
temp=(char *)malloc(n*sizeof(char));//n个叶子结点最长生成编码数为n-1位,故申请n个存储空间的数组(char temp[n])
temp[n-1]='\0';//初始化数组最后一位为‘\0 ’
for(i=0;i<n;i++)//n个叶子结点,循环n次,每一次对一个叶子结点进行编码
{
start=n-1;//temp数组将从后向前读入每一位编码
pos=i;
parent=HuffTree[i].parent;
while(parent!=-1)
{
if(HuffTree[parent].lchild==pos)//左0
{
temp[--start]='0';
}
else//右1
{
temp[--start]='1';
}
pos=parent;//将pos移位
parent=HuffTree[parent].parent;//将parent移位
}//while
Huffcode[i]=(char *)malloc((n-start)*sizeof(char));
strcpy(Huffcode[i],&temp[start]);//将temp中由start开始的字符串拷贝进哈夫曼树组中
}//for
free(temp);//释放零时存储
return Huffcode;//也可以定义成无返回值函数,则Huffcode要以char ***Huffcode进入函数
}
- 临时数组temp:
- 为什么需要一个临时存储数组temp?
因为程序在检索哈夫曼树的时候,是先由根节点往下检索到叶子结点,然后在判断当前叶子结点是左孩子还是右孩子,生成0或1,再逐步往上走(走向根节点)。也就是说程序的执行过程是:根节点—>叶子结点,叶子结点—>根节点,由根节点开始先找到叶子结点,再返回去生成哈夫曼编码。那么这样生成的哈夫曼编码和我们手动生成的编码顺序颠倒了过来,生成的是逆序编码,所以我们需要用一个临时数组temp先逆序存储生成的逆序编码(逆序编码被逆序存储故变为正序),在正序输出即可。 - 临时数组temp该定义多长呢?
该定义成有n个单元格的数组,因为哈夫曼树共有n个叶子结点,可能生成最长的编码是n-1,算上最后一个'\0'
,一共需要n个单元格。 'Huffcode[i]=(char *)malloc((n-start)*sizeof(char));'
(上代码第31行),我们知道哈夫曼编码是逆序在temp数组中生成的,每生成一位start就减1,那么这个(n-start)是什么意思呢?
所以我们知道了生成的编码长度就可以用malloc申请到对应的存储空间。
- 哈夫曼编码数组:
- Huffcode[i]数组的下标与哈夫曼数组HuffTree[i](0≤ i ≤n)的前n个元素一一对应,也就是n个叶子结点。也就是说哈夫曼数组HuffTree[ ]中第i个元素的哈夫曼编码储存在Huffcode[i]中(0≤ i ≤n)。
- 哈弗曼编码并不是直接存储在Huffcode[ ]数组的每一个单元格当中,生成的哈夫曼编码是一串字符串,而Huffcode[ ]数组就是指向这些字符串的指针,所以哈夫曼编码数组Huffcode[ ]中的每一个元素都是指针!
完整哈夫曼树、哈夫曼编码源代码
(内涵打印函数,可直接展示结果)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct
{
int weight;
int parent,rchild,lchild;
}HTnode;
void select(HTnode *HuffTree,int k,int *s1,int *s2)
{
int min1=9999,min2=9999;
int i;
int m1,m2;
for(i=0;i<k;i++)
{
if(HuffTree[i].parent==-1)//判断父母为-1的结点参与比较
{
if(HuffTree[i].weight<min2)//小于第二小
{
if(HuffTree[i].weight<min1)//不但小于第二小,还小于第一小
{
min2=min1;
min1=HuffTree[i].weight;
m1=i;//储存第一小的坐标 (地址)
}
else //小于第二小,但大于第一小
{
min2=HuffTree[i].weight;
m2=i;//储存第一小的坐标(地址)
}
}
}
}
*s1=m1;//将第一小的坐标带入指针中传出
*s2=m2;//将第二小的坐标带入指针中传出
}
HTnode *HuffmanTree(HTnode *HuffTree,int n)
{
int i;//i处理a[0]-a[n]
int k;//k处理a[n+1]-a[2n-1]
//1.建立HuffTree数组( HuffTree=&HuffTree[0]).
if(n<=1)
{
return;
}
HuffTree=(HTnode *)malloc((2*n-1)*sizeof(HTnode));//建立哈夫曼树组,有2n-1个结点,每个节点有4个域
//2.给HuffTree数组初始化为-1.
for(i=0;i<2*n-1;i++)
{//对2n-1个结点进行初始化
HuffTree[i].parent=-1;
HuffTree[i].lchild=-1;
HuffTree[i].rchild=-1;
}
//3.将权值写入HuffTree数组中.
printf("Please Enter Weights in Turn:");
for(i=0;i<n;i++)
{
scanf("%d",&HuffTree[i].weight);
}
//4.构建哈夫曼树.
for(k=n;k<2*n-1;k++)
{
int s1,s2;
select(HuffTree,k,&s1,&s2);//1.选择出权值第一小s1,和第二小s2(s1,s2是下标、地址,不是数值!)
HuffTree[k].weight=HuffTree[s1].weight+HuffTree[s2].weight;//2.权值加和
HuffTree[s1].parent=k;//3.s1,s2找父母(默认左孩子是第一小,右孩子是第二小)
HuffTree[s2].parent=k;
HuffTree[k].lchild=s1; //4.父母K找孩子
HuffTree[k].rchild=s2;
}
return HuffTree;//将建立好的哈夫曼数组地址带回主函数
也可以定义成无返回值函数,则HuffTree要以char **Huffcode进入函数
}
char **HuffmanCoding(HTnode *HuffTree,char **Huffcode,int n)//哈弗曼编码
{
char *temp;//临时存储,用于存放生成的编码
int start;//用于记录temp数组的下标
int i,pos,parent;//用于记录HuffTree数组的下标
Huffcode=(char **)malloc(n*sizeof(char *));//建立哈夫曼编码数组
temp=(char *)malloc(n*sizeof(char));//n个叶子结点最长生成编码数为n-1位,故申请n个存储空间的数组(char temp[n])
temp[n-1]='\0';//初始化数组最后一位为‘\0 ’
for(i=0;i<n;i++)//n个叶子结点,循环n次,每一次对一个叶子结点进行编码
{
start=n-1;//temp数组将从后向前读入每一位编码
pos=i;
parent=HuffTree[i].parent;
while(parent!=-1)
{
if(HuffTree[parent].lchild==pos)//左0
{
temp[--start]='0';
}
else//右1
{
temp[--start]='1';
}
pos=parent;//将pos移位
parent=HuffTree[parent].parent;//将parent移位
}//while
Huffcode[i]=(char *)malloc((n-start)*sizeof(char));
strcpy(Huffcode[i],&temp[start]);//将temp中由start开始的字符串拷贝进哈夫曼树组中
}//for
free(temp);//释放零时存储
return Huffcode;//也可以定义成无返回值函数,则Huffcode要以char ***Huffcode进入函数
}
void print(HTnode *HuffTree,int m)//打印哈夫曼树表
{
int i;
printf("\n");
printf(" --------------------------------------------- \n");
printf(" Huffman Tree:\n\n");
printf(" Loc weight parent lchild rchild\n");
for(i=0;i<m;i++)
{
printf(" HT[%d] %d %d %d %d\n",i,HuffTree[i].weight,HuffTree[i].parent,HuffTree[i].lchild,HuffTree[i].rchild);
}
printf("\n --------------------------------------------- \n");
}
void Inputcode(char **Huffcode,HTnode *HuffTree,int n)//打印权值对应其哈弗曼编码
{
int i;
printf(" Huffman code:\n\n");
for(i=0;i<n;i++)
{
printf(" Weight:%d code:%s\n",HuffTree[i].weight,Huffcode[i]);
}
}
int main()
{
HTnode *HuffTree;
char **Huffcode;//Huffcode是指向字符指针类型数组的指针
int n;//n个叶子结点
int i;
printf("Please Enter the Number of Elements:");
scanf("%d",&n);
printf("\n");
HuffTree=HuffmanTree(HuffTree,n);
print(HuffTree,2*n-1);
Huffcode=HuffmanCoding(HuffTree,Huffcode,n);
Inputcode(Huffcode,HuffTree,n);
return 0;
}
执行结果
其他问题
- 在哈夫曼树组HuffTree[i]与哈夫曼编码数组Huffcode[i]中,均采用的是用指针指向用malloc所申请的连续的数组存储区域,也就是说HuffTree和Huffcode是指针,在此处均采用以指针做数组名的方式对数组元素进行访问。(了解“指针如何做数组名”)
- HuffTree、Huffcode变量是指针,故在主函数中应当定义成相应的指针类型,HuffTree是
HTnode *HuffTree;
,而Huffcode指向的又是一个指针数组,所以Huffcode要定义成指向指针的指针char **Huffcode;
,二重指针!
- HuffmanTree和HuffmanCoding函数涉及到传参的问题,两个函数走的流程都是:传入一个变量->对变量进行操作修改->将变量传出。那我们如何能将操作完后的变量传出带回主函数呢?
① return,把两个函数写成带返回值的函数,最后将地址传出即可(上文所使用的方法)
② 传入函数的时候用指针做形参(写法如下)
//HT为地址传递的存储哈夫曼树的数组,w为存储结点权重值的数组,n为结点个数
void HuffmanTree(HTnode **HT, int *w, int n)
{
if(n<=1) return; // 如果只有一个编码就相当于0
int m = 2*n-1; // 哈夫曼树总节点数,n就是叶子结点
*HT = (HTnode *) malloc((m+1) * sizeof(HTnode)); // 0号位置不用
HTnode *p = *HT;
// 初始化哈夫曼树中的所有结点
for(int i = 1; i <= n; i++)
{
(p+i)->weight = *(w+i-1);
(p+i)->parent = 0;
(p+i)->left = 0;
(p+i)->right = 0;
}
//从树组的下标 n+1 开始初始化哈夫曼树中除叶子结点外的结点
for(int i = n+1; i <= m; i++)
{
(p+i)->weight = 0;
(p+i)->parent = 0;
(p+i)->left = 0;
(p+i)->right = 0;
}
//构建哈夫曼树
for(int i = n+1; i <= m; i++)
{
int s1, s2;
Select(*HT, i-1, &s1, &s2);
(*HT)[s1].parent = (*HT)[s2].parent = i;
(*HT)[i].left = s1;
(*HT)[i].right = s2;
(*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight;
}
}
//HT为哈夫曼树,HC为存储结点哈夫曼编码的二维动态数组,n为结点的个数
void HuffmanCoding(HTnode **HT, char ***HC,int n){
*HC = (HTnode *) malloc((n+1) * sizeof(char *));
char *cd = (char *)malloc(n*sizeof(char)); //存放结点哈夫曼编码的字符串数组
cd[n-1] = '\0';//字符串结束符
for(int i=1; i<=n; i++){
//从叶子结点出发,得到的哈夫曼编码是逆序的,需要在字符串数组中逆序存放
int start = n-1;
//当前结点在数组中的位置
int c = i;
//当前结点的父结点在数组中的位置
int j = HT[i].parent;
// 一直寻找到根结点
while(j != 0){
// 如果该结点是父结点的左孩子则对应路径编码为0,否则为右孩子编码为1
if(HT[j].left == c)
cd[--start] = '0';
else
cd[--start] = '1';
//以父结点为孩子结点,继续朝树根的方向遍历
c = j;
j = HT[j].parent;
}
//跳出循环后,cd数组中从下标 start 开始,存放的就是该结点的哈夫曼编码
(*HC)[i] = (char *)malloc((n-start)*sizeof(char));
strcpy((*HC)[i], &cd[start]);
}
//使用malloc申请的cd动态数组需要手动释放
free(cd);
}
注:以上两段代码引用参考1
参考:
1.部分文字及源代码参考 哈夫曼树(最优数)—解学武
2. 部分图片来自于漫画:“哈夫曼编码” 是什么鬼?
3. 数据结构(C语言第二版,严蔚敏)