二叉树的应用_哈夫曼树(C++)_静态

哈夫曼编码简介

  • 哈夫曼树是哈夫曼编码的基础,它在我们的生活中有着广泛的应用,其中最典型的就是文件压缩。
  • 为什么利用哈夫曼编码可以起到压缩文件的效果呢?
    这就涉及到哈夫曼编码的原理了,我们首先来看一些基本概念。
    脑图
  • 咱们来看个实例,直观感受一下等长编码与不等长编码的区别。
    两种编码方式比较

我们可以看出

  1. 不等长编码,就是文件压缩的核心之一。
  2. 在存储编码的时候可以采用字符编码与bit编码(二进制文件)两种方式,其中bit编码更节省空间。
  3. 在编码过程中,为了避免产生二义性,所有字符对应的编码都不可以是其他字符的前缀。

哈夫曼树

简介

哈夫曼树是一种用来进行哈夫曼编码的方法。
哈夫曼树
我们知道,对于同一组叶结点,使用不同结构的二叉树,得到的带权路径长度是不同的。如下图:

例题

哈夫曼树的构造

  • 问题:若一组叶结点的权值已知,如何构造哈夫曼树?(即构造带权路径最短的二叉树)
    思路: 让权值越大的叶结点越靠近根节点,权值越小就越原离根节点。

方法:(从下到上构造)

  1. 选择有效权值最小的两个,构成最小二叉树,标记这两个权值已使用。
  2. 将这两个权值相加,其和并入权值表,返回步骤1
    当权值表中的所有值都使用过,结束循环。
    1
    2

利用哈夫曼树进行编码

编码

哈夫曼树的实现

哈夫曼树的存储和建立

  • 哈夫曼树的特点:结点的度只有2和0,所以有n个叶结点的哈夫曼树总结点数一定是 2*n-1.
  • 顺序存储结构(设置一个huffTree[2*n-1]数组)
    结点
struct HNode
{
  int weight;//权值
  int lchild,rchild,parent;
};

哈夫曼编码表的存储和建立

建立

struct HCode
{
  char data;
  string code;
};

哈夫曼编码

编码

哈夫曼解码

解码

代码

程序框架
  1. Initial():对输入的字符串进行词频统计,并据此初始化数据成员。
  2. DouMin():求出可使用的树节点中,权值最小的两个。
  3. Reserve():实现编码的逆置(因为创建编码表时是从叶结点向上遍历的)
  4. Huffman():创建Huffman树并打印表格,表格表示Huffman树的建立过程。
  5. CreateCodeTable():创建并打印编码表。
  6. Encode():编码字符串,并打印结果。
  7. Decode():是Encode()函数的逆过程,对其结果进行解码。
  8. Analyze():自动分析Huffman编码的效果
  9. ~Huffman():析构函数,释放堆空间。
声明
//Huffman编码类
class Huffman
{
private:
	TNode *node;//树结点
	CNode *codeTable;//存储编码表
	int length;//字符串长度
	int N; //叶结点数量=不同字符种类
	string str;//存储编码后的字符串
	int en_length;//存储编码后字符串长度
	char name[128];//将string转化为字符数组并存储
	void Initial();//对输入的字符串进行频次统计,初始化相应的参数
	void DouMin(int &x, int &y,int a,int b);//取权值最小的两个结点
	void Reverse(string &s);//逆置编码
public:
	Huffman();//创建哈夫曼树并打印
	void CreateCodeTable();//创建并打印编码表
	void Encode();//编码,并打印结果
	void Decode();//解码,并打印结果
	void Analyze();//分析Huffman编码的效果
	~Huffman();//析构,释放堆空间
};
构造哈夫曼树

基本思想:

  • 利用不同符号的ASCII码作为索引来进行词频的统计。根据统计结果对数据成员进行堆空间的申请和初始化赋值。
//对输入的字符串进行频次统计,初始化相应的参数
void Huffman::Initial()
{//时间复杂度:O(n)
	length = 0;
	int nNum[256] = { 0 };
	int ch = cin.get();
	//初始化length和name[]
	while ((ch != '\r') && (ch != '\n'))
	{
		nNum[ch]++;//利用ACSII码统计频次
		name[length] = (char)ch;//存储字符串中每个字符
		length++;
		ch = cin.get();//读取下一个字符
	}
	//初始化N--字符种类
	N = 0;
	for (int i = 0; i < 256; i++)
	{
		if (nNum[i] != 0)N++;
	}
	//初始化树结点和编码表结点
	node = new TNode[2 * N - 1];//构建Huffman树结点
	codeTable = new CNode[N];//创建编码表结点
	int i = 0;
	for (int j = 0; j < 256; j++)
	{
		if (nNum[j] != 0)
		{
			node[i].weight = nNum[j];
			node[i].lchild = -1;//初始化结点,标记为未使用
			node[i].rchild = -1;
			node[i].parent = -1;
			codeTable[i].data = (char)j;
			codeTable[i].code = "";
			i++;
		}
	}
}

