当前位置: 首页 > news >正文

[C++数据结构](31)哈夫曼树,哈夫曼编码与解码

文章目录

  • 哈夫曼树
    • 带权路径长度 WPL
    • 哈夫曼算法
    • 哈夫曼编码
  • 代码实现
    • 框架
    • 统计字符频数与构建哈夫曼树
    • 构造与析构
    • 解码
    • 完整代码

哈夫曼树

哈夫曼树又称最优二叉树,它是由 n 个带权叶子结点构成的所有二叉树中带权路径长度 WPL 最短的二叉树。

带权路径长度 WPL

设二叉树有 n 个带权叶子结点,从根结点到各叶子结点的路径长度与相应叶子结点权值的乘积之和称为 树的带权路径长度(Weighted Path Length of Tree,WPL)

其计算公式如下:
W P L = ∑ i = 1 n w i l i WPL=\sum_{i=1}^{n}w_il_i WPL=i=1nwili
w i w_i wi 表示二叉树的第 i i i 个结点的权值, l i l_i li 为从根结点到第 i i i 个结点的路径长度

img
W P L = 2 × 2 + 2 × 3 + 2 × 4 + 2 × 7 = 32 WPL=2\times2+2\times3+2\times4+2\times7=32 WPL=2×2+2×3+2×4+2×7=32

哈夫曼算法

我们需要的是 WPL 最小的带权叶子结点所构成的树,怎样能让 WPL 最小?

哈夫曼给出了一种算法,可以帮我们构造出这种树,因此称之为哈夫曼树。

算法步骤如下:

  1. 初始化:给定 n n n 个权值,构造 n n n 棵只有一个根结点的二叉树,构成一个二叉树集合 F F F
  2. 选取与合并:从 F F F 中选取根结点权值最小的两棵树,将它们根结点的权值相加并构造一个新的结点,让这两棵树作为新结点的左右子树
  3. 删除与加入:从 F F F 中删除上一步作为左右子树的两棵树,将新建立的二叉树加入到 F F F
  4. 重复 2, 3 两步,直到 F F F 中只剩一棵树,这棵树就是哈夫曼树

注意:对于同一组权值,构造出的哈夫曼树不是唯一的

例子

li

哈夫曼编码

哈夫曼树具有叶结点权值越小,离根越远,叶结点权值越大,离根越近的特点,此外其仅有叶结点的度为0其他结点度均为2

因此我们可以利用这一特性对字符进行编码,对于使用频率较高的字符,其编码短一些,对于使用频率较低的字符,其编码可以长一些。

相较等长编码而言,使用哈夫曼编码对于使用频率不相等字符构造出一种 不等长编码,有助于获得更好的空间效率。

在设计编码时,要考虑解码的唯一性,如果一组编码中任一编码都不是其他任何一个编码的前缀,那么称这组编码为 前缀编码,其保证了编码被解码时的唯一性。哈夫曼树可用于构造 最短的前缀编码,即哈夫曼编码

基本步骤如下:

  1. 输入一段字符序列,统计每个字符出现的频率(频数)
  2. 以频率(频数)作为权值构建哈夫曼树
  3. 通过从根结点到每一个叶子结点的路径进行编码,向左的路径记为 “0”,向右的路径记为 “1”

例子

假设已经统计出 A, B, C, D, E, F 这 5 个字母的频数为 2, 3, 5, 7, 8

且以此构建出哈夫曼树如下

img

其编码为:

ABCDE
000001011011

代码实现

框架

  • 对于哈夫曼树,我们将其设计成二叉链表,编码的时候可以由上至下遍历二叉树,每个结点还包括一个权值和其对应的字符(除叶子结点以外都为\0)
  • 对于用来存储树的集合 F F F ,因为经常要从中选取根结点weight值最小的树,所以我们选取 小堆 这个数据结构
  • 使用哈希表来存储编码和字符的映射
