赫夫曼树创建与应用:对文件的编码与解码

赫夫曼树

性质

1.树的路径长度是从数根到每一结点的路径长度之和。
2.如果结点带权重,则结点的带权路径长度为从该结点到数根之间的路径长度与结点上权重的乘积。
3.树的带权路径长度为树中所有叶子结点的带权路径长度之和,通常记为WPL;
最小的二叉树被称为最优二叉树或者赫夫曼树。
4.由于赫夫曼树中没有度为1的结点(这种树又称严格的(或者正则的)二叉树),则一颗有n个叶子结点的赫夫曼树共有2n-1个结点。
二叉树的性质:末端结点的数量为n,则度为2的结点数为n-1。没有度为1的结点,则总结点数为2n-1。

原理

权重最小的两个结点构成一个结点的左右子树,结点权重等于左右子树权重之和,而后依次向根推进,直到全部统计完毕。

前缀编码:
将需要传送的文字转化为二进制的字符组成的字符串。
为了避免二义性,必须设计长短不等的编码,且任一字符的编码都不是另外一个字符的编码的前缀,这种编码被称为前缀编码。

例如:
1,2,3,4,四个数字根据大小按赫夫曼树构造,将变成:
100,101,11,0,这四个编码。

同样,如果将一个文件中字符出现次数按照赫夫曼树构造,比如字符a, b, c, d出现次数分别为1,2,3,4次,则对应编码同样为:
100,101,11,0。

由字符次数构造的赫夫曼树里,可以看到,d出现次数最多,但编码最短,长度为1,带权路径长度为4*1=4,形成最优二叉树

也正因为前缀编码的无二义性,赫夫曼编码可以达到无损传递信息的目的
另外,由于赫夫曼编码子树分支编码为‘0’和‘1’,因此,可以将信息压缩为二进制文件,达到压缩传递文件大小的目的

构造思路说明:
如果统计出要进行编码的字符数以及自身权重,就可以构造一个赫夫曼树。
同样,如果知道编码后的文件内容以及对应的字符编码表,就可以对加密文件进行解码。

赫夫曼编码构造

赫夫曼树

赫夫曼树结点:

typedef struct {
    char ch;//结点储存字符
	int weight;//结点权重
	int parent, lchild, rchild;//父亲结点,左子树,右子树
}HTNode,*HuffmanTree;//动态分配数组储存赫夫曼树

赫夫曼树中,结点结构通过权重比较,构造出赫夫曼树。
权重比较遵循结点权重最小两位构造树结点规则,最小位为0,次位为1。

其中,HTNode是结构结点别名。
HuffmanTree是结构指针。
通过动态分配空间,得到指向结构数组的指针,得到的就是构造后的赫夫曼树

如下为某一读取文件的部分赫夫曼树结点

在这里插入图片描述

赫夫曼编码表

//赫夫曼编码表
typedef char** HuffmanCode;

赫夫曼编码表存储了相应字符的前缀编码。
赫夫曼编码表实际是指向字符串指针数组的指针。它储存了指向前缀编码的指针(char *)。
这样做的原因是,char型指针的大小是一定的因此储存n个字符只需要n * sizeof(char *)大小的内存空间。而前缀编码的长度并不一定,如果按照最大编码长度进行储存,则会有许多内存空间被浪费。
用char指针指向对应的前缀编码,就可以省去大部分空间,尤其是在读取字符类型较多的情况下。

同样的,在得到字符数后,根据构造的赫夫曼树就可以得到字符对应编码。
如下为某一读取文件的部分赫夫曼编码

在这里插入图片描述

创建赫夫曼树

创建赫夫曼树需要知道字符的权重与数量,因此,选要先从文件中读取记录字符数量与统计个数。

读取文件内容生成顺序表

顺序表结构

typedef struct SqList
{
	char ch;//字符
	int weight;//权重
}SqList,*Sq;

//读取文件
int File_to_SQ(const char* filename, Sq& sql);

和上诉赫夫曼树类似,设立动态数组,通过循环统计得到文件内字符数。
实现函数如下:

int File_to_SQ(const char* filename, Sq& sql)
{
	using namespace std;
	ifstream InFile;
	InFile.open(filename);
	if (!InFile.is_open())
	{
		exit(EXIT_FAILURE);
	}
	char ch;
	//设置从1~num ,而不从0~num-1,方便统计
	int count=1;
	//读取会忽略空格,因此要强调不能忽略空格
	while (InFile >>noskipws>> ch)
	{
		count = 1;
		while (sql[count].ch!='\0' && ch != sql[count].ch)
			count++;
		if (sql[count].ch=='\0')
		{
			sql[count].ch = ch;
			sql[count].weight = 1;
		}
		else
		{
			sql[count].weight++;
		}
	}
	count = 1;
	//统计并返回总数
	while (sql[count].ch != '\0')
		count++;
	InFile.close();
	//注意此时指向,需要减去一才能得到正确统计数
	return (count-1);
}

