huffman树及huffman编码的算法实现

2017年12月29日 10:26 | 2960次浏览

Huffman Tree简介

    赫夫曼树(Huffman Tree),又称最优二叉树,是一类带权路径长度最短的树。假设有n个权值{w1,w2,...,wn},如果构造一棵有n个叶子节点的二叉树,而这n个叶子节点的权值是{w1,w2,...,wn},则所构造出的带权路径长度最小的二叉树就被称为赫夫曼树。


    这里补充下树的带权路径长度的概念。树的带权路径长度指树中所有叶子节点到根节点的路径长度与该叶子节点权值的乘积之和,如果在一棵二叉树中共有n个叶子节点,用Wi表示第i个叶子节点的权值,Li表示第i个也叶子节点到根节点的路径长度,则该二叉树的带权路径长度 WPL=W1*L1 + W2*L2 + ... Wn*Ln。


    根据节点的个数以及权值的不同,赫夫曼树的形状也各不相同,赫夫曼树具有如下特性:


对于同一组权值,所能得到的赫夫曼树不一定是唯一的。

赫夫曼树的左右子树可以互换,因为这并不影响树的带权路径长度。

带权值的节点都是叶子节点,不带权值的节点都是某棵子二叉树的根节点。

权值越大的节点越靠近赫夫曼树的根节点,权值越小的节点越远离赫夫曼树的根节点。

赫夫曼树中只有叶子节点和度为2的节点,没有度为1的节点。

一棵有n个叶子节点的赫夫曼树共有2n-1个节点。


Huffman Tree的构建

    赫夫曼树的构建步骤如下:

    1、将给定的n个权值看做n棵只有根节点(无左右孩子)的二叉树,组成一个集合HT,每棵树的权值为该节点的权值。

    2、从集合HT中选出2棵权值最小的二叉树,组成一棵新的二叉树,其权值为这2棵二叉树的权值之和。

    3、将步骤2中选出的2棵二叉树从集合HT中删去,同时将步骤2中新得到的二叉树加入到集合HT中。

    4、重复步骤2和步骤3,直到集合HT中只含一棵树,这棵树便是赫夫曼树。

假如给定如下5个权值:

 则按照以上步骤,可以构造出如下面左图所示的赫夫曼树,当然也可能构造出如下面右图所示的赫夫曼树,这并不是唯一的。

      -----


Huffman编码

    赫夫曼树的应用十分广泛,比如众所周知的在通信电文中的应用。在等传送电文时,我们希望电文的总长尽可能短,因此可以对每个字符设计长度不等的编码,让电文中出现较多的字符采用尽可能短的编码。为了保证在译码时不出现歧义,我们可以采取如下图所示的编码方式:

-------

即左分支编码为字符0,右分支编码为字符1,将从根节点到叶子节点的路径上分支字符组成的字符串作为叶子节点字符的编码,这便是赫夫曼编码。我们根据上面左图可以得到各叶子节点的赫夫曼编码如下:

    权值为5的也自己节点的赫夫曼编码为:11

    权值为4的也自己节点的赫夫曼编码为:10

    权值为3的也自己节点的赫夫曼编码为:00

    权值为2的也自己节点的赫夫曼编码为:011

    权值为1的也自己节点的赫夫曼编码为:010


    而对于上面右图,则可以得到各叶子节点的赫夫曼编码如下:

    权值为5的也自己节点的赫夫曼编码为:00

    权值为4的也自己节点的赫夫曼编码为:01

    权值为3的也自己节点的赫夫曼编码为:10

    权值为2的也自己节点的赫夫曼编码为:110

    权值为1的也自己节点的赫夫曼编码为:111


Huffman编码的C实现

    由于赫夫曼树中没有度为1的节点,则一棵具有n个叶子节点的的赫夫曼树共有2n-1个节点(最后一条特性),因此可以将这些节点存储在大小为2n-1的一维数组中。我们可以用以下数据结构来表示赫夫曼树和赫夫曼编码:

/*
赫夫曼树的存储结构,它也是一种二叉树结构,
这种存储结构既适合表示树,也适合表示森林。
*/
typedef struct Node
{
	int weight;                //权值
	int parent;                //父节点的序号,为-1的是根节点
	int lchild,rchild;         //左右孩子节点的序号,为-1的是叶子节点
}HTNode,*HuffmanTree;          //用来存储赫夫曼树中的所有节点
typedef char **HuffmanCode;    //用来存储每个叶子节点的赫夫曼编码

根据赫夫曼树的构建步骤,我们可以写出构建赫夫曼树的代码如下:

/*
根据给定的n个权值构造一棵赫夫曼树,wet中存放n个权值
*/
HuffmanTree create_HuffmanTree(int *wet,int n)
{
	//一棵有n个叶子节点的赫夫曼树共有2n-1个节点
	int total = 2*n-1;
	HuffmanTree HT = (HuffmanTree)malloc(total*sizeof(HTNode));
	if(!HT)
	{
		printf("HuffmanTree malloc faild!");
		exit(-1);
	}
	int i;

	//以下初始化序号全部用-1表示,
	//这样在编码函数中进行循环判断parent或lchild或rchild的序号时,
	//不会与HT数组中的任何一个下标混淆

	//HT[0],HT[1]...HT[n-1]中存放需要编码的n个叶子节点
	for(i=0;i<n;i++)
	{
		HT[i].parent = -1;
		HT[i].lchild = -1;
		HT[i].rchild = -1;
		HT[i].weight = *wet;
		wet++;
	}

	//HT[n],HT[n+1]...HT[2n-2]中存放的是中间构造出的每棵二叉树的根节点
	for(;i<total;i++)
	{
		HT[i].parent = -1;
		HT[i].lchild = -1;
		HT[i].rchild = -1;
		HT[i].weight = 0;
	}

	int min1,min2; //用来保存每一轮选出的两个weight最小且parent为0的节点
	//每一轮比较后选择出min1和min2构成一课二叉树,最后构成一棵赫夫曼树
	for(i=n;i<total;i++)
	{
		select_minium(HT,i,min1,min2);
		HT[min1].parent = i;
		HT[min2].parent = i;
		//这里左孩子和右孩子可以反过来,构成的也是一棵赫夫曼树,只是所得的编码不同
		HT[i].lchild = min1;
		HT[i].rchild = min2;
		HT[i].weight =HT[min1].weight + HT[min2].weight;
	}
	return HT;
}

上述代码中调用到了select_minium()函数,它表示从集合中选出两个最小的二叉树,代码如下:

/*
从HT数组的前k个元素中选出weight最小且parent为-1的两个,分别将其序号保存在min1和min2中
*/
void select_minium(HuffmanTree HT,int k,int &min1,int &min2)
{
	min1 = min(HT,k);
	min2 = min(HT,k);
}

这里调用到的min()函数代码如下:

/*
从HT数组的前k个元素中选出weight最小且parent为-1的元素,并将该元素的序号返回
*/
int min(HuffmanTree HT,int k)
{
	int i = 0;
	int min;        //用来存放weight最小且parent为-1的元素的序号
	int min_weight; //用来存放weight最小且parent为-1的元素的weight值

	//先将第一个parent为-1的元素的weight值赋给min_weight,留作以后比较用。
	//注意,这里不能按照一般的做法,先直接将HT[0].weight赋给min_weight,
	//因为如果HT[0].weight的值比较小,那么在第一次构造二叉树时就会被选走,
	//而后续的每一轮选择最小权值构造二叉树的比较还是先用HT[0].weight的值来进行判断,
	//这样又会再次将其选走,从而产生逻辑上的错误。
	while(HT[i].parent != -1)
		i++;
	min_weight = HT[i].weight;
	min = i;

	//选出weight最小且parent为-1的元素,并将其序号赋给min
	for(;i<k;i++)
	{
		if(HT[i].weight<min_weight && HT[i].parent==-1)
		{
			min_weight = HT[i].weight;
			min = i;
		}
	}

    //选出weight最小的元素后,将其parent置1,使得下一次比较时将其排除在外。
	HT[min].parent = 1; 

	return min;
}

构建了赫夫曼树,便可以进行赫夫曼编码了,要求赫夫曼编码,就需要遍历出从根节点到叶子节点的路径,下面给出两种遍历赫夫曼树求编码的方法。

1、采用从叶子节点到根节点逆向遍历求每个字符的赫夫曼编码,代码如下:

/*
从叶子节点到根节点逆向求赫夫曼树HT中n个叶子节点的赫夫曼编码,并保存在HC中
*/
void HuffmanCoding(HuffmanTree HT,HuffmanCode &HC,int n)
{
	//用来保存指向每个赫夫曼编码串的指针
	HC = (HuffmanCode)malloc(n*sizeof(char *));
	if(!HC)
	{
		printf("HuffmanCode malloc faild!");
		exit(-1);
	}

	//临时空间,用来保存每次求得的赫夫曼编码串
	//对于有n个叶子节点的赫夫曼树,各叶子节点的编码长度最长不超过n-1
	//外加一个'\0'结束符,因此分配的数组长度最长为n即可
	char *code = (char *)malloc(n*sizeof(char));
	if(!code)
	{
		printf("code malloc faild!");
		exit(-1);
	}

	code[n-1] = '\0';  //编码结束符,亦是字符数组的结束标志
	//求每个字符的赫夫曼编码
	int i;
	for(i=0;i<n;i++)
	{
		int current = i;           //定义当前访问的节点
		int father = HT[i].parent; //当前节点的父节点
		int start = n-1;           //每次编码的位置,初始为编码结束符的位置
		//从叶子节点遍历赫夫曼树直到根节点
		while(father != -1)
		{
			if(HT[father].lchild == current)   //如果是左孩子,则编码为0
				code[--start] = '0';    
			else                              //如果是右孩子,则编码为1       
				code[--start] = '1';
			current = father;
			father = HT[father].parent;
		}

		//为第i个字符的编码串分配存储空间
		HC[i] = (char *)malloc((n-start)*sizeof(char));
		if(!HC[i])
		{
			printf("HC[i] malloc faild!");
			exit(-1);
		}
		//将编码串从code复制到HC
		strcpy(HC[i],code+start);
	}

	free(code); //释放保存编码串的临时空间
}

