【数据结构基础整理】树--09:赫夫曼树

0x01.关于赫夫曼树

给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

0x02.相关概念

  • 路径:从树中一个结点到另一个结点之间的分支构成两个结点之间的路径。
  • 路径长度:路径上的分支称为路径长度
  • 结点的权:给每个结点赋予的具有特殊含义的值称为结点的权。
  • 结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积
  • 树的带权路径长度:树中所有叶子结点的带权路径长度之和,记为WPL。
  • 赫夫曼树:带权路径最短二叉树。(也称为哈夫曼树,最优二叉树)
  • 赫夫曼编码:哈夫曼编码就是在哈夫曼树的基础上构建的,这种编码方式最大的优点就是用最少的字符包含最多的信息内容。一般地,设需要编码的字符集为{ d_{1},d_{2},...,d_{n} },各个字符在电文中出现的次数或频率的集合为{ w_{1},w_{2},...,w_{n} },以 d_{1},d_{2},...,d_{n} 作为叶子结点,以 w_{1},w_{2},...,w_{n} 作为相应叶子结点的权值来构成一棵赫夫曼树。规定赫夫曼树的左分支代表0,右分支代表1,则从根结点到叶子结点所经过的路径分支组成的0和1的序列为该结点对应字符的编码,也就是赫夫曼编码。
  • 文本中字符出现的次数越多,在哈夫曼树中的体现就是越接近树根。编码的长度越短。

0x03.赫夫曼树的结构

赫夫曼树没有使用真正的树结构,而是用数组来控制相应的结构,每一个赫夫曼结点应该包含下列信息。其中parent,left,right分别是父结点,左右孩子的下标。weight是结点的权值。

typedef struct
{
	int weight;
	int parent, left, right;
}HTNode, * HuffmanTree;

0x04.赫夫曼树的实现思路

  1. 在 n 个权值中选出两个最小的权值,对应的两个结点组成一个新的二叉树,且新二叉树的根结点的权值为左右孩子权值的和。
  2. 原有的 n 个权值中删除那两个最小的权值,同时将新的权值加入到 n–2 个权值的行列中,以此类推;
  3. 重复 1 和 2 ,直到所以的结点构建成了一棵二叉树为止,这棵树就是哈夫曼树。

0x05.赫夫曼树的实现代码

查找最小的两个结点的算法:

//筛选赫夫曼树中未构建树且权值最小的两个结点
//end是赫夫曼树的终点下标,s1,s2分别是最小的两个结点的下标
void Select(HuffmanTree HT, int end, int* s1, int* s2)
{
	int min1, min2;
	int i = 1;
	while (HT[i].parent != 0 && i <= end)//寻找一个最近的,还没有构建树的结点
	{
		i++;
	}
	min1 = HT[i].weight;
	*s1 = i;
	i++;
	while (HT[i].parent != 0 && i <= end)//再寻找一个最近的,还没有构建树的结点
	{
		i++;
	}
	if (HT[i].weight < min1)//比较两个结点的权值
	{
		min2 = min1;
		*s2 = *s1;
		min1 = HT[i].weight;
		*s1 = i;
	}
	else
	{
		min2 = HT[i].weight;
		*s2 = i;
	}
	for (int j = i + 1; j <= end; j++)//与剩下的所有的还未创建树的结点进行比较
	{
		if (HT[j].parent != 0)//不予考虑
		{
			continue;
		}
		if (HT[j].weight < min1)//如果比最小的还小,替换最小的,原来最小的变成min2
		{
			min2 = min1;
			min1 = HT[j].weight;
			*s2 = *s1;
			*s1 = j;
		}
		if (HT[j].weight >= min1 && HT[j].weight < min2)//如果位于二者之间
		{
			min2 = HT[j].weight;
			*s2 = j;
		}
	}
}

创建赫夫曼树代码:

//创建赫夫曼树
//w为存储结点权值的数组,n为结点个数
void CreateHuffmanTree(HuffmanTree* HT, int* w, int n)
{
	if (n <= 1) return;//一个结点,无意义
	int m = 2 * n - 1;//n为叶结点,那么总结点为2*n-1
	*HT = (HuffmanTree)malloc((m + 1)*sizeof(HTNode));//0号下标不使用,下标从1开始
	HuffmanTree p = *HT;
	for (int i = 1; i <= n; i++)//初始化所有叶结点
	{
		(p + i)->weight = *(w + i - 1);//给结点赋权值
		(p + i)->left = 0;
		(p + i)->right = 0;
		(p + i)->parent = 0;
	}
	for (int i = n + 1; i <= m; i++)//构建哈夫曼树
	{
		int s1, s2;
		Select(*HT, i - 1, &s1, &s2);//选出最小的两个结点
		(*HT)[s1].parent = i;
		(*HT)[s2].parent = i;
		(*HT)[i].left = s1;
		(*HT)[i].right = s1;
		(*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight;
	}
}

0x06.赫夫曼编码实现

实现思路:

  1. 从叶子结点一直找到根结点,逆向记录途中经过的标记。
  2. 从根结点出发,一直到叶子结点,记录途中经过的标记。

赫夫曼编码存储结构:

typedef char ** HuffmanCode;

实现代码(方法1):

//HC为存储结点哈夫曼编码的二维动态数组,n为结点的个数
void  HuffmanCoding(HuffmanTree HT, HuffmanCode* HC, int n)
{
	*HC = (HuffmanCode)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)
		{
			if (HT[j].left == c) // 如果该结点是父结点的左孩子则对应路径编码为0,否则为右孩子编码为1
			{
				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]);
	}
	free(cd);
}

实现代码(方法2):

 

//从根结点到叶子结点
void HuffmanCoding(HuffmanTree HT, HuffmanCode* HC, int n)
{
	*HC = (HuffmanCode)malloc((n + 1) * sizeof(char*));
	char* cd = (char*)malloc(n * sizeof(char)); //存放结点哈夫曼编码的字符串数组
	int m = 2 * n - 1;//总结点数
	int p = m;
	int cdlen = 0;
	//将各个结点的权重用于记录访问结点的次数,首先初始化为0
	for (int i = 1; i <= m; i++)
	{
		HT[i].weight = 0;
	}
	//一开始 p 初始化为 m,也就是从树根开始。一直到p为0
	while (p)
	{
		//如果当前结点一次没有访问
		if (HT[p].weight == 0)
		{
			HT[p].weight = 1;//重置访问次数为1
			//如果有左孩子,则访问左孩子,并且存储走过的标记为0
			if (HT[p].left != 0)
			{
				p = HT[p].left;
				cd[cdlen++] = '0';
			}
			//当前结点没有左孩子,也没有右孩子,说明为叶子结点,直接记录哈夫曼编码
			else if (HT[p].right == 0)
			{
				(*HC)[p]= (char*)malloc((cdlen + 1) * sizeof(char));
				cd[cdlen] = '\0';
				strcpy((*HC)[p], cd);
			}
		}
		//如果weight为1,说明访问过一次,即是从其左孩子返回的
		else if (HT[p].weight == 1)
		{
			HT[p].weight = 2;//设置访问次数为2
			//如果有右孩子,遍历右孩子,记录标记值 1
			if (HT[p].right != 0)
			{
				p = HT[p].right;
				cd[cdlen++] = '1';
			}	
		}
		//如果访问次数为 2,说明左右孩子都遍历完了,返回父结点
		else
		{
			HT[p].weight = 0;
			p = HT[p].parent;
			--cdlen;
		}
	}
}

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ATFWUS

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

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

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

打赏作者

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

抵扣说明:

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

余额充值