【数据结构之哈夫曼树与编码实现】

文章目录

  • 前言
  • 一、哈夫曼树与哈夫曼编码简介
    • 1. 什么是哈夫曼树?
    • 2. 为什么需要哈夫曼编码?
  • 二、哈夫曼编码原理
  • 三、哈夫曼树的构建步骤详解
    • 1. 统计字符频率
    • 2. 定义哈夫曼树节点
    • 3. 最小堆(优先队列)的构造
    • 4. 合并节点,构建哈夫曼树
    • 5. 生成编码表(递归/迭代遍历)
    • 6. 编码过程
    • 7. 解码过程
  • 四、C++ 实现示例
      • 代码说明
  • 五、测试
  • 六、复杂度分析
  • 七、应用场景与扩展


前言

随着信息时代的发展,数据压缩变得越来越重要。哈夫曼编码(Huffman Coding)是一种经典的前缀编码方法,能够为出现频率不同的符号分配不同长度的二进制编码,从而达到压缩的效果。


一、哈夫曼树与哈夫曼编码简介

1. 什么是哈夫曼树?

哈夫曼树是一棵用于生成最优前缀编码(二进制)的二叉树,原理来源于 David A. Huffman 在 1952 年提出的贪心算法。对于待编码的各个符号,根据其出现频率(或权重)构造一颗二叉树;较高频次的符号被分配较短的编码,较低频次的符号被分配较长的编码,从而在总的编码长度上达到最小。

2. 为什么需要哈夫曼编码?

  • 可变长度编码:相比固定长度编码(例如 ASCII 固定 8 位),哈夫曼编码根据符号频率分配长度,能显著减少总编码长度。
  • 前缀属性:编码具有前缀属性,即任何一个编码都不是另一个编码的前缀,从而保证了解码的唯一可区分性,不需分隔符即可逐位解析。
  • 广泛应用:常见于文件压缩(如 ZIP)、图像压缩(如 JPEG 的某些阶段)等场景。

二、哈夫曼编码原理

哈夫曼编码的核心是构建一棵最优二叉树(哈夫曼树),步骤概括如下:

  1. 统计各符号的频率:对待处理的数据进行扫描,记录每个符号出现的次数。
  2. 初始化森林:将每个符号视为一棵单节点二叉树,节点权重即频率,将这些节点放入最小堆(或其他可快速取最小的结构)。
  3. 合并最小节点:每次从堆中取出两个权重最小的节点,创建一个新节点,其权重为两节点之和,新节点的左、右子节点指向这两个取出的节点,然后将新节点插回堆中。
  4. 重复合并:直到堆中只剩下一个节点,该节点即哈夫曼树的根。
  5. 生成编码表:从根出发,对左右分支分别约定 “0”/“1” (或反之),遍历到叶子时得到对应符号的二进制编码。
  6. 编码与解码:使用编码表将原始数据转换为比特串;解码时从根沿着比特(0/1)路径走到叶子,即得到符号,直到处理完整个比特流。

三、哈夫曼树的构建步骤详解

1. 统计字符频率

  • 对于待编码的文本(或文件内容),遍历每个字符,使用 std::unordered_map<char, int>std::map<char, int> 记录出现次数。
  • 如果处理更广泛的符号集(如扩展 ASCII,或多字节符号),可将键类型改为 std::stringuint8_t 等。

示例伪代码:

std::unordered_map<char, int> freq;
for (char c : input) {freq[c]++;
}

2. 定义哈夫曼树节点

  • 每个节点包含:

    • 符号(仅叶子节点有效)
    • 频率/权重
    • 左右子节点指针

示例:

struct HuffmanNode {char ch;                  // 符号int freq;                 // 频率HuffmanNode *left, *right;HuffmanNode(char c, int f) : ch(c), freq(f), left(nullptr), right(nullptr) {}HuffmanNode(int f, HuffmanNode* l, HuffmanNode* r) : ch('\0'), freq(f), left(l), right(r) {}
};

3. 最小堆(优先队列)的构造

  • 使用 std::priority_queue,并提供自定义比较器,使得频率小的节点优先级高(即堆顶为最小频率节点)。
  • 比较器可定义为:
struct CompareNode {bool operator()(HuffmanNode* a, HuffmanNode* b) {return a->freq > b->freq; // 小顶堆}
};
  • 初始化时:对于频率表中的每个 (字符, 频率),创建一个 HuffmanNode* 并推入优先队列。
std::priority_queue<HuffmanNode*, std::vector<HuffmanNode*>, CompareNode> pq;
for (auto &p : freq) {pq.push(new HuffmanNode(p.first, p.second));
}

4. 合并节点,构建哈夫曼树