//取权值最小的两个结点
void Huffman::DouMin(int &x,int &y,int a,int b)
{//时间复杂度:O(n)
	struct tempNode
	{
		int weight;//权重
		int position;//位置
	};
	tempNode *temp=new tempNode[b];
	int j = 0;
	for (int i = a; i < b; i++)
	{//选出所有可使用的结点并用temp[]存储
		if (node[i].parent == -1)
		{
			temp[j].weight = node[i].weight;
			temp[j].position = i;
			j++;
		}
	}
	int m1 = 0 , m2 = 0;
	//求第一个最小值
	for (int i = 0; i < j; i++)
		if (temp[i].weight < temp[m1].weight)
			m1 = i;
	//求第二个最小值	
	for (int i = 0; i < j; i++)
	{
		if (m1 == m2) 
			m2++;
		if (temp[i].weight <= temp[m2].weight)//注意这里要取等号
			m2 = i;
	}
	x = temp[m1].position;
	y = temp[m2].position;
	delete[]temp;
}

//创建哈夫曼树并打印
Huffman::Huffman()
{//时间复杂度:O(n)
	Initial();//初始化哈夫曼树相关数据成员
	if (N <= 1)//异常处理
		cout<<"Error!Cannot create a Huffman tree!";
	int x=0, y=0;
	for (int i = N; i < 2 * N - 1; i++)
	{//创建Huffman树(静态)
		DouMin(x, y,0,i);//选出权值最小的两个结点,且x<y
		node[x].parent = i;
		node[y].parent = i;
		node[i].weight = node[x].weight + node[y].weight;
		node[i].lchild = x;
		node[i].rchild = y;
		node[i].parent = -1;
	}
	cout << "Huffman tree: " << endl;
	//打印Huffman树的静态表格
	for (int i = 0; i < 2 * N - 1; i++)
	{
		cout << "-----------------------" << endl;
		cout << setiosflags(ios::left) << setw(3) << i << "│" << " " << setw(3) << node[i].weight
			<< "│" << " " << setw(3) << node[i].lchild << "│" << " " << setw(3) << node[i].rchild
			<< "│" << " " << setw(3) << node[i].parent << endl;
	}
	cout << "-----------------------" << endl;
}
构造哈夫曼编码表

基本思想:
从叶结点开始依次向上得到编码,再通过函数将其逆置,从而得到正确的编码。最后打印编码表。

//逆置编码
void Huffman::Reverse(string &s) 
{//时间复杂度:O(n)
	int length = s.size();
	char temp;
	for (int i = 0; i < length / 2; i++) 
	{
		temp = s[i];
		s[i] = s[length - i - 1];
		s[length - i - 1] = temp;
	}
}

//创建并打印编码表
void Huffman::CreateCodeTable()
{//时间复杂度:O(n^2)
	cout << "The code table is: " << endl;
	for (int i = 0; i < N; i++)
	{
		int child = i;//从叶结点开始编码,再逆置编码
		int parent = node[i].parent;
		int temp = 0;
		while (node[child].parent != -1)//判断是否为根结点
		{
			if (child == node[parent].lchild)//左孩子,编码取0
				codeTable[i].code += '0';
			else if(child == node[parent].rchild)//右孩子,编码取1
				codeTable[i].code += '1';
			temp = child;
			child = parent;
			parent = node[temp].parent;
		}
		Reverse(codeTable[i].code);//逆置
		//打印编码表
		cout << "------------" << endl;
		cout << setiosflags(ios::left) << ' ' << setw(4) << codeTable[i].data
			<< "│" << ' ' << setw(4) << codeTable[i].code << endl;
	}
	cout << "------------" << endl;
}
编码

基本思想:
在初始化函数中已经将输入的字符串存储在name[ ]数组中。遍历name[ ]数组,根据编码表中字符与字符编码的对应关系,生成Huffman编码。

//编码,并打印结果
void Huffman::Encode()
{//时间复杂度:O(n)
	string en_str;//编码结果
	for (int i = 0; i < length; i++)
	{
		for(int j = 0; j < N; j++)
		{
			if (name[i] == codeTable[j].data)
			{
				en_str += codeTable[j].code;
			}
		}
	}
	str = en_str;
	cout << en_str << endl;
	en_length = en_str.length();
}
解码

