基于哈夫曼树的数据压缩算法

目录

1.算法核心思想

2.算法原理

[1. 信息论基础](#1. 信息论基础)

[2. 核心概念](#2. 核心概念)

3.算法步骤

步骤1:统计字符频率

步骤2:构建哈夫曼树

步骤3:生成哈夫曼编码

步骤4:数据压缩与解压

4.实现

4.1头文件包含与函数声明

4.2整体框架

4.3构建哈夫曼树

4.4生成哈夫曼码

4.5数据压缩与解压


1.算法核心思想

哈夫曼编码是一种变长编码算法,由David A. Huffman于1952年提出。其核心思想是:

为出现频率高的字符分配较短的编码,为出现频率低的字符分配较长的编码,从而减少整体编码长度

2.算法原理

1. 信息论基础

  • 熵编码:根据信息出现的概率进行编码

  • 最优前缀码:没有任何一个编码是另一个编码的前缀,确保解码唯一性

2. 核心概念

  • 字符频率:每个字符在文本中出现的次数

  • 权重:节点的重要性,通常使用字符频率

  • 路径长度:从根节点到叶节点的边数

3.算法步骤

步骤1:统计字符频率

复制代码
输入:字符串 "aaaaaaabbbbbccdddd"
统计结果:
  a: 7次
  b: 5次  
  c: 2次
  d: 4次

步骤2:构建哈夫曼树

构建过程

  1. 将每个字符看作一个独立的树,权重为其频率

  2. 选择权重最小的两棵树合并,新节点权重为两者之和

  3. 重复直到只剩一棵树

示例构建

text

复制代码
初始:[(a,7), (b,5), (c,2), (d,4)]
第1步:合并c(2)和d(4) → 新节点(6)
第2步:合并b(5)和新节点(6) → 新节点(11)  
第3步:合并a(7)和新节点(11) → 根节点(18)

步骤3:生成哈夫曼编码

编码规则

  • 左分支标记为0

  • 右分支标记为1

  • 从根到叶子的路径即为该字符的编码

示例编码

text

复制代码
a: 0    (最短,频率最高)
b: 10   (次短)
d: 111  (较长)
c: 110  (最长,频率最低)

步骤4:数据压缩与解压

压缩 :将原文字符替换为对应的哈夫曼编码
解压:根据哈夫曼树从根开始,按位遍历直到叶子节点

4.实现

4.1头文件包含与函数声明

复制代码
#include<iostream>
#include<string>
#include<vector>
#include<unordered_map>
#include<queue>
#include<climits>
#include<algorithm>
using namespace std;

//结点使用三叉链,便于后续找双亲和孩子
struct treeNode
{
	int _weight;//权重
	int _lchild, _rchild, _parent;//左右孩子双亲结点
	//结点初始化
	treeNode()
	{
		_weight = _lchild = _rchild = _parent = 0;
	}
};
//字符对应信息
struct chInfo
{
	char _ch;//字符
	int _freq;//频率
	chInfo()
	{
		_ch = '\0';
		_freq = 0;
	}
	chInfo(char ch, int freq)
	{
		_ch = ch;
		_freq = freq;
	}
};
void select(vector<treeNode>& HuffmanTree, int n, int& node1, int& node2);


//构建哈夫曼树			节点数组       叶子结点个数
void createHuffmanTree(vector<treeNode>& HuffmanTree, int n);
//构建哈夫曼码			每个字符对应一个编码  有效字符个数
void createHuffmanCode(vector<treeNode>& HuffmanTree, vector<string>& HuffmanCode, int n);
//译码
string deHuffmanCode(string& str, vector<treeNode>& HuffmanTree, vector<chInfo>& chs, int n);

(1)结点使用三叉链,存放父亲、左孩子、右孩子的下标以及自身的权重

后续结点下标从1开始,令各变量初始值为0

(2)构建一个类,记录字符及字符出现的频率(权重)

题目以小写字符为主,令各变量初始值为0

(3)声明相关函数

4.2整体框架

复制代码
int main()
{
	string str;
	cin >> str;
	if (str.size() == 0 || str.size() == 1) return 0;//空串、单字符不参与后续


	//哈希表统计字符频率
	int hash[26] = { 0 };
	for (auto ch : str)
		hash[ch - 'a']++;
	//字符类对象数组
	vector<chInfo> chs(1);//有效字符的下标从1开始,与后续对应
	unordered_map<char, int> chMap;
	int num = 1;
	for (int i = 0; i < 26; ++i)//下标从小到大,对应ASCII顺序
	{
		if (hash[i] > 0)
		{
			chs.push_back(chInfo('a' + i, hash[i]));
			chMap['a' + i] = num++;
		}
	}
	//第一行输出频率
	int chs_size = chs.size()-1;
	for (int i = 1; i < chs_size; ++i)
	{
		printf("%c:%d ", chs[i]._ch, chs[i]._freq);
	}
	printf("%c:%d\n", chs[chs_size]._ch, chs[chs_size]._freq);


	//存储结点的数组   叶子节点由chs_size个,总结点由2*chs_size-1个,下标从1开始计数
	//自动调用结点的构造函数,初始时,三叉链、权重均为0
	vector<treeNode> HuffmanTree(2 * chs_size);
	//初始化叶子结点权重,叶子节点的下标为1~chs_size
	for (int i = 1; i <= chs_size; ++i)
	{
		HuffmanTree[i]._weight = chs[i]._freq;
	}
	//构建哈夫曼树
	createHuffmanTree(HuffmanTree, chs_size);
	//打印结构   2n至2n+2行
	for (int i = 1; i <= 2 * chs_size - 1; ++i)
	{
		cout << i << ' ' << HuffmanTree[i]._weight << ' '
			<< HuffmanTree[i]._parent << ' '
			<< HuffmanTree[i]._lchild << ' ' << HuffmanTree[i]._rchild << endl;
	}


	//构建哈夫曼码,共有chs_size个字符
	vector<string> HuffmanCode(chs_size + 1);
	createHuffmanCode(HuffmanTree, HuffmanCode, chs_size);
	//打印各字符对应编码
	for (int i = 1; i < chs_size; ++i)
	{
		printf("%c:%s ", chs[i]._ch, HuffmanCode[i].c_str());
	}
	printf("%c:%s\n", chs[chs_size]._ch, HuffmanCode[chs_size].c_str());
	//打印字符串编码
	string tmpstr;
	for (auto ch : str)
	{
		tmpstr += HuffmanCode[chMap[ch]];
	}
	cout << tmpstr << endl;


	//译码并打印
	cout << deHuffmanCode(tmpstr, HuffmanTree, chs, chs_size) << endl;

	return 0;
}

(1)输入小写字符串,空串、单字符不参与后续

(2)哈希表统计字符频率

字符、下标、频率三个变量后续常用,下标可用数组处理,将字符、频率封装为一个"字符类",那么数组中存储字符类对象,通过下标就可以找到对应字符类对象及其频率,再创建一个哈希表,建立字符与下标间的映射关系,那么通过字符就可以找到下标,进而找到频率

(3)字符类对象数组、后续的节点数组,哈夫曼码数组的对应下标一致

(4)chs_size是叶子结点个数,后续经常用到
(1)叶子结点有chs_size个,则哈夫曼树总结点数为2*chs_size-1个

(2)节点数确定,使用vectoc存储每个结点,注意:结点下标从1开始

(3)叶子节点的下标为1~n,构建出来的节点下标为n+1~2*n-1
(1)叶子结点有chs_size个,哈夫曼码就有chs_size个,注意:结点下标从1开始

4.3构建哈夫曼树

复制代码
void createHuffmanTree(vector<treeNode>& HuffmanTree, int n)
{
	for (int i = n + 1; i <= 2 * n - 1; ++i)
	{
		//数组中选取两个未被使用的结点
		int node1 = 0, node2 = 0;
		select(HuffmanTree, n, node1, node2);
		//新节点只不修改父亲
		HuffmanTree[i]._weight = HuffmanTree[node1]._weight + HuffmanTree[node2]._weight;
		HuffmanTree[i]._lchild = node1;
		HuffmanTree[i]._rchild = node2;
		//叶子节点修改父亲
		HuffmanTree[node1]._parent = i;
		HuffmanTree[node2]._parent = i;
	}
}

(1)哈夫曼树的构建过程就是每次从父亲节点为0的结点中,挑选出权重最小的两个结点组成一个新的节点(父亲节点为0)

(2)新节点的权重等于挑选出来的两节点权重之和,新节点的左右孩子为挑选出来的两个结点(顺序可交换),新节点的父亲不变,仍为0,挑选出来的两个结点的父亲节点就是他们组成的新节点

复制代码
void select(vector<treeNode>& HuffmanTree, int n, int& node1, int& node2)
{
	//直接查找
	int min1 = INT_MAX, min2 = INT_MAX;
	for (int i = 1; i <= 2 * n - 1; ++i)
	{
		if (HuffmanTree[i]._weight == 0)
			break;

		if (HuffmanTree[i]._parent == 0 && HuffmanTree[i]._weight < min1)
		{
			min2 = min1;
			node2 = node1;
			min1 = HuffmanTree[i]._weight;
			node1 = i;
		}	
		else if (HuffmanTree[i]._parent == 0 && HuffmanTree[i]._weight < min2)
		{
			min2 = HuffmanTree[i]._weight;
			node2 = i;
		}
	}
}

在当前数组中挑选出父亲结点为0,且权重最小的两个结点:

node1是父亲结点为0,权重最小的结点,node1是父亲结点为0,权重次小的结点

(1)令min1 为当前查找范围的最小权重min2 为当前查找范围的次小权重

遍历查找过程中:

(2)当前结点的权重为0,意味着遍历到了当前数组的最后一个结点的下一个结点

(3)如果当前结点的父亲结点为0且权重小于min1,更新min1,node1

如果当前结点的父亲结点为0且权重大于min1,小于min2,更新min2,node2

4.4生成哈夫曼码

复制代码
void createHuffmanCode(vector<treeNode>& HuffmanTree, vector<string>& HuffmanCode, int n)
{
	//自底向上遍历每个字符的祖先,所以string是相反的
	for (int i = 1; i <= n; ++i)
	{
		int cur = i, parent = HuffmanTree[i]._parent;
		while (parent != 0)
		{
			if (HuffmanTree[parent]._lchild == cur) HuffmanCode[i] += '0';
			else HuffmanCode[i] += '1';
			cur = parent;
			parent = HuffmanTree[parent]._parent;
		}
		reverse(HuffmanCode[i].begin(), HuffmanCode[i].end());
	}
}

(1)因为左分支标记为0,右分支标记为1,从根到叶子的路径即为该字符的编码且已知叶子节点的下标为1~n,那么我们可以选择从叶子结点向上遍历到根节点,记录遍历的路径,最后再翻转字符串

4.5数据压缩与解压

复制代码
string tmpstr;
for (auto ch : str)
{
	tmpstr += HuffmanCode[chMap[ch] + 1];
}

压缩:遍历所输入的字符串,先根据字符找到下标,再根据下标找到对应字符的哈夫曼码,然后把哈夫曼码添加到输出字符串中

复制代码
string deHuffmanCode(string& str, vector<treeNode>& HuffmanTree, vector<chInfo>& chs, int n)
{
	int len = str.size();
	string tmp;
	int cur = 2 * n - 1;
	for (int i = 0; i < len;)//遍历字符串
	{
				//不是叶子节点,字符串未越界
		while (HuffmanTree[cur]._lchild != 0 && i < len)
		{
			if (str[i] == '0') cur = HuffmanTree[cur]._lchild;
			else cur = HuffmanTree[cur]._rchild;

			++i;
		}
		//字符串正确的话一定不会越界
		tmp += chs[cur]._ch;
		cur = 2 * n - 1;
	}

	return tmp;
}

遍历所压缩的字符串并从哈夫曼树的根节点开始移动,字符零到左子树,字符1到右子树,直到叶子节点,解压出一个字符后,再重复上述步骤

相关推荐
多喝开水少熬夜2 小时前
算法-哈希表和相关练习-java
java·算法·散列表
余衫马2 小时前
聚类算法入门:像魔法一样把数据自动归类
人工智能·算法·机器学习·聚类
CAU界编程小白2 小时前
数据结构系列之快速排序
数据结构·c++·算法
卡提西亚3 小时前
一本通网站1130:找第一个只出现一次的字符
数据结构·c++·笔记·算法·一本通
luoganttcc3 小时前
DiffusionVLA 与BridgeVLA 相比 在 精度和成功率和效率上 有什么 优势
人工智能·算法
CoovallyAIHub3 小时前
注意力机制不再计算相似性?清华北大新研究让ViT转向“找差异”,效果出奇制胜
深度学习·算法·计算机视觉
CoovallyAIHub3 小时前
从图像导数到边缘检测:探索Sobel与Scharr算子的原理与实践
深度学习·算法·计算机视觉
蒙奇D索大3 小时前
【算法】递归算法的深度实践:深度优先搜索(DFS)从原理到LeetCode实战
c语言·笔记·学习·算法·leetcode·深度优先
一匹电信狗4 小时前
【C++11】右值引用+移动语义+完美转发
服务器·c++·算法·leetcode·小程序·stl·visual studio