数据结构05:树与二叉树[C++][哈夫曼树HuffmanTree]

数据结构05:树与二叉树[C++][哈夫曼树HuffmanTree]_第1张图片

图源:文心一言

小白友好、代码可跑,但是不一定适合考研~~

第1版:查资料、画导图、画配图~

参考用书:王道考研《2024年 数据结构考研复习指导》

参考用书配套视频:5.5_1_哈夫曼树_哔哩哔哩_bilibili

特别感谢: Chat GPT老师、文心一言老师~


目录

目录

思维导图 

基本概念

⏲️哈夫曼树简介

构造举栗

⌨️代码实现

分段代码

 P0:调用库文件

 P1:定义结点与指针

 P2:用于优先队列中的比较函数

 P3:构造哈夫曼树

 P4:打印哈夫曼树编码

 P5:计算哈夫曼树的权值路径长度(WPL)

 P6:main函数

完整代码

 P0:完整代码

 P1:执行结果

结语


思维导图 

数据结构05:树与二叉树[C++][哈夫曼树HuffmanTree]_第2张图片

备注:

  • 本篇仅涉及到哈夫曼树HuffmanTree的代码;
  • 思维导图为整理王道教材第5章 树与二叉树的所有内容,其余学习笔记在以下博客~
    • [树:双亲、孩子、兄弟表示法][二叉树:先序、中序、后序遍历]
    • 数据结构05:树与二叉树[C++][线索二叉树:先序、中序、后序]
    • 数据结构05:树与二叉树[C++][并查集]

基本概念

⏲️哈夫曼树简介

哈夫曼树的起源:

哈夫曼树的发明者是一位美国数学家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)”构成~

数据结构05:树与二叉树[C++][哈夫曼树HuffmanTree]_第3张图片

对应上图的哈夫曼树,对字符编码时,结点在左子树时编码+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",直到达到叶节点。通过遍历路径,我们可以为每个字符生成唯一的哈夫曼编码
  • 以图中的小树为例,列出哈夫曼树的构造代码~

数据结构05:树与二叉树[C++][哈夫曼树HuffmanTree]_第4张图片

图源:文心一言

⌨️代码实现

分段代码

 P0:调用库文件

  • 输入输出流文件iostream{本代码用于输入与输出};
  • 动态数组的向量文件vector{本代码用于比较队列中结点的大小};
  • 队列函数文件queue{本代码用于创建哈夫曼树}~
#include 
#include 
#include 

 P1:定义结点与指针

struct HuffmanNode {
    char data;       //定义字符
    int frequency;   //定义频率
    HuffmanNode *left, *right;  //定义左指针、右指针

    HuffmanNode(char data, int frequency) { //初始化
        this->data = data;
        this->frequency = frequency;
        left = right = nullptr;
    }
};

 P2:用于优先队列中的比较函数

创建结点的步骤在调整<优先队列>时重复出现,因此使用函数封装~

思路:比较指针指向的两个函数,权值高的结点优先度降低。

struct Compare {
    bool operator()(HuffmanNode* a, HuffmanNode* b) {   //接受两个HuffmanNode对象的指针作为参数,即HuffmanNode* a和HuffmanNode* b
        return a->frequency > b->frequency;              //判断a的频率是否大于b的频率,频率高的结点优先度更低
    }
};

 P3:构造哈夫曼树

传入main函数中的数据动态数组data和频度动态数组frequency~

相比于普通的构造,这里采用了一个队列调整结点的次序:priority_queue, Compare> pq; 它会将插入队列的结点自动调用刚才所讲的compare函数,将队列里的数据按照从低到高的顺序排列~

也就是说,如果采用了排序队列,我们实现代码的具体步骤就修改为以下的内容~

  • 统计字符的出现频率{即各个字符的权值}。实际操作中,这可以通过扫描待编码的数据来实现,统计每个字符出现的次数~
  • 将统计得到的每个字符及其对应的频率作为叶节点,构建一个优先队列~
  • 从优先队列中选取频率最小的两个节点,创建一个新的节点作为它们的父节点,并将父节点插入到优先队列中。重复上述步骤,直到优先队列中只剩下一个节点,这个节点就是哈夫曼树的根节点~
  • 通过遍历哈夫曼树,为每个字符生成对应的编码。从根节点开始,左子树路径表示编码位"0",右子树路径表示编码位"1",直到达到叶节点。通过遍历路径,我们可以为每个字符生成唯一的哈夫曼编码

 根据下面的代码,树的具体创建过程如下图:

数据结构05:树与二叉树[C++][哈夫曼树HuffmanTree]_第5张图片

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();    //返回根结点
}

 P4:打印哈夫曼树编码

传入树的根结点内存地址,编码由空值开始~

  • 先序遍历树中的结点{先遍历根结点,再遍历左子树、再遍历右子树,是一种不重不漏遍历树的方式},以左子树+0,右子树+1的方式遍历树中的结点~
  • 若遇到叶子结点则打印编码~
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");
}

 P5:计算哈夫曼树的权值路径长度(WPL)

传入树的根结点内存地址,树高由0开始(根结点那一行不算权值)~

  • 先序遍历树中的结点{先遍历根结点,再遍历左子树、再遍历右子树,是一种不重不漏遍历树的方式}~
  • 若遇到叶子结点则计算权值(频度x权值),并返回加和~

先序遍历的内容可以看这里:[树:双亲、孩子、兄弟表示法][二叉树:先序、中序、后序遍历]

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;
}

 P6:main函数

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;
}

完整代码

 P0:完整代码

为了凑本文的字数,我这里贴一下整体的代码,删掉了细部注释~

#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;
}

 P1:执行结果

运行结果如下图所示~

数据结构05:树与二叉树[C++][哈夫曼树HuffmanTree]_第6张图片


结语

博文到此结束,写得模糊或者有误之处,欢迎小伙伴留言讨论与批评,督促博主优化内容,不限于以下内容~‍️‍️

博文若有帮助,欢迎小伙伴动动可爱的小手默默给个赞支持一下~

你可能感兴趣的:(#,数据结构,数据结构,c++,考研)