本系列文章为浙江大学陈越、何钦铭数据结构学习笔记,前面的系列文章链接如下:
数据结构基础: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
,字符a
、x
、u
和z
的频率分别为4
、2
、1
和1
。我们可以将符号编码为{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}
不正确,因为aaaxuaxz
和aazuaxax
都可以从编码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=0∑N−1strlen(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;
}
运行,输入题目中的输入样例,结果如下: