数据结构基础:P7.3-图(二)--->树之习题选讲:Huffman Codes

本系列文章为浙江大学陈越、何钦铭数据结构学习笔记,前面的系列文章链接如下
数据结构基础:P1-基本概念
数据结构基础:P2.1-线性结构—>线性表
数据结构基础:P2.2-线性结构—>堆栈
数据结构基础:P2.3-线性结构—>队列
数据结构基础:P2.4-线性结构—>应用实例:多项式加法运算
数据结构基础:P2.5-线性结构—>应用实例:多项式乘法与加法运算-C实现
数据结构基础:P3.1-树(一)—>树与树的表示
数据结构基础:P3.2-树(一)—>二叉树及存储结构
数据结构基础:P3.3-树(一)—>二叉树的遍历
数据结构基础:P3.4-树(一)—>小白专场:树的同构-C语言实现
数据结构基础:P4.1-树(二)—>二叉搜索树
数据结构基础:P4.2-树(二)—>二叉平衡树
数据结构基础:P4.3-树(二)—>小白专场:是否同一棵二叉搜索树-C实现
数据结构基础:P4.4-树(二)—>线性结构之习题选讲:逆转链表
数据结构基础:P5.1-树(三)—>堆
数据结构基础:P5.2-树(三)—>哈夫曼树与哈夫曼编码
数据结构基础:P5.3-树(三)—>集合及运算
数据结构基础:P5.4-树(三)—>入门专场:堆中的路径
数据结构基础:P5.5-树(三)—>入门专场:File Transfer
数据结构基础:P6.1-图(一)—>什么是图
数据结构基础:P6.2-图(一)—>图的遍历
数据结构基础:P6.3-图(一)—>应用实例:拯救007
数据结构基础:P6.4-图(一)—>应用实例:六度空间
数据结构基础:P6.5-图(一)—>小白专场:如何建立图-C语言实现
数据结构基础:P7.1-图(二)—>树之习题选讲:Tree Traversals Again
数据结构基础:P7.2-图(二)—>树之习题选讲:Complete Binary Search Tree


一、题目描述

1953 年,David A. Huffman 发表了他的论文 “A Method for the Construction of Minimum-Redundancy Codes”,从而将他的名字印在了计算机科学史上。作为一个在期末考试给出霍夫曼编码问题的教授,我遇到了一个巨大的问题:霍夫曼编码不是唯一的。例如,给定一个字符串aaaxuaxz,字符axuz的频率分别为4211。我们可以将符号编码为{a=0,x=10,u=110,z=111}{a=0,x=11,u=100,z=101}或者{a=1,x=01,u=001,z=000},都能将字符串压缩成正确的14位。但是{a=0,x=01,u=011,z=001}不正确,因为aaaxuaxzaazuaxax都可以从编码00001011001001中解码出来。学生们正在提交各种代码,我需要一个计算机程序来帮助我确定哪些是正确的,哪些不是。

输入格式:
第一行输入一个代表字符数量的整数N(2≤N≤63),第二行输入这N个不同字符及对应频率,第三行输入学生的人数M(M≤1000),后面M×N行输入这N个学生的提交内容。

输出格式:
对于每个测试用例,如果学生的提交是正确的,则在每一行中打印Yes,如果不正确,则打印No。注意:最优解不一定是霍夫曼算法生成的。任何具有最佳码长的前缀码都被认为是正确的。

输入样例:
7
A 1 B 1 C 1 D 3 E 3 F 6 G 6
4
A 00000
B 00001
C 0001
D 001
E 01
F 10
G 11
A 01010
B 01011
C 0100
D 011
E 10
F 11
G 00
A 000
B 001
C 010
D 011
E 100
F 101
G 110
A 00000
B 00001
C 0001
D 001
E 00
F 10
G 11

输出样例:
Yes
Yes
No
No


二、思路

2.1 哈夫曼编码的问题

Huffman Codes最大的问题就在于编码是不唯一的,我们来看一个非常简单的例子:比如说我们有4个字符,它们的频率分别是1 1 2 2,则我可以构建出以下三棵树。这三棵树的编码总长度都是14,但是它们每个字符的编码却不一样,并且第三棵树还是一种等长编码。
在这里插入图片描述
根据上面我们可以得出结论:哈夫曼编码得到的结果一定是最优的,但是最优的编码不一定通过Huffman算法得到!比如上面的第三棵树是使用等长编码,如果将第三棵树叶子结点上的1和2位置互换,则最终结果也是最优的,并且也不是通过哈夫曼编码得到的。
在这里插入图片描述


2.2 算法流程

如果我们把这道题的意思理解为仅仅判断是不是Huffman Codes可能不太准确。事实上,只要满足最优编码的条件,就都是正确的。接下来我们看一下在写程序的时候,我们读进来一段编码要判断它的哪些特点:

①最优编码 —— 总长度(WPL)最小
②无歧义解码 —— 前缀码:数据仅存于叶子结点
③没有度为1的结点 —— 满足1、2则必然有3

最优编码长度:

