C语言-哈夫曼树、哈夫曼编码

  1. 哈夫曼树中的查找算法(Select)
  2. 哈夫曼树的构建(HuffmanTree)
  3. 哈夫曼编码的构建(HuffmanCoding)
  4. 打印哈夫曼树表(Print)
  5. 打印权值及其编码(Inputcode)

什么是哈夫曼树?

  1. 当有 n 个结点(都做叶子结点且都有各自的权值)构建一棵树时,如果构建的这棵树的带权路径长度(WPL)最小,称这棵树为“最优二叉树”,有时也叫“赫夫曼树”或者“哈夫曼树”(HuffmanTree)。
  2. 权值可以理解为访问频率,权值越大访问频率越高,访问次数越多;而带权路径长度=权值 x 该节点到树根的路径长度——WPL。
  • 总而言之,哈夫曼树所构建出来的树,会将最频繁被访问的结点放在最前面,不常访问的结点依次往后排,这样的话每次都能最快速的查找到最常被访问的结点。

如何构建哈夫曼树?

  1. 选取并合并:选取取值最小的两个结点合并成一棵树(根节点为权值之和)
  2. 删除并加入:从序列中删除上述选取的两个两个最小结点,加入新合并的树
  3. 重复:重复上述操作即可得到哈夫曼树
  • 不难发现在中有两个权值都为5的结点,此时选取任意一个都是可行的,所以哈夫曼树的构造是不唯一的,但是带权路径长度WPL是唯一的

代码实现哈夫曼树的构造

  • 首先我们要知道,我们的哈夫曼树使用的是结构体数组,每一个结点有4个域。
  • 拥有n个叶子结点的哈夫曼树,共有2n-1个结点
typedef struct
{
	int weight;//权值
	int parent,rchild,lchild;//存储地址(下标)
}HTnode;//每一个域存储的都是地址(数组下标)
  1. 我们需要建立起哈夫曼树组HuffTree[2n-1] (如下图,哈夫曼数组用HT[ ]代替)
    在这里插入图片描述
  2. 数组初始化:建立好有2n-1个单元格的哈夫曼数组后要对每个单元初始化。
    初始化内容包括:
    ① 把每个结点的地址域(parent,lchild,rchild)赋值为-1
    ② 给每个节点填上(输入)对应的权值。

  3. 完成前两步的初始化才开始构建哈夫曼树: ① 选出权值最小的两个结点(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函数,选出权值最小的两个结点?

  1. 可行的方法 排序:在权值数组w[ ]中,我们可以先对数组元素排序,然后取走第一小和第二小。 升级版选择排序:我写了一个哈夫曼树中的查找算法(Select),是选择排序的升级版。
  2. 需要注意的问题:① 只对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;//将第二小的坐标带入指针中传出
}

哈夫曼编码

  1. 什么是哈夫曼编码?
  • 哈夫曼编码是不等长编码。
  • 哈夫曼编码就是在哈夫曼树的基础上构建的,这种编码方式最大的优点就是用最少的字符包含最多的信息内容
  1. 哈夫曼编码的优越性在哪?
  • 哈夫曼编码是前缀编码(前缀永不重叠),且是最优前缀编码!因为哈夫曼编码是前缀永不重叠的前缀编码,保证了编码的可读取性可解码性,与此同时结合自身是不等长编码的特性,实现了与等长编码一样的编码解码功能,但编码更短,数据量更少。
    在这里插入图片描述
  • 看完上图,你就能明白什么叫用最少的字符包含最多的信息。

如何手动构建哈夫曼编码?

  • 手动构建哈夫曼编码非常简单,只需要构建出哈夫曼树,然后按"左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进入函数 
}
  1. 临时数组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申请到对应的存储空间。
  1. 哈夫曼编码数组:
  • 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;
}

执行结果


其他问题

  1. 在哈夫曼树组HuffTree[i]与哈夫曼编码数组Huffcode[i]中,均采用的是用指针指向用malloc所申请的连续的数组存储区域,也就是说HuffTree和Huffcode是指针,在此处均采用以指针做数组名的方式对数组元素进行访问。(了解“指针如何做数组名
  2. HuffTree、Huffcode变量是指针,故在主函数中应当定义成相应的指针类型,HuffTree是 HTnode *HuffTree;,而Huffcode指向的又是一个指针数组,所以Huffcode要定义成指向指针的指针char **Huffcode;二重指针!
  3. 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语言第二版,严蔚敏)

  • 19
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Attract1206

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值