为确保正确性,可在读取完成后,检测:
检测部分代码:

   //函数返回值为文件内总字符数
   int num=File_to_SQ("school-profile.txt", SqL);
	//字符读取测试
	for (int i = 1; i <= num; i++)
	{
		cout << endl<<SqL[i].ch << '\t' << SqL[i].weight << endl;
	}

生成赫夫曼树及编码表

辅助函数:
得到尚未连接到赫夫曼树结点里的权重最小两位在赫夫曼树表中的顺序数。

//其中i为增加后的赫夫曼数表中的最后一位数目的序号。
//需要注意到的是,能执行到这步,i的值至少为2,而保障的至少有两个比较数
void Weight_Select(const HuffmanTree& HT, int i, int& s1, int& s2)
{
	s1 = s2 = 0;
	for (int n = 1; n <= i; n++)
	{
		if (HT[n].parent == 0)
		{
			//两个 if 条件语句,只能执行一个,使得优先让s1成为最小值,
			//让s2继承s1之前的值。
			if (s1 == 0 || HT[n].weight < HT[s1].weight)
			{
				s2 = s1;
				s1 = n;
			}
			else if (s2 == 0 || HT[n].weight < HT[s2].weight)
			{
				s2 = n;
			}
		}
	}
}

创建函数:

通过以及创建好的顺序表SqL以及最大字符数量n,得到赫夫曼树表与赫夫曼编码。

void HuffmanCoding(HuffmanTree& HT, HuffmanCode& HC, const Sq&SQ, int n)
{
	if (n <= 1)
		return;

	//赫夫曼树总结点数
	int m;
	m = 2 * n - 1;
	int i;
	HuffmanTree p;//移动指针
	Sq ps;//创建移动指针
	for (p = HT + 1, i = 0,ps=SQ+1; i < n; ++i, ++p, ++ps)
	{
		*p = { ps->ch,ps->weight,0,0,0 };
		//charArr[i] = ps->ch;
	}
	//i=n
	for (; i <= m; ++i, ++p)
	{
		//储存字符  权重  父节点  左子树  右子树
		*p = { '\0',0,0,0,0 };
	}

	//最小值  第二最小值
	int s1, s2;

	//走到这步n至少等于2,m=2n-1
	for (i = n + 1; i <= m; ++i)
	{
		//从HT+1到HT+i-1中寻找到parent==0的两个最小值下标
		Weight_Select(HT, i - 1, s1, s2);
		
		//两个最小值和i连接成夫结点与左右子树
		HT[s1].parent = i; HT[s2].parent = i;
		HT[i].lchlld = s1; HT[i].rchild = s2;
		HT[i].weight = HT[s1].weight + HT[s2].weight;
	}

	//生成赫夫曼编码
	// HC = new char*[n];
	//赫夫曼编码缓存
	char* code = new char[n];
	code[n - 1] = '\0';

	//HT[1] ~HT[n]
	for (i = 1; i <=n; ++i)
	{
		int start = n - 1;
		int c = i;
		int p = HT[i].parent;
		//从叶子结点向上直到根来得出赫夫曼编码
		//叶子结点的父节点为根
		while (p != 0)
		{
			if (HT[p].lchlld == c)
				code[--start] = '0';
			else
				code[--start] = '1';
			c = p;
			p = HT[p].parent;
		}
		//为每个字符分配赫夫曼编码的储存空间
		//char* am = new char[n - start];
		HC[i - 1] = new char[n- start];
		//复制到对应赫夫曼编码表中
		strcpy_s(HC[i - 1], n - start, &code[start]);
		//delete[] am;
	}
	delete[] code;
	code = NULL;
}

外部设置:
动态分配空间,其中为赫夫曼树表动态分配2\*num个空间(首位空间弃置,结点树为2\*num-1),为赫夫曼编码表分配num个空间内存。

    HuffmanTree HT=new HTNode[2*num]; 
	HT[0]={0,0,0,0,0};
	HuffmanCode HC=new char *[num]();

检测代码:
通过循环,将赫夫曼树表与赫夫曼编码表的内容展示,方便检查。

//赫夫曼树
	cout << "\t\t\t\t----赫夫曼树----\n";
	for (i = 1; i <= 2 * num - 1; i++)
	{
		cout <<"\t\t\t" << HT[i].ch << "\t";
		cout << HT[i].weight << "\t";
		cout << HT[i].parent << '\t';
		cout << HT[i].lchlld << '\t';
		cout << HT[i].rchild << endl;
	}
	cout << "\t\t\t\t----字符的赫夫曼编码----\n";
	cout << "\t\t\tcharacter\tweight\tcode\n";
	for (i = 0; i < num; ++i)
	{
		cout << "\t\t\t\t" << HT[i + 1].ch;
		cout << "\t" << HT[i + 1].weight;
		cout << "\t" << HC[i] << endl;
	}