struct HTNode
{
	HTNode(unsigned int w, char ch = '\0')
		: _weight(w)
		, _ch(ch)
		, _LChild(nullptr)
		, _RChild(nullptr)
	{}

	unsigned int _weight;
	char _ch;
	HTNode* _LChild;
	HTNode*	_RChild;
};

class HuffmanTree
{
private:
    // 仿函数,按weight,建小堆
	struct CmpByWeight
	{
		bool operator()(const HTNode* p1, const HTNode* p2)
		{
			return p1->_weight > p2->_weight;
		}
	};
public:
    
private:
	priority_queue<HTNode*, vector<HTNode*>, CmpByWeight> _HuffmanTree; // 小堆,存森林
	unordered_map<string, char> _HuffmanCode; // 哈希表,存编码和相应字符的pair
};

统计字符频数与构建哈夫曼树

使用哈希表统计每个字符出现的次数:

	unordered_map<char, unsigned int> StatisticalFrequency(const string& s)
	{
		unordered_map<char, unsigned int> tmp;
		for (auto& e : s)
		{
			++tmp[e];
		}
		return tmp;
	}

首先构造叶子结点插入堆中,然后递归进行编码,将每个编码和其对应的字符存到 _HuffmanCode 中:

	void _HuffmanCoding(HTNode* root, string& tmp)
	{
		if (root->_LChild == nullptr && root->_RChild == nullptr)
		{
			_HuffmanCode[tmp] = root->_ch;
			return;
		}
		_HuffmanCoding(root->_LChild, tmp += '0');
		tmp.pop_back();
		_HuffmanCoding(root->_RChild, tmp += '1');
		tmp.pop_back();
	}

	// 创建哈夫曼树,生成哈夫曼编码
	void CrtHuffmanTree(unordered_map<char, unsigned int>& frequency)
	{
		//插入叶子结点
		for (auto& e : frequency)
		{
			_HuffmanTree.push(new HTNode(e.second, e.first));
		}
		//创建非叶子结点,建立哈夫曼树
		while (_HuffmanTree.size() > 1)
		{
            // 分两次取出堆顶元素,构造新结点作为这两棵子树的父亲,并将新树入堆
			HTNode* s1 = _HuffmanTree.top();
			_HuffmanTree.pop();
			HTNode* s2 = _HuffmanTree.top();
			_HuffmanTree.pop();
			HTNode* parent = new HTNode(s1->_weight + s2->_weight);
			parent->_LChild = s1;
			parent->_RChild = s2;
			_HuffmanTree.push(parent);
		}
		// 递归,由上至下求哈夫曼编码
		string tmp;
		_HuffmanCoding(_HuffmanTree.top(), tmp);
	}

构造与析构

	void Destroy(HTNode* root)
	{
		if (root == nullptr) return;
		Destroy(root->_LChild);
		Destroy(root->_RChild);
		delete root;
	}

	HuffmanTree(const string& s)
	{
		// 统计频数
		auto&& m = StatisticalFrequency(s);
		// 构建哈夫曼树并编码
		CrtHuffmanTree(m);
	}

	~HuffmanTree()
	{
		Destroy(_HuffmanTree.top());
	}

解码

	void decode(const string& code)
	{
		string s;
		int count = 0;
		for (int i = 0; i < code.size(); ++i)
		{
			s += code[i];
			auto&& it = _HuffmanCode.find(s);
			if (it != _HuffmanCode.end())
			{
				cout << it->second;
				++count;
				s.clear();
			}
		}
		if (!s.empty())
		{
			cout << "...输入的序列有误!" << endl;
			printf("已成功解码 %d 个字符\n", count);
		}
	}

完整代码

完整代码将上述整合后,还增加了存储哈夫曼树和哈夫曼编码的文件操作。

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <unordered_map>
#include <fstream>
using namespace std;

