C++详解哈夫曼树的概念与实现步骤

 更新时间:2022年04月24日 16:37:41   作者:BugMaker-shen  
给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近

一、基本概念

结点的权: 有某种现实含义的数值

结点的带权路径长度: 从结点的根到该结点的路径长度与该结点权值的乘积

树的带权路径长度: 树上所有叶结点的带权路径长度之和

哈夫曼树: 在含有 n n n个带权叶结点的二叉树中, w p l wpl wpl 最小 的二叉树称为哈夫曼树,也称最优二叉树(给定叶子结点,哈夫曼树不唯一)。

二、构造哈夫曼树

比较简单,此处不赘述步骤

三、哈夫曼树的基本性质

  • 每个初始结点最终都是叶结点,且权值越小的结点到根结点的路径长度越大
  • 具有 n n n个根结点的哈夫曼树的结点总数为 2 n − 1
  • 哈夫曼树中不存在度为1的结点
  • 哈夫曼树不唯一,但 w p l必然相同且最优

四、哈夫曼编码

目的:为给定的字符集合构建二进制编码,使得编码的期望长度达到最短

在考试中,小渣利用哈夫曼编码老渣发电报传递100道选择题的答案,小渣传递了10个A、8个B、80个C、2个D,老渣利用哈夫曼编码的方式解码。

小渣构造的哈夫曼树如下:

可以发现,A、B、C、D的编码分别为10、111、0、110。

这样小渣只要根据1~100题的答案顺序发送01序列,老渣收到后进行解码就能正确收到答案了。而且哈夫曼编码的方式不会有歧义,因为哈夫曼编码是一种前缀编码。

前缀编码: 没有一个编码是另一个编码的前缀,因为数据节点都是叶子节点。如果出现一个字符的编码是另一个字符编码的前缀,那这个字符一定处于内部节点,这是不可能的

由哈夫曼树得到的哈夫曼编码: 字符集中的每个字符都是以叶子结点出现在哈夫曼树中,各个字符出现的频率为结点的权值。

给字符串进行编码的时候,由于出现频率越高(权值大)的字符距离根节点越进,编码越短;只有出现频率越低(权值小)的字符距离根节点较远,编码长。没关系,由于频率高的字符编码都短,所以哈夫曼编码可以得到最短的编码序列

五、哈夫曼解码

哈夫曼编码不同于ASCII和Unicode这些字符编码,这些字符集中的码长都采用的是长度相同的编码方案,而哈夫曼编码使用的是变长编码,而且哈夫曼编码满足立刻可解码性(就是说任一字符的编码都不会是另一个更长字符编码的前缀),只要当某个字符的编码中所有位被全部接收时,可以立即进行解码而无须等待后面接收的位来决定是否存在另一个合法的更长的编码

第一张表不满足立刻可解码性,第二张表满足

我们接收100的时候,需要考虑是立刻解码成D还是等待接收下一位,如果下一位是0就可以解码成B,这就说明表中的编码不具有立刻可解码性

第二张表就具有立刻可解码性,因为任一字符的编码都不是另一个更长字符编码的前缀。只要收到的序列对应了某个字符的编码,直接解码成对应字符即可,无需等待后面的数据

我们的代码实现是用字符串构建哈夫曼树,只能针对由该字符串包含的字符组成的字符串进行编解码。代码里使用字符串存储的编码,实际上应该用bit进行存储