基本思想:
Encode()函数的逆过程。遍历字符串str,根据编码表中字符编码与字符的对应关系,求出原始的字符串。

void Huffman::Decode()
{//时间复杂度:O(n)
	string de_str;
	int x = str.length();
	for (int i = 0; i < x; i++)
	{
		int parent = 2 * N - 2;//根节点在静态哈夫曼树表中的索引
		int j = 0;
		while (node[parent].lchild != -1)//不是叶结点
		{
			if (str[i + j] == '0')
				parent = node[parent].lchild;
			else
				parent = node[parent].rchild;
			j++;
		}
		i += j - 1;
		de_str += codeTable[parent].data;
	}
	cout << de_str << endl;
}
析构
//析构,释放堆空间
Huffman::~Huffman()
{//时间复杂度:O(1)
	delete[]node;
	delete[]codeTable;
}
完整代码

你也可以直接在我的Github下载

头文件
#pragma
#include<iostream>
#include<string>
#include<iomanip>
using namespace std;

//树结点
struct TNode
{
	int weight;//权值
	int lchild, rchild, parent;
};
//编码表结点
struct CNode
{
	char data;//被编码内容
	string code;//编码
};

//Huffman编码类
class Huffman
{
private:
	TNode *node;//树结点
	CNode *codeTable;//存储编码表
	int length;//字符串长度
	int N; //叶结点数量=不同字符种类
	string str;//存储编码后的字符串
	int en_length;//存储编码后字符串长度
	char name[128];//将string转化为字符数组并存储
	void Initial();//对输入的字符串进行频次统计,初始化相应的参数
	void DouMin(int &x, int &y,int a,int b);//取权值最小的两个结点
	void Reverse(string &s);//逆置编码
public:
	Huffman();//创建哈夫曼树并打印
	void CreateCodeTable();//创建并打印编码表
	void Encode();//编码,并打印结果
	void Decode();//解码,并打印结果
	void Analyze();//分析Huffman编码的效果
	~Huffman();//析构,释放堆空间
};

//对输入的字符串进行频次统计,初始化相应的参数
void Huffman::Initial()
{//时间复杂度:O(n)
	length = 0;
	int nNum[256] = { 0 };
	int ch = cin.get();
	//初始化length和name[]
	while ((ch != '\r') && (ch != '\n'))
	{
		nNum[ch]++;//利用ACSII码统计频次
		name[length] = (char)ch;//存储字符串中每个字符
		length++;
		ch = cin.get();//读取下一个字符
	}
	//初始化N--字符种类
	N = 0;
	for (int i = 0; i < 256; i++)
	{
		if (nNum[i] != 0)N++;
	}
	//初始化树结点和编码表结点
	node = new TNode[2 * N - 1];//构建Huffman树结点
	codeTable = new CNode[N];//创建编码表结点
	int i = 0;
	for (int j = 0; j < 256; j++)
	{
		if (nNum[j] != 0)
		{
			node[i].weight = nNum[j];
			node[i].lchild = -1;//初始化结点,标记为未使用
			node[i].rchild = -1;
			node[i].parent = -1;
			codeTable[i].data = (char)j;
			codeTable[i].code = "";
			i++;
		}
	}
}

//取权值最小的两个结点
void Huffman::DouMin(int &x,int &y,int a,int b)
{//时间复杂度:O(n)
	struct tempNode
	{
		int weight;//权重
		int position;//位置
	};
	tempNode *temp=new tempNode[b];
	int j = 0;
	for (int i = a; i < b; i++)
	{//选出所有可使用的结点并用temp[]存储
		if (node[i].parent == -1)
		{
			temp[j].weight = node[i].weight;
			temp[j].position = i;
			j++;
		}
	}
	int m1 = 0 , m2 = 0;
	//求第一个最小值
	for (int i = 0; i < j; i++)
		if (temp[i].weight < temp[m1].weight)
			m1 = i;
	//求第二个最小值	
	for (int i = 0; i < j; i++)
	{
		if (m1 == m2) 
			m2++;
		if (temp[i].weight <= temp[m2].weight)//注意这里要取等号
			m2 = i;
	}
	x = temp[m1].position;
	y = temp[m2].position;
	delete[]temp;
}

