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

哈夫曼树

哈夫曼树又称最优二叉树,它是由 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;
}
  • 3
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
编码是一种可变字长编码(VLC)的一种。它是由David A. Huffman在1952年发明的。它是一种形结构,在编码过程中,权值较小的叶节点会被编码为较短的比特串,而权值较大的叶节点会被编码为较长的比特串,这样可以实现对数据的高效压缩。下面是哈编码解码的C语言实现: 先定义哈的节点结构体: ```c typedef struct node { int weight; int parent; int lchild; int rchild; }HuffNode, *HuffTree; ``` 接着,定义哈的构建函数,该函数的输入参数为存储字符频率的数组 `weight[]`,以及字符的个数 `n`。 ```c HuffTree createHuffTree(int weight[], int n) { HuffTree huffTree; int m = 2 * n - 1; huffTree = (HuffNode*)malloc((m + 1) * sizeof(HuffNode)); for(int i = 1; i <= n; i++) { huffTree[i].weight = weight[i - 1]; huffTree[i].parent = 0; huffTree[i].lchild = 0; huffTree[i].rchild = 0; } for(int i = n + 1; i <= m; i++) { huffTree[i].weight = 0; huffTree[i].parent = 0; huffTree[i].lchild = 0; huffTree[i].rchild = 0; } int s1, s2; for(int i = n + 1; i <= m; i++) { select(huffTree, i - 1, &s1, &s2); huffTree[s1].parent = i; huffTree[s2].parent = i; huffTree[i].lchild = s1; huffTree[i].rchild = s2; huffTree[i].weight = huffTree[s1].weight + huffTree[s2].weight; } return huffTree; } ``` 其中,`select()`函数用于选择权值最小的两个节点(即叶节点或新节点)。 ```c void select(HuffTree huffTree, int n, int *s1, int *s2) { int i, j; i = 1; while(huffTree[i].parent != 0) i++; *s1 = i; j = i + 1; while(huffTree[j].parent != 0) j++; *s2 = j; if(huffTree[*s2].weight < huffTree[*s1].weight) { int temp = *s1; *s1 = *s2; *s2 = temp; } for(int k = i + 1; k <= n; k++) { if(huffTree[k].parent == 0) { if(huffTree[k].weight < huffTree[*s1].weight) { *s2 = *s1; *s1 = k; } else if(huffTree[k].weight >= huffTree[*s1].weight && huffTree[k].weight < huffTree[*s2].weight) { *s2 = k; } } } } ``` 接下来,是对字符串进行编码的函数,该函数的输入参数为待编码的字符串 `str`,以及哈 `huffTree`。 ```c void encode(char *str, HuffTree huffTree) { printf("编码结果:\n"); while(*str != '\0') { int i; char c = *str; str++; for(i = 1; i <= strlen(huffCode[c]); i++) { printf("%c", huffCode[c][i - 1]); } } printf("\n"); } ``` 其中,`huffCode[]`数组存储了每个字符的哈编码。 最后,是对编码进行解码的函数,该函数的输入参数为待解码的字符串 `str`,以及哈 `huffTree`。 ```c void decode(char *str, HuffTree huffTree) { printf("解码结果:\n"); int p = 2 * strlen(huffCode) - 1; while(*str != '\0') { if(*str == '0') { p = huffTree[p].lchild; } else { p = huffTree[p].rchild; } if(huffTree[p].lchild == 0 && huffTree[p].rchild == 0) { printf("%c", huffTree[p].weight); p = 2 * strlen(huffCode) - 1; } str++; } printf("\n"); } ``` 最终,将上述函数整合在一起即可实现哈编码解码

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

世真

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值