#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <unordered_map>
#include <queue>
using namespace std;
using uint = unsigned int;
class HuffmanTree {
public:
    // 这里的lambda表达式用来初始化function函数对象
    // priority_queue的构造函数指出,如果传入一个参数,那这个参数用来初始化比较器对象
    // 如果传入两个参数,第一个是比较器对象,第二个是底层容器
    HuffmanTree()
        :min_heap_([](Node* n1, Node* n2)->bool {return n1->weight_ > n2->weight_; })
        , root_(nullptr)
    {}
    ~HuffmanTree() {
        init();
        cout << "已释放所有内存!" << endl;
    }
    // 根据字符串创建哈夫曼树
    void create(const string& str) {
        if (root_ != nullptr) {
            cout << "哈夫曼树初始化..." << endl;
            init();
            cout << "初始化完成!" << endl;
        }
        // 统计频率(权重)
        unordered_map<char, uint> w_map;
        for (char c : str) {
            w_map[c]++;
        }
        // 遍历w_map,把所有的字符对应的权重放入小根堆,按照权重排序
        for (pair<const char, uint>& p : w_map) {
            min_heap_.push(new Node(p.first, p.second));
        }
        // 根据优先级队列,从小根堆中取出节点,构建哈夫曼树
        while (min_heap_.size() > 1) {
            Node* n1 = min_heap_.top();
            min_heap_.pop();
            Node* n2 = min_heap_.top();
            min_heap_.pop();
            Node* node = new Node('\0', n1->weight_ + n2->weight_);  // 内部节点存\0
            node->left_ = n1;
            node->right_ = n2;
            min_heap_.push(node);
        }
        root_ = min_heap_.top();
        min_heap_.pop();
        // 创建完哈夫曼树,直接对传入的海量字符进行编码并存储到code_map_
        create_huffman_code(str);
    }
    string get_code(const string& str) {
        // 利用哈夫曼树对str编码并返回
        string code;
        for (char c : str) {
            code += code_map_[c];
        }
        return code;
    }
    void show_huffman_code() const {
        // 打印哈夫曼编码
        for (const auto& pair : code_map_) {
            cout << pair.first << " : " << pair.second << endl;
        }
    }
    string decode(const string& encode_str) {
        Node* cur = root_;
        string decode_str;
        for (char c : encode_str) {
            if (c == '0') {
                cur = cur->left_;
            }
            else {
                cur = cur->right_;
            }
            if (cur->left_ == nullptr && cur->right_ == nullptr) {
                // 到达叶子节点
                decode_str.push_back(cur->data_);
                cur = root_;
            }
        }
        return decode_str;
    }
    uint get_wpl() {
        if (root_ == nullptr) {
            return 0;
        }
        if (root_->left_ == nullptr && root_->right_ == nullptr) {
            // 对于叶子节点,直接返回自己的weight * depth
            return root_->weight_ * 1;
        }
        else {
            // 对于内部节点,直接返回从子节点拿到的weight之和
            return get_w(root_->left_, 2) + get_w(root_->right_, 2);
        }
    }
private:
    struct Node {
        Node(char data, uint weight)
            :data_(data)
            , weight_(weight)
            , left_(nullptr)
            , right_(nullptr)
        {}
        char data_;
        uint weight_;
        Node* left_;
        Node* right_;
    };
private:
    // 防止当前对象重新构建哈夫曼树,释放所有的节点,然后初始化私有成员
    void init() {
        // 释放哈夫曼树的节点
        if (root_ != nullptr) {
            queue<Node*> q;
            q.push(root_);
            while (!q.empty()) {
                Node* node = q.front();
                q.pop();
                if (node->left_ != nullptr) {
                    q.push(node->left_);
                }
                if (node->right_ != nullptr) {
                    q.push(node->right_);
                }
                delete node;
            }
            MinHeap empty([](Node* n1, Node* n2)->bool {return n1->weight_ > n2->weight_; });
            swap(empty, min_heap_);
            code_map_.clear();
        }
    }
    void create_huffman_code(const string& str) {
        string code;
        create_huffman_code(root_, code);
    }
    void create_huffman_code(Node* node, string code) {
        if (node->left_ == nullptr && node->right_ == nullptr) {
            code_map_[node->data_] = code;
            return;
        }
        create_huffman_code(node->left_, code + "0");
        create_huffman_code(node->right_, code + "1");
    }
    uint get_w(Node* node, int depth) {
        if (node == nullptr) {
            return 0;
        }
        if (node->left_ == nullptr && node->right_ == nullptr) {
            // 对于叶子节点,直接返回自己的weight * depth
            return node->weight_ * depth;
        }
        else {
            // 对于内部节点,直接返回从子节点拿到的weight之和
            return get_w(node->left_, depth + 1) + get_w(node->right_, depth + 1);
        }
    }
private:    
    Node* root_;
    unordered_map<char, string> code_map_;  // 存储字符对应的哈夫曼编码
    using MinHeap = priority_queue<Node*, vector<Node*>, function<bool(Node*, Node*)>>;
    MinHeap min_heap_; // 构建哈夫曼树的时候使用小根堆
};
int main() {
    string str = "Aa";
    HuffmanTree htree;
    htree.create(str);
    htree.show_huffman_code();
    cout << htree.get_wpl() << endl;
    str = "ABC";
    htree.create(str);
    htree.show_huffman_code();
    cout << htree.get_wpl() << endl;;
    string encode = htree.get_code(str);
    cout << "encode:" << encode << endl;
    cout << "decode:" << htree.decode(encode) << endl;
    return 0;
}