编码与解码文件

 通过以及得到的赫夫曼树表以及对应编码表,参照原文件进行编码。

编码文件函数

读取原文件的同时,生成编码文件,需要用到赫夫曼树表以及编码表。

void File_to_File(const char* filename, const char* tofilename, const HuffmanCode& HC,const HuffmanTree&HT)
{
	using namespace std;
	ifstream InFile;
	ofstream OutFile;
	InFile.open(filename);
	if (!InFile.is_open())
	{
		exit(EXIT_FAILURE);
	}
	OutFile.open(tofilename);
	//ios_base:out|ios_base:app
	if (!OutFile.is_open())
	{
		exit(EXIT_FAILURE);
	}
	char ch;
	int count = 0;
	//noskipws的作用是使输入流不忽视空格字符
	while (InFile >>noskipws>>ch)
	{
		count = 0;
		while (ch != HT[count + 1].ch)
			count++;
		OutFile << HC[count];
	}
	InFile.close();
	OutFile.close();
}

文件编码结果

编码文件部分结果:
在这里插入图片描述

解码函数

利用前缀编码无二义性的特点,根据编码文件与赫夫曼树的对照,得到解码文件。

void FileB_Decode(const char* Bf, const char* Tf, const HuffmanTree& HT,const int num)
{
	using namespace std;
	ifstream InFile;
	ofstream OutFile;
	//ofstream Tfile;
	InFile.open(Bf);
	if (!InFile.is_open())
	{
		exit(EXIT_FAILURE);
	}
	OutFile.open(Tf);
	//ios_base:out|ios_base:app
	if (!OutFile.is_open())
	{
		exit(EXIT_FAILURE);
	}
	char ch='0';
	int count = 2*num-1;
	while (ch!='\0')
	{
		//从根向叶子找编码符号
		count = 2 * num-1;
		while (HT[count].lchlld != 0 || HT[count].rchild != 0)
		{
			InFile >> ch;
			//输入流失败,代表文件到达末尾,可以通过设置判断语句
			//跳出循环
			if (!InFile)
			{
				ch = '\0';
				break;
			}
			if (ch == '0')
				count = HT[count].lchlld;
			else
				count = HT[count].rchild;
			
		}
		if (ch == '\0')
			break;
		OutFile << HT[count].ch;
	}
	InFile.close();
	OutFile.close();
}

解码文件对比

原文件:

在这里插入图片描述
编码又解码后文件:
在这里插入图片描述

主程序部分

#include<iostream>
#include"header.h"

#define MAXSIZE 100
int main()
{
	using namespace std;
	
	//
	Sq SqL = new SqList[MAXSIZE]();
	int num=File_to_SQ("school-profile.txt", SqL);
	//字符读取测试
	/*for (int i = 1; i <= num; i++)
	{
		cout << endl<<SqL[i].ch << '\t' << SqL[i].weight << endl;
	}*/

	int i;
	HuffmanTree HT=new HTNode[2*num]; 
	HT[0]={0,0,0,0,0};
	HuffmanCode HC=new char *[num]();
	HuffmanCoding(HT, HC, SqL, num);
	//赫夫曼树
	cout << "\t\t\t\t----赫夫曼树----\n";
	for (i = 1; i <= 2 * num - 1; i++)
	{
		cout <<"\t\t\t" << HT[i].ch << "\t";
		cout << HT[i].weight << "\t";
		cout << HT[i].parent << '\t';
		cout << HT[i].lchlld << '\t';
		cout << HT[i].rchild << endl;
	}
	cout << "\t\t\t\t----字符的赫夫曼编码----\n";
	cout << "\t\t\tcharacter\tweight\tcode\n";
	for (i = 0; i < num; ++i)
	{
		cout << "\t\t\t\t" << HT[i + 1].ch;
		cout << "\t" << HT[i + 1].weight;
		cout << "\t" << HC[i] << endl;
	}

	//得到赫夫曼编码文件
	File_to_File("school-profile.txt", "school-profile_binary.txt", HC, HT);
	//赫夫曼解码
	FileB_Decode("school-profile_binary.txt", "school-profile_decode.txt", HT, num);

	//销毁指针
	for (i = 0; i < num; i++)
	{
		delete[] HC[i];
	}
	delete[] SqL;
	//delete[] HC;
    //delete[] HT;
	
	return 0;
}

代码仅供参考,如有错误,敬请指正。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值