哈夫曼树与哈夫曼编码原理与代码

哈夫曼编码是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码,是一种很好的文件压缩得到编码形式。

举一个例子:哈夫曼编码根据不同的字母(汉字)在文章中出现的频率不同构建不等长的编码,给出现频率最高的字最短的编码,给出现频率最低的字最长的编码,这样可以有效的节省空间,并且所有较短的编码都不是较长的编码的前缀,避免了二义性的问题。

哈夫曼编码需要通过哈夫曼树来实现,而哈夫曼树中各个元素的位置则与不同元素出现的频率(权重)有关系,需要通过权重来为其安排合适的位置,我先介绍安排位置的思想和代码实现。
下面是哈夫曼树节点的结构

typedef struct{
	int weight;//权值
	int parents;//父母
	int LChild,RChild;//左右孩子
	int flag;//标志(后面有解释)
}HT;

假如现在又一组数组 ``int a[] = {15,10,2,1,4}` 代表了a b c d e 在文章中的出现频率,我们发现a的出现频率(权重)最高,所以对于a的编码一定是最短的,而d出现的频率最低,所以d编码最长。
为了能满足权重大的距离根节点位置近,权重小的距离根节点位置远,我们可以每次在数组中挑选出两个权重最小的节点,在产生一个新节点,新节点的权值是两个子节点之和,然后在数组中剔除两个子节点,加入新节点。之后再重复这个过程,数组a产生的哈夫曼树应该是这样的
哈夫曼树与哈夫曼编码原理与代码_第1张图片

我们可以发现,数组中所有的权值都在哈夫曼树的最末端,那么如何用代码实现呢
给出每次找出数组中最小值的方法

int GetMin(HT tree[],int n)//获取初始数组中的最小值,并且返回最小值元素的下标,在前1~n-1中寻找最小值 
{
	int min;
	int loc;
	for(int i = 1;i <= n;i++)//先选择第一个标志是0的作为临时最小值(标志的意思是有没有参与过比较)
	{
		if(tree[i].flag == 0)
		{
			min = tree[i].weight;
			loc = i;
			tree[i].flag = 1;//选定后,将其标志设为1 
			break;
		}
	 } 
	for(int i = 2;i <= n;i++)//遍历,寻找真正的标志为0的最小值 
	{
		if(min < tree[i].weight&&tree[i].flag == 0)
			loc = loc;
		else if(min >= tree[i].weight&&tree[i].flag == 0)//要替换最小值,且替换值之前没有被使用过,则
		//将上一个假最小值的标志还原 
		{
			min = tree[i].weight; 
			tree[loc].flag = 0;
			loc = i;
			tree[i].flag = 1;
		}
	}
	return loc;//返回最小值的位置 
 } 

从上面的图可以看出,哈夫曼树做需要的总结点数是数组中元素数量的两倍,如果数组的规模为n,哈夫曼树的节点规模为m = 2n,能用的节点数是m - 1,给出构造哈夫曼树的代码

void CreatHuffmanTree(int a[],HT tree[],int n)//创建哈夫曼树 
{
	int m = 2 * n - 1;
	for(int i = 0;i <= m;i++)//所有节点初始化 
	{
		tree[i].parents = 0;
		tree[i].LChild = 0;
		tree[i].RChild = 0;
		tree[i].flag = 0;
	}
	for(int i = 0;i < n;i++)//建立哈夫曼树的初始节点,1~n的哈夫曼树节点的权值就是数组中0~n-1对应的权值 
	{
		tree[i + 1].weight = a[i];
	}
	//最后一个节点tree[m]不用比较,因为最后一个节点是根节点,直接和处他以外的两个节点建立联系即可,
	for(int i = n;i < m;i++)
	{
		int s1 = GetMin(tree,i);//获取哈夫曼节点中最小的两个权值的节点位置 
		int s2 = GetMin(tree,i);
		tree[s1].parents = i + 1;//给两个最小值的节点找父母节点,就将当前的节点(第i + 1)个节点当场他们的父母节点 
		tree[s2].parents = i + 1;
		tree[i + 1].LChild = s1;//给当前父母节点规定儿子,两个最小直接点分别为当前节点的左右孩子 
		tree[i + 1].RChild = s2;
		tree[i + 1].weight = tree[s1].weight + tree[s2].weight;//更改父母节点的权值,是两个最小节点的权值之和 
	}
}

因为我们之前提到过,每次选择出来两个节点之后,要将两个节点删除,这里是怎么做到的呢,在寻找最小值的函数中,每次挑选出来最小值之后,要将flag变为1,在下一次选择的时候就要跳过,就相当于将其从数组中删除了。

这样我们就能获得一个正确的哈夫曼树,可以对他进行哈夫曼编码,我们规定,如果一个节点是他的父母的左孩子,他的编码值是0,如果是右孩子,他的编码值是1,比如“1”,是“3”的左孩子,“3”是“7”的左孩子,一直向上推,我们推出来”1“的编码是1000,

上面说过,哈夫曼编码中较短的编码一定不是较长的编码的前缀,原因就是如果编码1是编码2的前缀,那么编码2一定是在编码1的基础上再走了一步,这就说明编码1不是叶子结点的编码,而所有需要编码的字母的权重都在叶子节点上,与前面的有矛盾,所以得证。

下面是哈夫曼编码的代码实现

void HuffmanCode(HT tree[],char code[],int loc,int n)//code是保存编码结果的数组,loc是指对数组中第几个元素进行编码,元素下标从1开始(可以自己定义,我这里是从1开始的),n是code数组的规模
{
	code[n - 1] = '\n';//将数组的最后一为改为换行符
	int parents = tree[loc].parents;//获取要编码的节点的父母节点的位置
	for(int i = n - 2;i >= 0;i--)
	{
		if(tree[parents].parents == 0)//遇到头节点,跳出
		{
			if(tree[parents].LChild == loc)
				code[i] = '0';
			else if(tree[parents].RChild == loc)
			code[i] = '1';
			break;
		 }     
		else
		{
			if(tree[parents].LChild == loc)
				code[i] = '0';
			else if(tree[parents].RChild == loc)
				code[i] = '1';
			loc = parents;//再从父母节点开始,寻找他的父母节点,重复该过程
			parents = tree[loc].parents; 
		}
	 } 
	return ;
 } 

这样我们就可以获得任意的叶子结点的编码了,希望这篇博客对大家理解哈夫曼树有一些帮助。
顺便,我最近搭建了一个个人博客,会和csdn同步更新,博客链接在这里https://wenruo-shusheng.github.io/,欢迎大家来看看。

你可能感兴趣的:(数据结构,算法,哈夫曼树,哈夫曼;编码;译码;)