首先我得知道最优编码长度到底是多少,即我先得有一个标准答案,我再把学生的答案往那个标准答案上面去套。要求出这个标准答案,我就必须要去建一棵哈夫曼树。有些同学想不建那棵哈夫曼树,他就想通过判断2和3来决定这个编码是不是对的。但是你要知道,同时满足2和3的树不一定是我们要的那个有最优编码的那棵树。具体案例如下:两棵树都是前缀码,且没有度为1的结点,但是第一棵树不是最优的。
在这里插入图片描述

根据输入建立哈夫曼树的伪代码如下:

MinHeap H = CreateHeap( N ); /* 创建一个空的、容量为N的最小堆 */
H = ReadData( N ); /* 将f[]读入H->Data[]中 */
HuffmanTree T = Huffman( H ); /* 建立Huffman树 */
int CodeLen = WPL( T, 0 ); //求出WPL,这个0代表当前结点深度,因为WPL=各个结点深度*频率的和

//一棵树的总的WPL就是他左子树的WPL加上他右子树的WPL
//因此可以使用递归的方法
int WPL( HuffmanTree T, int Depth ) 
{ 
	if ( !T->Left && !T->Right ) 
		return (Depth*T->Weight);
	else /* 否则T一定有2个孩子 */
		return (WPL(T->Left, Depth+1) + WPL(T->Right, Depth+1));
}

编码检查:
接下来我们要对每一位学生的提交进行检查,主要检查两件事:①编码的总长度是否正确、②在长度正确的前提下,我们根据每个学生的编码构建对应的树,在建树的过程中检查他的编码是否满足前缀码的要求。

长度检查
检查长度是否正确这件事情是比较简单的。我只要把每一个字符对应的编码读到一个code字符串里面,然后我求这个字符串的长度,然后用它字符串长度乘以它的这个频率,然后求和就得到了它的总长度。然后把这个总长度跟我上一步算出来的标准答案去比对一下。
L e n = ∑ i = 0 N − 1 s t r l e n ( c o d e [ i ] ) × f [ i ] Len = \sum\limits_{i = 0}^{N - 1} {strlen(code[i]) \times f[i]} Len=i=0N1strlen(code[i])×f[i]
这里头有一个细节需要注意,学生有可能是给错误的提交,那么code这个字符串你要定义到多长是合理的呢。换句话说,如果这个编码是正确的话,那么每一个字符他对应的编码的最大长度应该是多少呢?最变态的情况下,这棵哈夫曼树就会长成下面这个样子。在这种情况下,如果我们有N个结点的话,就会有N-1个中间结点,也就对应最长的编码的长度:N-1
在这里插入图片描述
前缀码检查
前缀码的检查过程如下:
①首先,我建立一个根结点,然后准备输入编码。
在这里插入图片描述
②读取编码的第1位,这里是个1,于是右边新增一个结点。
在这里插入图片描述
③继续往后读取,这里是个0,于是左边新增一个结点。
在这里插入图片描述
④继续往后读取,这里是个1,于是左边新增一个结点。
在这里插入图片描述
⑤继续往后读取,这里是个1,于是左边新增一个结点。
在这里插入图片描述
⑥往后看,发现没有了,于是将频率写入这个叶子结点
在这里插入图片描述
⑦按照以上规律继续读取下一个编码
在这里插入图片描述
⑧第三个编码是1001,明显不对,因为要经过100,而100不可能有孩子。
在这里插入图片描述
⑨第四个编码是101,明显不对,因为101已经存在且不是叶结点。
在这里插入图片描述


三、整体代码

整体代码如下:可以参考这篇博客,代码很详细:Huffman Codes

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAXSIZE 63

typedef struct TreeNode *HuffmanTree;
struct TreeNode {
	int weight;
	HuffmanTree Left, Right;
};

typedef struct HeapStruct *MinHeap;
struct HeapStruct {
	HuffmanTree *Elements;
	int Size;
	int Capacity;
};

MinHeap CreateHeap(int MaxSize)
{
	MinHeap H = (MinHeap)malloc(sizeof(struct HeapStruct));
	H->Elements = (HuffmanTree*)malloc((MaxSize + 1) * sizeof(HuffmanTree));
	H->Size = 0;
	H->Capacity = MaxSize;
	H->Elements[0] = (HuffmanTree)malloc(sizeof(struct TreeNode));
	H->Elements[0]->weight = -1;
	return H;
}

int IsFull(MinHeap H)
{
	return (H->Size == H->Capacity);
}

int IsEmpty(MinHeap H)
{
	return (H->Size == 0);
}

void Insert(MinHeap H, HuffmanTree X)
{
	int i;
	if (IsFull(H)) return;
	i = ++H->Size;
	for (; H->Elements[i / 2]->weight > X->weight; i /= 2)
		H->Elements[i] = H->Elements[i / 2];
	H->Elements[i] = X;
}

