图源:文心一言
小白友好、代码可跑,但是不一定适合考研~~
第1版:查资料、画导图、画配图~
参考用书:王道考研《2024年 数据结构考研复习指导》
参考用书配套视频:5.5_1_哈夫曼树_哔哩哔哩_bilibili
特别感谢: Chat GPT老师、文心一言老师~
目录
思维导图
基本概念
⏲️哈夫曼树简介
构造举栗
⌨️代码实现
分段代码
P0:调用库文件
P1:定义结点与指针
P2:用于优先队列中的比较函数
P3:构造哈夫曼树
P4:打印哈夫曼树编码
P5:计算哈夫曼树的权值路径长度(WPL)
P6:main函数
完整代码
P0:完整代码
P1:执行结果
结语
备注:
哈夫曼树的起源:
哈夫曼树的发明者是一位美国数学家David A. Huffman,它可以通过给出现频率高的字符短编码,让编码效率更高。
哈夫曼树的用途:
我们平时发邮件或是在线看视频,要传输或存储大量的数据,这就要占用很多带宽或存储空间,有时候就会很麻烦。但有了哈夫曼树,我们就可以用最优的编码方式,让频率高的字符用短编码,频率低的字符用长编码,这样就可以在不影响数据的情况下,大大减小数据的体积,让数据传输更快更高效。
哈夫曼树的定义:
哈夫曼树是一种特殊的二叉树,它的每个结点都带有一个权值,并且它的路径长度最小{权值,就是每个数字在一份文件中出现的频率}~
带权路径的公式为 = 求和(结点的权值 x 路径长度)
如何理解路径最小,以下我们举栗说明~
例如在《天才枪手》中,我们需要利用时差向队友传递一串选择题答案,这个答案包含“7个a、5个b、2个c、4个d”;在通信方面,“a、b、c、d”这4个选择由“0”和“1”这两个数字编码加密组成~
那我们应该如何编码?首先绘制三棵小树,这三棵小树的结点均为“数据a(权值7)、数据b(权值5)、数据c(权值2)、数据d(权值4)”构成~
对应上图的哈夫曼树,对字符编码时,结点在左子树时编码+0,结点在左子树时编码+1,那么我们就可以很快得到每棵树字符的编码:
- 树a的编码:a(00)、b(01)、c(10)、d(11),根据权值公式计算的结果,传递选择题答案需要编码36个数字 {7个a,也就是7个00,传送所有的a加起来是14个数字,同理所有的b加起来是10个数字、所有的c加起来是4个数字、所有的d加起来是8个数字,传送整个答案就是14+10+4+8=36个数字};
- 树b的编码:a(010)、b(011)、c(1)、d(00),根据权值公式计算的结果,传递选择题答案需要编码46个数字;
- 树c的编码:a(0)、b(10)、c(110)、d(111),根据权值公式计算的结果,传递选择题答案需要编码35个数字;
在这里,传输的字符数量也就是树的权值{或者频率}~
- 树a的WPL:(7+5+2+4)x2=36
- 树b的WPL:2x1+4x2+(7+5)x3=46
- 树c的WPL:7x1+5x2+(2+4)x3=35
树c的编码相比树b的编码,大概节省了24%的传输量,这就体现出选择树c编码的好处了~
如果不幸选择树b编码的话,会把答案出现频率最高a、b的答案排最长的3字编码(010、011),这在信息传输中显然是非常不合理的~
那如何构成树c呢?
核心思想:让频率低的结点待在底层,频率高的结点在高层,以减少文本段整体编码长度~
- 统计字符的出现频率{{以下案例我们还是以传送选择题答案为例,选项a的频率为7,选项b的频率为5、选项c的频率为2、选项d的频率为4考虑}~
- 选取其中频率最小的两个点{选项c的频率为2、选项d的频率为4}组成一棵小树,其父结点c+d的频率为6{并且这个阶};
- 重复上面的操作,再次选取其中频率最小的两个点{选项b的频率为5、选项c+d的频率为6}组成一棵小树,其父结点b+c+d的频率为11;
- 重复上面的操作,再次选取其中频率最小的两个点{选项a的频率为7、选项b+c+d的频率为11}组成一棵小树,其父结点a+b+c+d的频率为18;
- 此时没有剩余的结点,退出循环,树C就构成了~
- 通过遍历哈夫曼树,为每个字符生成对应的编码。从根节点开始,左子树路径表示编码位"0",右子树路径表示编码位"1",直到达到叶节点。通过遍历路径,我们可以为每个字符生成唯一的哈夫曼编码。
- 以图中的小树为例,列出哈夫曼树的构造代码~
图源:文心一言
#include
#include
#include
struct HuffmanNode {
char data; //定义字符
int frequency; //定义频率
HuffmanNode *left, *right; //定义左指针、右指针
HuffmanNode(char data, int frequency) { //初始化
this->data = data;
this->frequency = frequency;
left = right = nullptr;
}
};
创建结点的步骤在调整<优先队列>时重复出现,因此使用函数封装~
思路:比较指针指向的两个函数,权值高的结点优先度降低。
struct Compare {
bool operator()(HuffmanNode* a, HuffmanNode* b) { //接受两个HuffmanNode对象的指针作为参数,即HuffmanNode* a和HuffmanNode* b
return a->frequency > b->frequency; //判断a的频率是否大于b的频率,频率高的结点优先度更低
}
};
传入main函数中的数据动态数组data和频度动态数组frequency~
相比于普通的构造,这里采用了一个队列调整结点的次序:priority_queue
也就是说,如果采用了排序队列,我们实现代码的具体步骤就修改为以下的内容~
- 统计字符的出现频率{即各个字符的权值}。实际操作中,这可以通过扫描待编码的数据来实现,统计每个字符出现的次数~
- 将统计得到的每个字符及其对应的频率作为叶节点,构建一个优先队列~
- 从优先队列中选取频率最小的两个节点,创建一个新的节点作为它们的父节点,并将父节点插入到优先队列中。重复上述步骤,直到优先队列中只剩下一个节点,这个节点就是哈夫曼树的根节点~
- 通过遍历哈夫曼树,为每个字符生成对应的编码。从根节点开始,左子树路径表示编码位"0",右子树路径表示编码位"1",直到达到叶节点。通过遍历路径,我们可以为每个字符生成唯一的哈夫曼编码。
根据下面的代码,树的具体创建过程如下图:
HuffmanNode* buildHuffmanTree(const vector& data, const vector& frequency) {
priority_queue, Compare> pq; //声明了一个优先队列(priority_queue),其中存储的是HuffmanNode类型的对象指针。这个优先队列使用了一个比较函数(Compare)来定义元素的优先级
// 创建叶结点并将它们插入优先队列
for (int i = 0; i < data.size(); i++) {
pq.push(new HuffmanNode(data[i], frequency[i]));
}
// 构建哈夫曼树
while (pq.size() > 1) {
HuffmanNode* left = pq.top(); //队首元素记录并出列,即左子结点(left)
pq.pop();
HuffmanNode* right = pq.top(); //队首元素记录并出列,即右子结点(right)
pq.pop();
HuffmanNode* newNode = new HuffmanNode('$', left->frequency + right->frequency); //创建了一个新的HuffmanNode对象,它的频率是左子节点和右子节点的频率之和
newNode->left = left; //在树中链接新节点和左子结点
newNode->right = right; //在树中链接新节点和右子结点
pq.push(newNode); //将新的结点插回队列
}
return pq.top(); //返回根结点
}
传入树的根结点内存地址,编码由空值开始~
void printHuffmanCodes(HuffmanNode* root, string code) {
if (root == nullptr) {
return;
}
// 如果是叶结点,则打印字符和对应的编码
if (!root->left && !root->right) {
cout << root->data << " : " << code << endl;
}
// 递归打印左子树和右子树
printHuffmanCodes(root->left, code + "0");
printHuffmanCodes(root->right, code + "1");
}
传入树的根结点内存地址,树高由0开始(根结点那一行不算权值)~
先序遍历的内容可以看这里:[树:双亲、孩子、兄弟表示法][二叉树:先序、中序、后序遍历]
int calculateWPL(HuffmanNode* root, int depth) {
if (root == nullptr) {
return 0;
}
// 如果是叶结点,返回权值乘以深度
if (!root->left && !root->right) {
return root->frequency * depth;
}
// 递归计算左子树和右子树的WPL
int leftWPL = calculateWPL(root->left, depth + 1);
int rightWPL = calculateWPL(root->right, depth + 1);
return leftWPL + rightWPL;
}
main函数除了P0~P5的函数调用,就创建了频度与结点值,以及示意性地增加了结果输出~
int main() {
// 示例数据
vector data = {'A', 'B', 'C', 'D'};
vector frequency = {7, 5, 2, 4};
// 构建哈夫曼树
HuffmanNode* root = buildHuffmanTree(data, frequency);
// 打印哈夫曼树的编码
cout << "Huffman Codes:" << endl;
printHuffmanCodes(root, "");
// 计算并打印哈夫曼树的权值路径长度(WPL)
int wpl = calculateWPL(root, 0);
cout << "Weighted Path Length (WPL): " << wpl << endl;
return 0;
}
为了凑本文的字数,我这里贴一下整体的代码,删掉了细部注释~
#include
#include
#include
using namespace std;
// 哈夫曼树的结点定义
struct HuffmanNode {
char data;
int frequency;
HuffmanNode *left, *right;
HuffmanNode(char data, int frequency) {
this->data = data;
this->frequency = frequency;
left = right = nullptr;
}
};
// 用于优先队列中的比较函数
struct Compare {
bool operator()(HuffmanNode* a, HuffmanNode* b) {
return a->frequency > b->frequency;
}
};
// 生成哈夫曼树
HuffmanNode* buildHuffmanTree(const vector& data, const vector& frequency) {
priority_queue, Compare> pq;
for (int i = 0; i < data.size(); i++) {
pq.push(new HuffmanNode(data[i], frequency[i]));
}
while (pq.size() > 1) {
HuffmanNode* left = pq.top();
pq.pop();
HuffmanNode* right = pq.top();
pq.pop();
HuffmanNode* newNode = new HuffmanNode('$', left->frequency + right->frequency);
newNode->left = left;
newNode->right = right;
pq.push(newNode);
}
return pq.top();
}
// 打印哈夫曼树中的编码
void printHuffmanCodes(HuffmanNode* root, string code) {
if (root == nullptr) {
return;
}
if (!root->left && !root->right) {
cout << root->data << " : " << code << endl;
}
printHuffmanCodes(root->left, code + "0");
printHuffmanCodes(root->right, code + "1");
}
// 计算哈夫曼树的权值路径长度(WPL)
int calculateWPL(HuffmanNode* root, int depth) {
if (root == nullptr) {
return 0;
}
if (!root->left && !root->right) {
return root->frequency * depth;
}
int leftWPL = calculateWPL(root->left, depth + 1);
int rightWPL = calculateWPL(root->right, depth + 1);
return leftWPL + rightWPL;
}
int main() {
vector data = {'A', 'B', 'C', 'D'};
vector frequency = {7, 5, 2, 4};
HuffmanNode* root = buildHuffmanTree(data, frequency);
cout << "Huffman Codes:" << endl;
printHuffmanCodes(root, "");
int wpl = calculateWPL(root, 0);
cout << "Weighted Path Length (WPL): " << wpl << endl;
return 0;
}
运行结果如下图所示~
博文到此结束,写得模糊或者有误之处,欢迎小伙伴留言讨论与批评,督促博主优化内容,不限于以下内容~️️
博文若有帮助,欢迎小伙伴动动可爱的小手默默给个赞支持一下~