//创建哈夫曼树并打印
Huffman::Huffman()
{//时间复杂度:O(n)
	Initial();//初始化哈夫曼树相关数据成员
	if (N <= 1)//异常处理
		cout<<"Error!Cannot create a Huffman tree!";
	int x=0, y=0;
	for (int i = N; i < 2 * N - 1; i++)
	{//创建Huffman树(静态)
		DouMin(x, y,0,i);//选出权值最小的两个结点,且x<y
		node[x].parent = i;
		node[y].parent = i;
		node[i].weight = node[x].weight + node[y].weight;
		node[i].lchild = x;
		node[i].rchild = y;
		node[i].parent = -1;
	}
	cout << "Huffman tree: " << endl;
	//打印Huffman树的静态表格
	for (int i = 0; i < 2 * N - 1; i++)
	{
		cout << "-----------------------" << endl;
		cout << setiosflags(ios::left) << setw(3) << i << "│" << " " << setw(3) << node[i].weight
			<< "│" << " " << setw(3) << node[i].lchild << "│" << " " << setw(3) << node[i].rchild
			<< "│" << " " << setw(3) << node[i].parent << endl;
	}
	cout << "-----------------------" << endl;
}

//逆置编码
void Huffman::Reverse(string &s) 
{//时间复杂度:O(n)
	int length = s.size();
	char temp;
	for (int i = 0; i < length / 2; i++) 
	{
		temp = s[i];
		s[i] = s[length - i - 1];
		s[length - i - 1] = temp;
	}
}

//创建并打印编码表
void Huffman::CreateCodeTable()
{//时间复杂度:O(n^2)
	cout << "The code table is: " << endl;
	for (int i = 0; i < N; i++)
	{
		int child = i;//从叶结点开始编码,再逆置编码
		int parent = node[i].parent;
		int temp = 0;
		while (node[child].parent != -1)//判断是否为根结点
		{
			if (child == node[parent].lchild)//左孩子,编码取0
				codeTable[i].code += '0';
			else if(child == node[parent].rchild)//右孩子,编码取1
				codeTable[i].code += '1';
			temp = child;
			child = parent;
			parent = node[temp].parent;
		}
		Reverse(codeTable[i].code);//逆置
		//打印编码表
		cout << "------------" << endl;
		cout << setiosflags(ios::left) << ' ' << setw(4) << codeTable[i].data
			<< "│" << ' ' << setw(4) << codeTable[i].code << endl;
	}
	cout << "------------" << endl;
}

//编码,并打印结果
void Huffman::Encode()
{//时间复杂度:O(n)
	string en_str;//编码结果
	for (int i = 0; i < length; i++)
	{
		for(int j = 0; j < N; j++)
		{
			if (name[i] == codeTable[j].data)
			{
				en_str += codeTable[j].code;
			}
		}
	}
	str = en_str;
	cout << en_str << endl;
	en_length = en_str.length();
}

//解码,并打印结果
void Huffman::Decode()
{//时间复杂度:O(n)
	string de_str;
	int x = str.length();
	for (int i = 0; i < x; i++)
	{
		int parent = 2 * N - 2;//根节点在静态哈夫曼树表中的索引
		int j = 0;
		while (node[parent].lchild != -1)//不是叶结点
		{
			if (str[i + j] == '0')
				parent = node[parent].lchild;
			else
				parent = node[parent].rchild;
			j++;
		}
		i += j - 1;
		de_str += codeTable[parent].data;
	}
	cout << de_str << endl;
}

//分析Huffman编码的效果
void Huffman::Analyze()
{
	cout << "编码前字符串所占内存(单位:bit):" << length*8<<endl;
	cout << "编码后字符串所占内存(不采用二进制编码,单位:bit):" << en_length*8<<endl;
	cout << "编码后字符串所占内存(假设采用二进制编码,单位:bit):" << en_length<<endl;
	cout << "结论:只有采用二进制编码才有压缩效果。" << endl;
}

//析构,释放堆空间
Huffman::~Huffman()
{//时间复杂度:O(1)
	delete[]node;
	delete[]codeTable;
}
main函数
#include"Huffman.h"
using namespace std;
int main()
{
	cout << "请输入待编码字符:";
	Huffman huff;
	huff.CreateCodeTable();
	cout << "编码结果:" ;
    huff.Encode();
	cout << "解码结果:";
	huff.Decode();
	cout << endl;
	huff.Analyze();
	cout << endl;
}
不足与反思
  • 没有真正实现文件的压缩,只是从形式上体现了Huffman的原理和方法。迭代方向:使用二进制进行编码。
  • 交互性不强,无法实现编码的传送。迭代方向:利用文件读写,将编码表与编码都写入文件,再将此文件发送给有解码程序的用户,即可实现交互。
    关于上述两个问题,我会尽量抽时间来解决。如果你有好的想法,欢迎分享~

希望这篇文章可以帮助你理解Huffman编码,如果有任何疑问或者建议,欢迎评论或私聊我。
一起学习,一起进步~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值