我们以上面给出的5、4、3、2、1这五个权值为例,得到的编码结果如下:

这恰好符合上面两棵赫夫曼树中左边的那一棵树的赫夫曼编码,因此该程序构造出的赫夫曼树即为上图左边的那棵。

    该方法是按照5、4、3、2、1的顺序(也即是输入的字符顺序)来求每个字符的赫夫曼编码的,同时也是按照这个顺序打印到终端的。


    2、采用从根节点到叶子节点无栈非递归遍历赫夫曼树,求每个字符的赫夫曼编码,代码如下:

/*
从根节点到叶子节点无栈非递归遍历赫夫曼树HT,求其中n个叶子节点的赫夫曼编码,并保存在HC中
*/
void HuffmanCoding2(HuffmanTree HT,HuffmanCode &HC,int n)
{
	//用来保存指向每个赫夫曼编码串的指针
	HC = (HuffmanCode)malloc(n*sizeof(char *));
	if(!HC)
	{
		printf("HuffmanCode malloc faild!");
		exit(-1);
	}

	//临时空间,用来保存每次求得的赫夫曼编码串
	//对于有n个叶子节点的赫夫曼树,各叶子节点的编码长度最长不超过n-1
	//外加一个'\0'结束符,因此分配的数组长度最长为n即可
	char *code = (char *)malloc(n*sizeof(char));
	if(!code)
	{
		printf("code malloc faild!");
		exit(-1);
	}

	int cur = 2*n-2;    //当前遍历到的节点的序号,初始时为根节点序号
	int code_len = 0;   //定义编码的长度

	//构建好赫夫曼树后,把weight用来当做遍历树时每个节点的状态标志
	//weight=0表明当前节点的左右孩子都还没有被遍历
	//weight=1表示当前节点的左孩子已经被遍历过,右孩子尚未被遍历
	//weight=2表示当前节点的左右孩子均被遍历过
	int i;
	for(i=0;i<cur+1;i++)
	{
		HT[i].weight = 0;   
	}

	//从根节点开始遍历,最后回到根节点结束
	//当cur为根节点的parent时,退出循环
	while(cur != -1)
	{
		//左右孩子均未被遍历,先向左遍历
		if(HT[cur].weight == 0)   
		{	
			HT[cur].weight = 1;    //表明其左孩子已经被遍历过了
			if(HT[cur].lchild != -1)  
			{   //如果当前节点不是叶子节点,则记下编码,并继续向左遍历
				code[code_len++] = '0';
				cur = HT[cur].lchild;
			}
			else
			{   //如果当前节点是叶子节点,则终止编码,并将其保存起来
				code[code_len] = '\0';
				HC[cur] = (char *)malloc((code_len+1)*sizeof(char));
				if(!HC[cur])
				{
					printf("HC[cur] malloc faild!");
					exit(-1);
				}
				strcpy(HC[cur],code);  //复制编码串
			}
		}

		//左孩子已被遍历,开始向右遍历右孩子
		else if(HT[cur].weight == 1)   
		{
			HT[cur].weight = 2;   //表明其左右孩子均被遍历过了
			if(HT[cur].rchild != -1)
			{   //如果当前节点不是叶子节点,则记下编码,并继续向右遍历
				code[code_len++] = '1';
				cur = HT[cur].rchild;
			}
		}

		//左右孩子均已被遍历,退回到父节点,同时编码长度减1
		else
		{
			HT[cur].weight = 0;
			cur = HT[cur].parent;
			--code_len;
		}

	}
	free(code);
}

该方法与方法1不同,它是根据赫夫曼树的构造来求每个字符的编码的,程序构造的赫夫曼树如上图中的左图所示,那么该方法便是按照3、1、2、4、5的顺序来球每个字符的赫夫曼编码的,但是我们在main函数中将其按照输入的顺序(5、4、3、2、1)打印到了终端。


完整代码下载

    完整的代码下载地址

    第一版:只含第一种编码方法:

    http://download.csdn.net/detail/mmc_maodun/6923741

    第二版:含有两种编码方法,并对部分代码做了些改进:

    http://download.csdn.net/detail/mmc_maodun/6925275



小说《我是全球混乱的源头》

感觉本站内容不错,读后有收获?小额赞助,鼓励网站分享出更好的教程