数据结构:Huffman树(哈夫曼树)原理及C++实现

前言:

        最近要到期末了,事情有点多有几个星期没有写这个了,我们数据结构实验课要求写Huffman树,因此这次我将对Huffman树进行介绍。

原理:

哈夫曼树是一种单词树,广泛使用于数据压缩之中。将会根据每个字符的权重,来构建一颗Huffman树,同时根据Huffman树对原来的文本进行二次编码,以达到压缩数据的目的。

比如当我们对AAABBBA进行Huffman树压缩时,A的编码将会是1,B的编码将会是0,那么这串字符串将会被压缩为1110001,只需要7个bit即可进行储存,而AAABBBA则至少需要7 * 8个bit,这样我们就可以达到数据 压缩(compress)的目的。当然,当我们预见一个已经压缩好的数据时,我们只需要拥有其对应的编码表,则可将其进行 展开(expend)
这里先给出一个我写的Huffman树的声明:
/* Huffman类:哈夫曼树
 * 接口:
 * MakeEmpty:重置功能,重置哈夫曼树
 * DisSt:展示编码表
 * IsLeaf:判断对应节点是否为叶子节点
 * GetFreq:获取权重数组
 * BuildTree:根据权重数组构建哈夫曼树
 * BuildCode:根据哈夫曼树构建编码表
 * Expend:根据哈夫曼树展开压缩文本
 * Compress:根据编码表压缩原始文本
 */
class Huffman
{
public:
	// 构造函数
	Huffman();
	// 析构函数
	~Huffman();

	// 接口函数
	void MakeEmpty();
	void DisSt();

	bool IsLeaf(Node*);
	void GetFreq(int);
	void BuildTree();
	void BuildCode();

	string Expend(string);
	string Compress(string);

private:
	// 辅助功能函数
	void MakeEmpty(Node *);
	void BuildCode(Node *, string);

	// 数据成员
	const int R = 128;
	int *freq; // 权重数组
	string *st; // 编码表
	Node *Root; // 哈夫曼树根
};

Huffman树节点:
Huffman树的节点中将会储存 字符(ch),字符的 权重(Weight),其 左右子树(Left, Right),值得注意的时,除了叶子节点以外,我们不会储存具体的字符,因为Huffman 我们需要保证Huffman树的编码是前缀编码,因此它必须是一颗满树,同时只能在叶子节点中存储具体的字符
其具体结构如下:
struct Node {
	// 储存元素
	char ch;
	int weigth;
	Node *Left;
	Node *Right;

	// 构造函数
	Node() {
		Left = NULL;
		Right = NULL;
	}

	// 拷贝构造函数
	Node(const Node &P) {
		ch = P.ch;
		weigth = P.weigth;
		if (P.Left == NULL)
			Left = NULL;
		else {
			Left = new Node();
			*Left = *(P.Left);
		}
		if (P.Right == NULL)
			Right = NULL;
		else {
			Right = new Node();
			*Right = *(P.Right);
		}
	}

	// 重载"<="符号
	bool operator <= (const Node &P) {
		return this->weigth <= P.weigth;
	}
}; 
PS:可以发现的是,我们对该节点的"<="符号进行了重置,同时编写了适合于深拷贝的拷贝构造函数,这是因为我们将会使用堆的方法对Huffman树进行构建,因此这些工作是很有必要的。

Huffman树构建:
Huffman树的构建将会使用到经典的Huffman算法,即构建一个最小的Huffman树使,为了使压缩后的文本最小,我们必须先知道(或大致知道)字典中每个字符的多少(即权重),出现越多的字符其编码长度越短。因此我们的Huffman算法将根据树的权重来进行构建。
首先我们将获取字典中所有的字符,以及其对应的权重,分别生成我们的Huffman节点,这些节点将会是树叶,因此其左右子树都为NULL。首先我们将所有的节点存入优先队列也就是堆中(这就是我们之前重载"<="符号以及编写拷贝构造函数的原因,因为 我们将根据节点的权重对其进行排序)。之后我们将存入堆中的节点取出两个,这将是权重最小的两个节点,我们将生成一个新的节点并将其作为取出的两个节点的父节点,该父节点的权重将会是子节点的权重之和,并将其加入堆中。之后重复之前的步骤,直到堆中只剩一个节点,那么该节点将会是我们Huffman树的树根,此时Huffman树构建完成。
具体的可以参考下图:
图中的有四个字符,他们的权重分别为:A(2)、B(11)、C(18)、D(7);我们将根据这些信息构建一颗生成树。

如上图所示,经过三次合并之后,我们将的到对应的Huffman树。注意一个小的细节,我们左子树会是权重更小的树,这也更符合我们二叉树的习惯。

