本篇文章的读者是初学者,旨在帮助他们了解数据结构中的哈夫曼编码和哈夫曼树,同时也检验自己的水平,从而共同提高代码水平和效率,共勉之。
本文为读者详细介绍了哈夫曼编码的应用和哈夫曼树的实现。
哈夫曼编码
哈夫曼编码(Huffman Code)是一种变长编码,用于数据压缩。它是由David A. Huffman于1952年提出的,通过对不同符号赋予不同的变长编码来实现对数据的高效压缩。
基本思想是根据符号出现的频率,为频率较高的符号分配较短的编码,而为频率较低的符号分配较长的编码。这样做的目的是使得出现频率高的符号用较短的编码表示,从而减少整体编码长度,实现数据压
例子:哈夫曼编码与二进制编码在文本数据中的对比
考虑以下文本数据:“ABRACADABRA”
1. 二进制编码:
使用ASCII编码,每个字符都用8位二进制表示。
- A: 01000001
- B: 01000010
- R: 01010010
- C: 01000011
- D: 01000100
整个字符串的二进制表示为:
0100000101000010010100100100000101000010010100100101001001000100
2. 哈夫曼编码:
首先,统计字符频率:
- A: 5
- B: 2
- R: 2
- C: 1
- D: 1
构建哈夫曼树:(下面有过程)
(11)
/ \
(7) (4)
/ \ / \
A (2) R B
/ \
C D
生成哈夫曼编码:(从根节点出发向左为’0’,向右为’1’)
- A: 00
- B: 11
- R: 10
- C: 010
- D: 011
整个字符串的哈夫曼编码表示为:
001110000100011001110000100011
对比与分析:
- 二进制编码需要固定的8位表示每个字符,而哈夫曼编码根据字符频率动态分配不同长度的编码,因此哈夫曼编码在这个例子中显著减少了总体的比特数。
- 哈夫曼编码中字符’A’,由于频率最高,获得最短的编码,这在二进制编码中是无法实现的。
- 对于频率较低的字符,哈夫曼编码引入了一定的额外开销,例如字符’C’和’D’。然而,总体上由于高频字符的短编码,哈夫曼编码在这个例子中实现了更好的压缩效果。
哈夫曼树(Huffman Tree)
哈夫曼树是一种带权路径长度最短的二叉树,常用于数据压缩。它的构建基于一组权值,每个权值表示一个字符在数据中出现的频率。构建哈夫曼树的目标是通过赋予权值较小的字符以较短的编码,而给予权值较大的字符以较长的编码,从而达到压缩数据的目的。
哈夫曼树的构建过程包括以下步骤:
- 初始化:给定一组权值,每个权值对应一个字符。初始时,每个字符都被视为一个只包含一个结点的树。
- 选择:在剩余的树中选择两棵树,它们的权值最小。这两棵树将成为哈夫曼树的左右子树。
- 合并:将选中的两棵树合并为一棵新树,新树的根结点的权值为两棵树根结点的权值之和。
- 重复:重复以上步骤,直到只剩下一棵树为止。这棵树就是哈夫曼树。
在哈夫曼树中,左子树的编码为0,右子树的编码为1。通过从根结点到每个叶子结点的路径,即可得到每个字符的哈夫曼编码。
哈夫曼编码的特点是没有编码是另一个编码的前缀,这被称为“前缀码”。这使得在解码时不会出现歧义。
在构建完成哈夫曼树后,可以通过遍历树的路径,从根结点到每个叶子结点,得到每个字符的哈夫曼编码。这些编码可以用于数据的压缩和解压。
2. 构建哈夫曼树
构建哈夫曼树的过程包括选择两个权值最小的结点,创建一个新的父节点,将这两个结点作为新节点的左右子节点,并将新节点的权值设置为左右子节点权值之和。这个过程一直重复,直到只剩下一个节点,这个节点就是哈夫曼树的根节点。
在构建过程中,可以使用优先队列(最小堆)来保持权值最小的两个结点始终在队列的前面。这确保了在每一步中我们都选择了最小的两个权值。
示例
考虑字符串 “ABRACADABRA”,我们首先计算字符的频率:
A: 5
B: 2
R: 2
C: 1
D: 1
然后按照哈夫曼树的构建规则,构建哈夫曼树:
-
选择最小的两个权值,创建一个新节点,更新权值:
S2 A5 B2 R2 / \ C1 D1 S2 A5 B2 R2
-
再次选择最小的两个权值,创建一个新节点,更新权值:
P4 A5 S2 / \ R2 B2
-
继续这个过程,直到只剩下一个节点:
E7 P4 / \ A5 S2 / \ C1 D1
11
/ \
E7 P4
/ \ / \
A5 S2 R2 B2
/ \
C1 D1
可能存在差异性
这就是构建的哈夫曼树。通过遍历从根到每个叶子节点的路径,我们可以为每个字符构建哈夫曼编码:
这样,原始数据就可以通过这些短编码进行高效的压缩。
哈夫曼树代码讲解:
-
结构体定义:
typedef struct { int weight; // 结点的权值 int parent, lchild, rchild; // 结点的双亲、左孩子、右孩子的下标 } HTNode, *HuffmanTree;
这定义了一个结构体
HTNode
用于表示哈夫曼树中的节点。它包含了权值、双亲、左孩子和右孩子的字段。typedef
用于为指向HTNode
的指针创建一个别名HuffmanTree
,使得使用动态数组更加方便。 -
Select 函数:
void Select(HuffmanTree HT, int k, int& s1, int& s2) { unsigned int minWeight = 10000; unsigned int secondMinWeight = 10000; for (int i = 1; i <= k; i++) { if (!HT[i].parent) { if (HT[i].weight < minWeight) { secondMinWeight = minWeight; minWeight = HT[i].weight; s2 = s1; // 将当前最小值更新为次最小值 s1 = i; } else if (HT[i].weight < secondMinWeight) { secondMinWeight = HT[i].weight; s2 = i; } } } }
该函数在前
k
个结点中选择两个双亲域为0且权值最小的结点,并通过引用参数s1
和s2
返回它们的下标。它通过遍历哈夫曼树的结点,根据权值找到最小和次最小的两个结点。 -
CreateHuffmanTree 函数:
void CreateHuffmanTree(HuffmanTree& HT, int n) { // 构造哈夫曼树 if (n <= 1) return; int m = 2 * n - 1; HT = new HTNode[m + 1]; // 初始化结点数组 for (int i = 1; i <= m; i++) { HT[i].parent = 0; HT[i].lchild = 0; HT[i].rchild = 0; HT[i].weight = 0; } // 输入叶子结点的权值 for (int i = 1; i <= n; i++) { cin >> HT[i].weight; } // 创建哈夫曼树 for (int i = n + 1; i <= m; i++) { int s1 = 0, s2 = 0; Select(HT, i - 1, s1, s2); HT[s1].parent = i; HT[s2].parent = i; HT[i].lchild = s1; HT[i].rchild = s2; HT[i].weight = HT[s1].weight + HT[s2].weight; } }
该函数用于构建哈夫曼树。首先,动态分配一个包含
m+1
个结点的数组,然后初始化叶子结点的权值。接着,通过选择、删除、合并的过程构建哈夫曼树。 -
PrintHuffmanTree 函数:
void PrintHuffmanTree(HuffmanTree HT, int m) { cout << std::left << setw(10) << "i" << setw(10) << "weight" << setw(10) << "parent" << setw(10) << "lchild" << setw(10) << "rchild" << endl; for (int i = 1; i <= m; i++) { cout << setw(12) << i << setw(11) << HT[i].weight << setw(10) << HT[i].parent << setw(10) << HT[i].lchild << setw(10) << HT[i].rchild << endl; } }
该函数用于打印哈夫曼树的节点信息,包括节点序号、权值、双亲、左孩子和右孩子的下标。
-
PrintHuffmanCodes 函数:
void PrintHuffmanCodes(HuffmanTree HT, int n) { string code; for (int i = 1; i <= n; i++) { int current = i; code = ""; // 从叶子节点向上遍历树以找到编码 while (HT[current].parent != 0) { if (HT[HT[current].parent].lchild == current) { code = '0' + code; // '0' 代表左孩子 } else { code = '1' + code; // '1' 代表右孩子 } current = HT[current].parent; } cout << "结点 " << i << " 的编码: " << code << endl; } }
该函数用于打印哈夫曼树中每个叶子节点的编码。它从叶子节点向上遍历树以找到对应的编码,并输出结果。
-
主函数 main:
int main() { HuffmanTree HT = new HTNode; int n = 0; cin >> n; int m = 2 * n - 1; CreateHuffmanTree(HT, n); PrintHuffmanTree(HT, m); cout << "哈夫曼编码:" << endl; PrintHuffmanCodes(HT, n); }
主函数首先创建了一个
HuffmanTree
类型的变量HT
,然后输入结点个数n
。接着,调用CreateHuffmanTree
构建哈夫曼树,然后使用PrintHuffmanTree
打印哈夫曼树的结点信息,最后使用PrintHuffmanCodes
打印每个叶子节点的哈夫曼编码。完整代码:
#include<iostream>
#include<iomanip>
using namespace std;
typedef struct {
int weight;//结点的权值
int parent, lchild, rchild;//结点的双亲、左孩子、右孩子的下标
}HTNode,*HuffmanTree;//动态分配数组存储哈夫曼树
/*----------选择两个权值最小的结点-----------*/
void Select(HuffmanTree HT, int k, int& s1, int& s2) {
unsigned int minWeight = 10000;
unsigned int secondMinWeight = 10000;
for (int i = 1; i <= k; i++) {
if (!HT[i].parent) {
if (HT[i].weight < minWeight) {
secondMinWeight = minWeight;
minWeight = HT[i].weight;
s2 = s1; // 将当前最小值更新为次最小值
s1 = i;
} else if (HT[i].weight < secondMinWeight) {
secondMinWeight = HT[i].weight;
s2 = i;
}
}
}
}
/*-------构造哈夫曼树-------*/
void CreateHuffmanTree(HuffmanTree& HT, int n) {
//构造哈夫曼树
if (n <= 1)
return;
int m = 2 * n - 1;//一棵有n个叶子节点的哈夫曼树共有2*n-1个结点
HT = new HTNode[m + 1];//0号单元未用,所以需要动态分配m+1个单元,HT[m]表示根结点
for (int i = 1; i <= m; i++)
{//将1-m号单元中的双亲、左孩子、右孩子的下标都初始化为0
HT[i].parent = 0;
HT[i].lchild = 0;
HT[i].rchild = 0;
HT[i].weight = 0;
}
for (int i = 1; i <= n; i++)
{//输入前n个单元中叶子结点的权值
cin >> HT[i].weight;
}
/*--------初始化工作结束,下面开始创建哈夫曼树------*/
for (int i = n+1; i <= m; i++)
{//通过n-1次的选择、删除、合并来创建哈夫曼树
int s1=0, s2=0;
Select(HT, i - 1, s1, s2);//在HT[k](1<=k<=i-1)中选择两个其双亲域为0且权值最小的结点,并返回它们在HT中的序号s1和s2
HT[s1].parent = i; HT[s2].parent = i;
//得到新结点i,从森林中删除s1,s2,将s1和s2的双亲域由0改为i
HT[i].lchild = s1; HT[i].rchild = s2;//s1,s2分别作为i的左右孩子
HT[i].weight = HT[s1].weight + HT[s2].weight;//i的权值为左右孩子权值之和
}
}
/*----------打印哈夫曼树---------*/
void PrintHuffmanTree(HuffmanTree HT,int m) {
cout <<std::left << setw(10) << "i" << setw(10) << "weight" << setw(10) << "parent" << setw(10) << "lchild" << setw(10) << "rchild" << endl;
for (int i = 1; i <= m; i++)
{
cout << setw(12) << i << setw(11) << HT[i].weight << setw(10) << HT[i].parent << setw(10) << HT[i].lchild << setw(10) << HT[i].rchild << endl;
}
}
void PrintHuffmanCodes(HuffmanTree HT, int n) {
string code;
for (int i = 1; i <= n; i++) {
int current = i;
code = "";
// 从叶子节点向上遍历树以找到编码
while (HT[current].parent != 0) {
if (HT[HT[current].parent].lchild == current) {
code = '0' + code; // '0' 代表左孩子
} else {
code = '1' + code; // '1' 代表右孩子
}
current = HT[current].parent;
}
cout << "结点 " << i << " 的编码: " << code << endl;
}
}
/*--------主函数--------*/
int main() {
HuffmanTree HT = new HTNode;//定义一个哈夫曼树变量
int n=0;
cin >> n;//输入结点个数
int m = 2 * n - 1;//m为哈夫曼树的所有结点的个数
CreateHuffmanTree(HT, n);//构建一个哈夫曼树
PrintHuffmanTree(HT, m);//输出哈夫曼树
cout << "哈夫曼编码:" << endl;
PrintHuffmanCodes(HT, n);
}