HuffmanTree DeleteMin(MinHeap H)
{
	int Parent, Child;
	HuffmanTree MinItem, X;
	if (IsEmpty(H)) return 0;

	MinItem = H->Elements[1];
	X = H->Elements[H->Size--];
	for (Parent = 1; Parent * 2 <= H->Size; Parent = Child) {
		Child = Parent * 2;
		if ((Child != H->Size) && (H->Elements[Child]->weight > H->Elements[Child + 1]->weight))
			Child++;
		if (X->weight <= H->Elements[Child]->weight) break;
		else
			H->Elements[Parent] = H->Elements[Child];
	}
	H->Elements[Parent] = X;
	return MinItem;
}

MinHeap BuildMinHeap(int *freq_arr, int n)
{
	int i;
	HuffmanTree data;
	MinHeap H;
	H = CreateHeap(n);

	for (i = 0; i < H->Capacity; ++i) {
		data = (HuffmanTree)malloc(sizeof(struct TreeNode));
		data->weight = freq_arr[i];
		data->Left = 0; data->Right = 0;
		Insert(H, data);
	}
	return H;
}

int HuffmanTreeWPL(int f[], MinHeap H)
{
	int i, wpl = 0;
	HuffmanTree T;

	for (i = 1; i < H->Capacity; ++i) {
		T = (HuffmanTree)malloc(sizeof(struct TreeNode));
		T->Left = DeleteMin(H);
		T->Right = DeleteMin(H);
		T->weight = T->Left->weight + T->Right->weight;
		Insert(H, T);
		wpl += T->weight;
	}
	T = DeleteMin(H);
	return wpl;
}

void init_codeArr(char *code)   // 初始化code数组每个元素为\0
{
	int i;
	for (i = 0; i < MAXSIZE && code[i] != '\0'; ++i)
		code[i] = '\0';
}

HuffmanTree createTreeNode()
{
	HuffmanTree HT;
	HT = (HuffmanTree)malloc(sizeof(struct TreeNode));
	HT->weight = 1; HT->Left = 0; HT->Right = 0;
	return HT;
}

// 这里使用struct TreeNode结构体,其中的weight用来表示是否为本次code新添加结点,若是则为1,否则为0
HuffmanTree RecoverHFTreeByCode(HuffmanTree HT, char *code, int *flag, int *counter)
{
	int i;  HuffmanTree node;
	if (!HT) {
		HT = createTreeNode();
		++(*counter);
	}
	for (i = 0, node = HT; code[i] != '\0'; ++i) {
		// 第一种情况:该结点不是新添加结点,并且没有左右孩子,即之前字符对应的子节点
		// 说明之前某字符编码是该编码的前缀码
		if (node->weight == 0 && !node->Left && !node->Right) {
			(*flag) = 0;
			break;
		}
		node->weight = 0;
		if (code[i] == '0') {   // 读到0向左孩子走一位
			if (!node->Left) {
				node->Left = createTreeNode();
				++(*counter);
			}
			node = node->Left;
		}
		else {
			if (!node->Right) {
				node->Right = createTreeNode();
				++(*counter);
			}
			node = node->Right;
		}
	}
	// 第二种情况:读完所有code后,该位置有孩子结点,说明该编码是之前某字符编码的前缀码
	if (node->Left || node->Right)
		(*flag) = 0;
	return HT;
}

void check_code(int *freq_arr, int n, int wpl)
{
	int i, sum_wpl, flag, counter; char c;
	HuffmanTree HT;
	char code[MAXSIZE] = "\0";
	sum_wpl = 0; flag = 1; counter = 0; HT = 0;
	for (i = 0; i < n; ++i) {
		init_codeArr(code);
		scanf("\n%c %s", &c, code);
		if (flag) { // 判断该次提交是否已经不正确了,如果还正确则继续处理
			sum_wpl += strlen(code) * freq_arr[i];
			HT = RecoverHFTreeByCode(HT, code, &flag, &counter);
		}
	}
	// 这里有三个判断条件
	// 1. 提交的总wpl与预设的完全相同
	// 2. 没有前缀码情况出现,此处用flag标识
	// 3. 没有度为1的结点,此处的counter表示生成的Huffman树结点数,如果正确应该等于2*n-1,n为叶节点个数,即所有字符数
	if (sum_wpl == wpl && flag && counter == 2 * n - 1)
		printf("Yes\n");
	else
		printf("No\n");

}

int main()
{
	int n, m, i, wpl;
	char c[MAXSIZE];
	int f[MAXSIZE];
	scanf("%d\n", &n);
	for (i = 0; i < n; i++)
	{
		getchar();
		scanf("%c %d", &c[i],&f[i]);
	}
	MinHeap H = BuildMinHeap(f, n);  // 根据读入数据建立小顶堆
	wpl = HuffmanTreeWPL(f, H);    // 根据读入字符及频率使用小顶堆建立Huffman树求得WPL值
	scanf("%d", &m);
	for (i = 0; i < m; ++i) {
		check_code(f, n, wpl);
	}

	return 0;
}

运行,输入题目中的输入样例,结果如下:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

知初与修一

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

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

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

打赏作者

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

抵扣说明:

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

余额充值