具体代码:
/* 构建函数:根据权重数组构建哈夫曼树
 * 参数:无
 * 返回值:无
 */
void Huffman::BuildTree() {
	// 储存配对堆
	PairHeap<Node> ph;

	// 将所有的字母生成对于的节点存入配对堆中
	for(int c = 0; c < R; c++)
		if (freq[c] > 0) {
			Node *NewNode = new Node();
			NewNode->ch = c;
			NewNode->Left = NewNode->Right = NULL;
			NewNode->weigth = freq[c];
			ph.Insert(*NewNode);
		}

	// 将配对堆中最小的两个节点取出,进行合并,将合并后的节点重新加入配对堆
	// 特别注意:
	//		对于Node,应该处理其拷贝构造函数,因为我们将对其进行深拷贝
	while (ph.size() > 1) {
		Node *x = &ph.Top();
		ph.DeleteMin();
		Node *y = &ph.Top();
		ph.DeleteMin();

		Node *Parent = new Node();
		Parent->ch = '\0';
		Parent->Left = x;
		Parent->Right = y;
		Parent->weigth = x->weigth + y->weigth;
		ph.Insert(*Parent);
	}

	// 储存根节点同时特殊处理,防止因数据析构而消失
	Root = new Node();
	*Root = ph.Top();
	ph.DeleteMin();
}

PS:代码中的PairHeap是我自己写的一个配对堆,代码在这次的blog中并没有贴出来,如果有需要可以给我留言;或者我会在以后的blog中写出来;当然也可以使用其他的优先队列,当然可能会有所不同,具体在Huffman树的Node节点重载需要重新写一下。

编码:

在我们完成了Huffman树的构建后,我们可以开始构建我们的编码表。这里有个规则:从树根开始,右子树编码为1,左子树编码为0。那么对于图四我们将有如下的编码:A(100),B(11),C(0),D(101);那么我们如何获得这些编码呢,一个简单的方法是 对树进行遍历,同时记录当前的遍历路径,当我们检索到叶子节点的时候根据其存储字符以及路径编写其对应的编码
代码很简单,具体如下:
/* 编码函数:根据哈夫曼树构建编码表
 * 参数:无
 * 返回值:无
 */
void Huffman::BuildCode() {
	if(Root != NULL)
		BuildCode(Root, "");
}

/* 编码函数:根据对应节点信息进行编码
 * 参数:T:当前的编码节点,s:当前的编码信息
 * 返回值:无
 */
void Huffman::BuildCode(Node *T, string s) {
	// 递归终止条件
	if (IsLeaf(T)) {
		st[T->ch] = s;
		return;
	}

	// 递归的对下一层节点进行编码
	BuildCode(T->Left, s + '0');
	BuildCode(T->Right, s + '1');
}

展开和压缩:
当我们有了Huffman树和编码表后,我们即可进行展开和压缩操作。
压缩我们将根据Huffman树进行,当我们读入一个压缩文本的时候,我们将从树根处开始遍历,若读入'0'我们将遍历其左子树,读入'1'遍历其右子树,同时读入文本的下一位。若当前处理的节点为叶子节点,那么我们就会暂时储存其存储的字符,同时也将压缩文本读入下一位,直到压缩文本读完,以此输出所有储存字符。

展开操作将根据编码表进行,这将比压缩更加简单。首先是我们需要有其所对应的编码表,之后我们将从原始文本的开头开始读入字符,然后再编码表中找到对应的字符,同时存储其对应的编码。直到所有的字符读入,最后输出我们的压缩编码。
具体代码如下:
/* 展开函数:根据哈夫曼树展开压缩文本
 * 参数:txt:想要进行展开的压缩文本
 * 返回值:string:压缩文本展开后的字符串
 */
string Huffman::Expend(string txt) {
	// 储存展开结果
	string re;

	// 遍历整个目标文本
	for (int i = 0; i < txt.length(); ) {
		if (txt[i] == '#')
			break;

		Node *x = Root;

		// 获取压缩信息
		while (!IsLeaf(x)) {
			if (txt[i] == '0')
				x = x->Left;
			else 
				x = x->Right;
			i++;
		}

		// 更新结果文本
		re += x->ch;
	}

	return re;
}

/* 压缩函数:根据编码表压缩原始文本
 * 参数:txt:想要进行压缩的原始文本
 * 返回值:string:原始文本压缩后的字符串
 */
string Huffman::Compress(string txt) {
	// 储存压缩结果
	string re;

	// 遍历原始文本同时读取编码表
	for (int i = 0; i < txt.length(); i++)
		re += st[txt[i]];

	return re;
}

那么这次的Huffman树到这里就差不多结束了,之后我会补全其完整代码。
转载请说明出处哦~~

参考文献:《算法——第四版》

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页