struct HTNode
{
	HTNode(unsigned int w, char ch = '\0')
		: _weight(w)
		, _ch(ch)
		, _LChild(nullptr)
		, _RChild(nullptr)
	{}

	unsigned int _weight;
	char _ch;
	HTNode* _LChild;
	HTNode*	_RChild;
};

class HuffmanTree
{
private:
	// 仿函数,按weight,建小堆
	struct CmpByWeight
	{
		bool operator()(const HTNode* p1, const HTNode* p2)
		{
			return p1->_weight > p2->_weight;
		}
	};

	void _PreOrder(HTNode* root, ostream& treeFile)
	{
		if (root == nullptr)
		{
			treeFile << '#' << endl;
			return;
		}
		treeFile << root->_weight << ' ' << root->_ch << endl;
		_PreOrder(root->_LChild, treeFile);
		_PreOrder(root->_RChild, treeFile);
	}

	void _HuffmanCoding(HTNode* root, string& tmp)
	{
		if (root->_LChild == nullptr && root->_RChild == nullptr)
		{
			_HuffmanCode[tmp] = root->_ch;
			return;
		}
		_HuffmanCoding(root->_LChild, tmp += '0');
		tmp.pop_back();
		_HuffmanCoding(root->_RChild, tmp += '1');
		tmp.pop_back();
	}

	// 创建哈夫曼树,生成哈夫曼编码
	void CrtHuffmanTree(unordered_map<char, unsigned int>& frequency)
	{
		//插入叶子结点
		for (auto& e : frequency)
		{
			_HuffmanTree.push(new HTNode(e.second, e.first));
		}
		//创建非叶子结点,建立哈夫曼树
		while (_HuffmanTree.size() > 1)
		{
			HTNode* s1 = _HuffmanTree.top();
			_HuffmanTree.pop();
			HTNode* s2 = _HuffmanTree.top();
			_HuffmanTree.pop();
			HTNode* parent = new HTNode(s1->_weight + s2->_weight);
			parent->_LChild = s1;
			parent->_RChild = s2;
			_HuffmanTree.push(parent);
		}
		// 递归,由上至下求哈夫曼编码
		string tmp;
		_HuffmanCoding(_HuffmanTree.top(), tmp);
	}

	unordered_map<char, unsigned int> StatisticalFrequency(const string& s)
	{
		unordered_map<char, unsigned int> tmp;
		for (auto& e : s)
		{
			++tmp[e];
		}
		return tmp;
	}

	void Destroy(HTNode* root)
	{
		if (root == nullptr) return;
		Destroy(root->_LChild);
		Destroy(root->_RChild);
		delete root;
	}

public:
	HuffmanTree(const string& s)
	{
		// 统计频数
		auto&& m = StatisticalFrequency(s);
		// 构建哈夫曼树并编码
		CrtHuffmanTree(m);
	}

	void decode(const string& code)
	{
		string s;
		int count = 0;
		for (int i = 0; i < code.size(); ++i)
		{
			s += code[i];
			auto&& it = _HuffmanCode.find(s);
			if (it != _HuffmanCode.end())
			{
				cout << it->second;
				++count;
				s.clear();
			}
		}
		if (!s.empty())
		{
			cout << "...输入的序列有误!" << endl;
			printf("已成功解码 %d 个字符\n", count);
		}
	}

	// 打印并存文件
	void Print(ostream& codeFile, ostream& treeFile)
	{
		// 存树
		_PreOrder(_HuffmanTree.top(), treeFile);
		// 存编码并打印
		cout << "字符\t编码" << endl;
		codeFile << "字符\t编码" << endl;
		for (auto& e : _HuffmanCode)
		{
			cout << e.second << '\t' << e.first << endl;
			codeFile << e.second << '\t' << e.first << endl;
		}
	}

	~HuffmanTree()
	{
		Destroy(_HuffmanTree.top());
	}

private:
	priority_queue<HTNode*, vector<HTNode*>, CmpByWeight> _HuffmanTree; // 小堆,存森林
	unordered_map<string, char> _HuffmanCode; // 哈希表,存编码和相应字符的pair
};