  • pq.size() > 1 时:

    1. 取出 node1 = pq.top(); pq.pop();
    2. 取出 node2 = pq.top(); pq.pop();
    3. 创建新节点 merged = new HuffmanNode(node1->freq + node2->freq, node1, node2);
    4. merged 推回 pq
  • 循环结束后,堆中唯一节点即哈夫曼树根 root = pq.top();

5. 生成编码表(递归/迭代遍历)

  • 从根开始,维护一个字符串 code

    • 到左子节点时,code.push_back('0')
    • 到右子节点时,code.push_back('1')
    • 访问到叶子节点(left==nullptr && right==nullptr),将 code 对应当前叶子符号保存到编码表 std::unordered_map<char, std::string>
  • 递归示例:

void buildCodes(HuffmanNode* node, const std::string &prefix, std::unordered_map<char, std::string> &codeTable) {if (!node) return;if (!node->left && !node->right) {// 叶子节点codeTable[node->ch] = prefix;} else {buildCodes(node->left, prefix + '0', codeTable);buildCodes(node->right, prefix + '1', codeTable);}
}

6. 编码过程

  • 遍历原始输入串,每个字符查表拼接对应二进制字符串,例如 std::string encoded; for (char c: input) encoded += codeTable[c];
  • 注意:实际压缩通常需要把这些“字符 ‘0’/‘1’”转换成位并打包存储;这里示例为演示流程,可用 std::vector<bool> 或自定义位操作将比特写入文件/内存。

7. 解码过程

  • 给定哈夫曼树根和编码后的二进制串,逐位遍历:

    • 从根开始,遇 ‘0’ 则走左子树,遇 ‘1’ 则走右子树。
    • 到达叶子节点时,输出该叶子符号,指针重置到根,继续处理后续比特,直到遍历完整个比特串。

示例:

std::string decode(HuffmanNode* root, const std::string &encoded) {std::string result;HuffmanNode* node = root;for (char bit : encoded) {if (bit == '0') node = node->left;else node = node->right;if (!node->left && !node->right) {// 叶子result.push_back(node->ch);node = root;}}return result;
}

四、C++ 实现示例

#include <iostream>
#include <queue>
#include <unordered_map>
#include <string>
#include <vector>// 哈夫曼树节点定义
struct HuffmanNode {char ch;int freq;HuffmanNode *left, *right;// 叶子节点构造HuffmanNode(char c, int f) : ch(c), freq(f), left(nullptr), right(nullptr) {}// 非叶子节点构造HuffmanNode(int f, HuffmanNode* l, HuffmanNode* r) : ch('\0'), freq(f), left(l), right(r) {}
};// 比较器:频率小的优先
struct CompareNode {bool operator()(HuffmanNode* a, HuffmanNode* b) {return a->freq > b->freq;}
};// 递归释放哈夫曼树内存
void freeTree(HuffmanNode* node) {if (!node) return;freeTree(node->left);freeTree(node->right);delete node;
}// 构建频率表
std::unordered_map<char, int> buildFrequency(const std::string &input) {std::unordered_map<char, int> freq;for (char c : input) {freq[c]++;}return freq;
}// 构建哈夫曼树,返回根指针
HuffmanNode* buildHuffmanTree(const std::unordered_map<char,int> &freq) {std::priority_queue<HuffmanNode*, std::vector<HuffmanNode*>, CompareNode> pq;// 初始化:所有叶节点入堆for (auto &p : freq) {pq.push(new HuffmanNode(p.first, p.second));}// 特殊情况:只有一种字符if (pq.size() == 1) {// 复制一个节点使得树有两层HuffmanNode* only = pq.top();pq.pop();// 新建一个虚拟节点,频率为 0HuffmanNode* dummy = new HuffmanNode('\0', 0);HuffmanNode* root = new HuffmanNode(only->freq + dummy->freq, only, dummy);return root;}// 合并过程while (pq.size() > 1) {HuffmanNode* n1 = pq.top(); pq.pop();HuffmanNode* n2 = pq.top(); pq.pop();HuffmanNode* merged = new HuffmanNode(n1->freq + n2->freq, n1, n2);pq.push(merged);}return pq.top();
}// 生成编码表
void buildCodes(HuffmanNode* node, const std::string &prefix, std::unordered_map<char, std::string> &codeTable) {if (!node) return;if (!node->left && !node->right) {// 叶子节点codeTable[node->ch] = prefix.empty() ? "0" : prefix; // 若只有一个字符,prefix 可能为空,此处设为 "0"} else {buildCodes(node->left, prefix + '0', codeTable);buildCodes(node->right, prefix + '1', codeTable);}
}// 编码
std::string encode(const std::string &input, const std::unordered_map<char, std::string> &codeTable) {std::string encoded;for (char c : input) {encoded += codeTable.at(c);}return encoded;
}// 解码
std::string decode(HuffmanNode* root, const std::string &encoded) {std::string result;HuffmanNode* node = root;for (char bit : encoded) {if (bit == '0') node = node->left;else node = node->right;// 到达叶子if (!node->left && !node->right) {result.push_back(node->ch);node = root;}}return result;
}int main() {// 示例文本std::string text;std::cout << "请输入要编码的文本: ";std::getline(std::cin, text);if (text.empty()) {std::cout << "输入为空,退出。\n";return 0;}// 1. 统计频率auto freq = buildFrequency(text);// 2. 构建哈夫曼树HuffmanNode* root = buildHuffmanTree(freq);// 3. 生成编码表std::unordered_map<char, std::string> codeTable;buildCodes(root, "", codeTable);// 4. 打印编码表std::cout << "字符  哈夫曼编码\n";for (auto &p : codeTable) {// 注意:部分字符(如空格)打印可能不易区分,可转义显示if (p.first == ' ') std::cout << "' '";else std::cout << p.first;std::cout << "    " << p.second << "\n";}// 5. 编码std::string encoded = encode(text, codeTable);std::cout << "编码后比特串长度: " << encoded.size() << "\n";// 可视化输出前若长度过长可只显示前若干std::cout << "编码比特串示例(前100位): " << encoded.substr(0, std::min(size_t(100), encoded.size())) << (encoded.size() > 100 ? "..." : "") << "\n";// 6. 解码验证std::string decoded = decode(root, encoded);std::cout << "解码后文本: " << decoded << "\n";if (decoded == text) {std::cout << "解码验证通过,原文与解码结果一致。\n";} else {std::cout << "解码验证失败!\n";}// 7. 释放内存freeTree(root);return 0;
}

代码说明

  • 使用 std::getline 读取整行文本,可包含空格。
  • 频率统计用 unordered_map;若需要对多字节符号(如 UTF-8 多字节),需改为按字节或按宽字符处理。
  • 哈夫曼树构建时,若仅一种符号,需要特别处理确保编码至少一位。
  • 编码表生成时,若 prefix 为空(仅一种叶子),指定 “0” 作为编码。
  • 示例中直接以 std::string 存储“0”“1”,实际压缩应用中,需将这些比特打包成字节或写入文件。
  • 解码时遍历比特流,走树直到叶子,输出字符。
  • 注意释放动态分配的节点,避免内存泄漏;也可用智能指针(如 std::unique_ptr)做改进。

五、测试

  • 示例输入
    "this is an example for huffman encoding"

  • 频率统计
    统计各字符出现次数,如 ' ' 频率最高等。

  • 编码表
    输出各字符的二进制编码,频率高的字符(空格、e、…)对应较短编码。

  • 压缩比估算

    • 原始:假设使用 ASCII,每字符 8 位,总长度 ≈ 输入长度 × 8。
    • 哈夫曼:总比特长度为编码后 encoded.size()。压缩比 = encoded.size() / (input.size() * 8)
  • 测试
    在本地编译运行,验证编码-解码过程正确性,并对不同文本测试压缩率。


六、复杂度分析

  • 时间复杂度

    • 频率统计:O(N),N 为输入长度。
    • 堆初始化:若 M 为不同符号个数,则 O(M) 节点推入堆,建堆 O(M)。
    • 合并过程:需要做 M-1 次合并,每次从堆中取两次和插入一次,时间 O(log M),故总 O(M log M)。
    • 生成编码表:遍历哈夫曼树,节点总数约 2M-1,时间 O(M)。
    • 编码/解码:编码 O(N * average_code_length) → 实际拼接字符串为 O(N * L),但若打包为位操作可做到 O(N);解码 O(encoded_length) ≈ O(N * average_code_length)。一般平均编码长度有限。
  • 空间复杂度

    • 频率表:O(M)。
    • 哈夫曼树:O(M)节点。
    • 编码表:O(M)存储每个符号对应字符串。
    • 编码输出:O(encoded_length)。
  • M 通常远小于 N(字符总种类有限),因此主要成本在 O(N) 级别。


七、应用场景与扩展

  • 文件压缩:ZIP、GZIP 等,哈夫曼编码常作为最后一步变长编码,但通常结合其他算法(如 LZ77)先做模式替换,再做哈夫曼编码。
  • 图像/音频压缩:JPEG、MP3 等在某些阶段使用哈夫曼编码对量化后的符号进行压缩。
  • 网络传输:可对文本或协议字段做哈夫曼编码以节省带宽(需双方约定编码表)。
  • 扩展到字典外符号:若在线构建编码表,可考虑动态哈夫曼(如 FGK 算法)或自适应哈夫曼。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/87832.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/87832.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于Hadoop的京东厨具商品数据分析及商品价格预测系统的设计与实现

文章目录有需要本项目的代码或文档以及全部资源&#xff0c;或者部署调试可以私信博主项目介绍数据采集用户界面系统展示管理员界面每文一语有需要本项目的代码或文档以及全部资源&#xff0c;或者部署调试可以私信博主 项目介绍 本项目围绕“京东厨具数据分析系统的设计与实…

深入解析TCP:可靠传输的核心机制与实现逻辑(三次握手、四次挥手、流量控制、滑动窗口、拥塞控制、慢启动、延时应答、面向字节流、粘包问题)

Linux系列 文章目录 Linux系列一、TCP连接的建立与断开1.1 TCP 三次握手1.2 TCP四次挥手1. TCP连接的本质是应用层间的通信通道2. 断开连接的核心是终止应用层通信3. 常见误解澄清 二、TCP协议的机制2.1 流量控制2.2 滑动窗口2.2.1 滑动窗口的工作原理2.2.2 基于滑动窗口快重传…

基于开源AI智能客服、AI智能名片与S2B2C商城小程序的微商服务质量提升路径研究

摘要&#xff1a;在科技飞速发展的背景下&#xff0c;产品技术含量与复杂度显著提升&#xff0c;客户正确使用产品并体验其价值愈发依赖代理的专业指导与服务。本文聚焦开源AI智能客服、AI智能名片与S2B2C商城小程序在微商服务中的应用&#xff0c;通过分析其技术原理与实践案例…

[netty5: HttpHeaders HttpHeadersFactory]-源码分析

HttpHeaders HttpHeaders 是用于存储和操作HTTP请求或响应头部字段的接口。 // DefaultHttpHeaders, HttpHeadersFactory.TrailingHttpHeaders public interface HttpHeaders extends Iterable<Entry<CharSequence, CharSequence>> {static HttpHeaders emptyHead…

基于Flink 1.20、StarRocks与TiCDC构建高效数据处理链路教程

在大数据处理领域&#xff0c;实现高效、实时的数据处理与分析至关重要。Flink作为强大的流批一体化计算框架&#xff0c;结合StarRocks这一高性能的实时分析型数据库&#xff0c;再搭配TiCDC&#xff08;TiDB Change Data Capture&#xff09;用于捕获数据变更&#xff0c;能够…

便捷的Office批量转PDF工具

软件介绍 本文介绍的软件是一款能实现Office批量转换的工具&#xff0c;名为五五Excel word批量转PDF。 软件小巧 这款五五Excel word批量转PDF软件大小不到2M。 操作步骤一 使用该软件时&#xff0c;只需把软件和需要转换的Word或Excel文件放在同一个文件夹里。 操作步骤…

tcp长连接与短连接

TCP连接本身是一个传输层协议&#xff0c;它既可以实现长连接&#xff0c;也可以实现短连接。这取决于应用层的使用方式。 短连接&#xff08;Short Connection&#xff09; 特点&#xff1a;每次请求都建立新的TCP连接&#xff0c;完成后立即关闭流程&#xff1a;建立连接 →…

llvm polly,亲自测试

1&#xff09;下载并安装 Polly - Getting Started git clone https://github.com/llvm/llvm-project.git 大概需要半个小时&#xff0c;有时候被墙掉就打不开 2&#xff09; mkdir build && cd build cmake -DLLVM_ENABLE_PROJECTSclang;polly ../llvm cmake --b…

Spring AI 项目实战(十四):Spring Boot + Vue3 +AI + DeepSeek 实现空气质量智能预测系统(附完整源码)

系列文章 序号文章名称1Spring AI 项目实战(一):Spring AI 核心模块入门2Spring AI 项目实战(二):Spring Boot + AI + DeepSeek 深度实战(附完整源码)3Spring AI 项目实战(三):Spring Boot + AI + DeepSeek 打造智能客服系统(附完整源码)4

腾讯云 CDN 不支持 WebSocket 的现状与华为云 CDN 的替代方案-优雅草卓伊凡

腾讯云 CDN 不支持 WebSocket 的现状与华为云 CDN 的替代方案-优雅草卓伊凡 问题背景 卓伊凡今天发现&#xff0c;腾讯云 CDN 不支持 WebSocket 协议&#xff0c;而公司的部分业务&#xff08;如实时聊天、在线协作、游戏互动、股票行情推送等&#xff09;依赖长连接通信。昨…

MybatisPlus(一)扩展功能

扩展功能 一、静态工具二、逻辑删除三、通用枚举1、定义枚举2、配置枚举处理器3、测试 四、JSON类型处理器1、定义实体2、使用类型处理器 五、分页1、配置分页插件2、分页API3、示例 一、静态工具 有的时候Service之间也会相互调用&#xff0c;为了避免出现循环依赖问题&#…

Redis哨兵模式之Sentinel模式(二)

一、多节点哨兵如何配置&#xff1f; 哨兵配置原理图 注意&#xff1a;sentinel哨兵模式的搭建是建立在redis主从复制节点配置基础而搭建&#xff0c;在主从配置中从库需要配置好replicaof关联上主库并关闭安全模式&#xff0c;然后设置好bind端口才能关联上机器&#xff0c;而…

基于Excel的数据分析思维与分析方法

数据分析一定要会Excel、SQL和Python&#xff1f;非常肯定地回答您&#xff0c;Python、R语言、Excel函数和VBA&#xff0c;以及高级数据分析软件&#xff0c;都学不到&#xff0c;您将学到&#xff1a;5个有效的数据分析利器&#xff0c;以及分析思维 一、描述性统计分析 在…

计算机网络笔记(不全)

一、计算机网络体系结构1.计算机网络的概念计算机网络&#xff1a;由若干结点和连接这些结点的链路组成。结点可以是计算机、集线器、交换机、路由器等。互连网(internet)&#xff1a;多个计算机网络通过路由器互相连接而成&#xff0c;可用任意协议通信。互联网(因特网Interne…

XML Schema 复合元素

XML Schema 复合元素 引言 XML(可扩展标记语言)作为一种灵活的标记语言,广泛应用于数据交换和存储。XML Schema 是一种用于描述和定义 XML 文档结构的语言,它定义了 XML 文档的元素、属性、类型和约束。本文将详细介绍 XML Schema 中的复合元素,并探讨其在实际应用中的重…

华为云Flexus+DeepSeek征文 | 弹性算力实战:Flexus X实例自动扩缩容策略优化

华为云FlexusDeepSeek征文 | 弹性算力实战&#xff1a;Flexus X实例自动扩缩容策略优化 &#x1f31f; 嗨&#xff0c;我是IRpickstars&#xff01; &#x1f30c; 总有一行代码&#xff0c;能点亮万千星辰。 &#x1f50d; 在技术的宇宙中&#xff0c;我愿做永不停歇的探索者…

【仓颉】运行环境配置VSCode + Win11

作者&#xff1a;大李子 团队&#xff1a;坚果派 十年iOS&#xff0c;All in转鸿蒙 前言 “仓颉编程语言是一款面向全场景智能的新一代编程语言&#xff0c;主打原生智能化、天生全场景、高性能、强安全。融入鸿蒙生态&#xff0c;为开发者提供良好的编程体验。” ——摘自仓…

【K线训练软件研发历程】【日常记录向】1.K线滑动窗口

文章目录 当前效果未来发展思路技术选型值得分享的技术点数据加载、解析的代码echats的代码当前效果 👆相当于有个hello world了。 未来发展思路 开源 技术选型 界面直接采用electron,等开源后,可以直接挂release,用户下载安装包后,一键安装,一键运行,降低使用门槛…

抖音解析下载工具 v1.0.0:免安装单文件,一键无水印保存高清视音频

宝子们&#xff0c;今天给你们带来一款超轻量的抖音下载神器——抖音解析下载工具 v1.0.0。 它只有单文件&#xff0c;双击就能用&#xff0c;免安装、无广告、完全免费&#xff0c;复制粘贴链接即可一键解析下载高清无水印视频/音频&#xff0c;简直不要太方便&#xff01; 为…

Ingress——2

目录 ‌一. 域名重定向&#xff08;HTTP→HTTPS/旧域名跳转&#xff09;‌ ‌二. 前后端分离Rewrite&#xff08;路径改写&#xff09;‌ ‌三. 混合配置示例&#xff08;重定向Rewrite&#xff09;‌ ‌四. SSL/TLS配置&#xff08;HTTPS加密&#xff09;‌ ‌五. 基本认…