六、文件的压缩和解压缩

我们利用哈夫曼编码压缩文件的时候,如果文件是100M,我们可以压缩成20M,如果文件时1K,我们可能压缩成2K,当文件较小的时候,我们得到的压缩文件反而更大了,这是为什么?

文件的压缩过程如下:

  • 按字节读取原文件的所有内容,并统计字节数据的权值,构建哈夫曼树
  • 通过哈夫曼树,得到文件的哈夫曼编码
  • 把文件的内容按字节进行编码,将编码内容按bit存储成压缩文件,还要存储文件字节数据以及权值

解码的过程如下:

  • 读取原始文件的字节数据以及权值,构建哈夫曼树
  • 读取压缩文件的01码,利用哈夫曼树对01进行解码,将解码数据存储成新的文件,就解码出了原始文件

由于压缩文件不仅存储01码,还需要存储文件字节数据以及权值用来重建哈夫曼树(就是代码中的w_map)。当原始文件较小时,文件字节数据以及权值可能大于原始文件的大小,故小文件压缩后可能变大

到此这篇关于C++详解哈夫曼树的概念与实现步骤的文章就介绍到这了,更多相关C++哈夫曼树内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 利用C语言实现一个最简单的飞机游戏

    利用C语言实现一个最简单的飞机游戏

    在前面弹跳小球 的基础上实现一个简单的飞机游戏,主要包括飞机的显示、控制移动、显示复杂图案、发射激光、打靶练习等功能,感兴趣的可以尝试一下
    2022-10-10
  • C++特性之智能指针shared_ptr详解

    C++特性之智能指针shared_ptr详解

    shared_ptr是C++11提供的一种智能指针类,它足够智能,可以在任何地方都不使用时自动删除相关指针,从而帮助彻底消除内存泄漏和悬空指针的问题。本文主要是来和大家聊聊shared_ptr的使用,需要的可以参考一下
    2022-12-12
  • C++初识类和对象

    C++初识类和对象

    类是创建对象的模板,一个类可以创建多个对象,每个对象都是类类型的一个变量;创建对象的过程也叫类的实例化。每个对象都是类的一个具体实例(Instance),拥有类的成员变量和成员函数
    2021-10-10
  • 详解c++ libuv工作队列

    详解c++ libuv工作队列

    这篇文章主要介绍了c++ libuv工作队列的相关资料,帮助大家更好的理解和使用libuv,感兴趣的朋友可以了解下
    2021-02-02
  • C++文件读和写的使用

    C++文件读和写的使用

    本文主要介绍了C++文件读和写的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • C++之内存泄漏排查详解

    C++之内存泄漏排查详解

    这篇文章主要介绍了c++ 如何排查内存泄漏,帮助大家更好的理解和学习使用c++,感兴趣的朋友可以了解下,希望能够给你带来帮助
    2021-10-10
  • C++11新特性之四种类型转换cast说明

    C++11新特性之四种类型转换cast说明

    类型转换是项目中常使用的一种语法规则,几乎每个编程语言都不可避免的涉及到这方面,下面这篇文章主要给大家介绍了关于C++11新特性之四种类型转换cast说明的相关资料,需要的朋友可以参考下
    2023-02-02
  • c++实现的常见缓存算法和LRU

    c++实现的常见缓存算法和LRU

    LRU缓存算法也叫LRU页面置换算法,是一种经典常用的页面置换算法,下面这篇文章主要介绍了c++实现的常见缓存算法和LRU,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-01-01
  • 基于QT5的文件读取程序的实现

    基于QT5的文件读取程序的实现

    本文主要介绍了基于QT5的文件读取程序的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-05-05
  • C++深入讲解函数重载

    C++深入讲解函数重载

    C++ 允许多个函数拥有相同的名字,只要它们的参数列表不同就可以,这就是函数的重载(Function Overloading),借助重载,一个函数名可以有多种用途
    2022-07-07

最新评论