int main()
{
	// 打开文件
	ofstream codeFile("code.txt");
	ofstream treeFile("tree.txt");

	// 输入字符序列
	string s;
	cout << "请输入一串字符序列:" << endl;
	cin >> s;

	// 构建哈夫曼树,生成编码打印并存文件
	HuffmanTree ht(s);
	ht.Print(codeFile, treeFile);

	//输入序列以解码
	cout << "\n请输入01序列进行解码:" << endl;
	cin >> s;
	ht.decode(s);

	return 0;
}

相关文章:

  • 创建自己的AVA行为识别数据集以及MMLAB系列训练、测试自己的数据
  • C++多态之虚函数表详解及代码示例
  • 廊坊特色农业 国稻种芯·中国水稻节:河北复合农业促增收
  • 陈吉宁经典演讲:平庸与卓越的差别
  • 【IO】文件操作基础知识
  • Win11安装怎么跳过TPM-Win11安装跳过TPM的方法介绍
  • 【沃趣科技】初探MySQL迁移到ClickHouse
  • EndNote使用技巧之引用文献信息的导入与修改
  • java怎么连接数据库mysql
  • AI绘画逆着玩火了,敢不敢发自拍看AI如何用文字形容你?
  • 是时候掌握SpringMVC源码了-初探篇
  • 上手Python之set(集合)
  • Visual Studio 2022开发Arduino详述
  • 【机器人定位引导中的机器视觉技术】
  • 零售商贩mysql表设计:主题信息表(theme)
  • Codepen 每日精选(2018-3-25)
  • Golang-长连接-状态推送
  • gops —— Go 程序诊断分析工具
  • React-redux的原理以及使用
  • Redis提升并发能力 | 从0开始构建SpringCloud微服务(2)
  • unity如何实现一个固定宽度的orthagraphic相机
  • 从重复到重用
  • 复习Javascript专题(四):js中的深浅拷贝
  • - 概述 - 《设计模式(极简c++版)》
  • 计算机常识 - 收藏集 - 掘金
  • 前端存储 - localStorage
  • 浅谈Golang中select的用法
  • 小程序测试方案初探
  • 写给高年级小学生看的《Bash 指南》
  • 怎么把视频里的音乐提取出来
  • ​比特币大跌的 2 个原因
  • ​如何在iOS手机上查看应用日志
  • # 透过事物看本质的能力怎么培养?
  • ###51单片机学习(1)-----单片机烧录软件的使用,以及如何建立一个工程项目
  • #include
  • #我与虚拟机的故事#连载20:周志明虚拟机第 3 版:到底值不值得买?
  • (10)STL算法之搜索(二) 二分查找
  • (2022版)一套教程搞定k8s安装到实战 | RBAC
  • (C语言)fread与fwrite详解
  • (Python) SOAP Web Service (HTTP POST)
  • (附源码)计算机毕业设计ssm高校《大学语文》课程作业在线管理系统
  • (过滤器)Filter和(监听器)listener
  • (七)MySQL是如何将LRU链表的使用性能优化到极致的?
  • (学习日记)2024.04.10:UCOSIII第三十八节:事件实验
  • (一)python发送HTTP 请求的两种方式(get和post )
  • (转)Oracle 9i 数据库设计指引全集(1)
  • (转)Sublime Text3配置Lua运行环境
  • (转载)Linux网络编程入门
  • .NET Core6.0 MVC+layui+SqlSugar 简单增删改查
  • .NET 将多个程序集合并成单一程序集的 4+3 种方法
  • .net 设置默认首页
  • .net网站发布-允许更新此预编译站点
  • .NET微信公众号开发-2.0创建自定义菜单
  • .vue文件怎么使用_我在项目中是这样配置Vue的
  • @ModelAttribute 注解