哈夫曼编码实现字符串的压缩与解压(C语言)(数据结构大作业)

前言

本文章为作者数据结构大作业的总结,附有源码和思路,附实验报告,各位学数据结构的本科生可以直接拿走食用,大一新生可以先收藏备用,记得点赞关注---会说话的锅

一. 实现步骤

输入字符串

--->统计字符串中各个字符出现的频率

--->将字符串出现的频率视为哈夫曼树节点的权重来构建哈夫曼树

--->根据生成的哈夫曼树得到每个字符所对应的哈夫曼编码

--->将输入的字符串根据对应的哈夫曼编码转换为二进制位(压缩)

--->计算压缩前后所占字节大小

--->将压缩后的二进制码还原为字符串(解压)

二. 实现原理

原本字符串的储存一个字符需要占据一个字节也就是八个二进制位的内存,字符串的字符出现频率越高,它的哈夫曼编码就越短,将字符串中的字符储存的二进制位替换为它所对应的哈夫曼编码,就能减少储存需要占用的内存大小。

举个栗子,我想要压缩一段字符串aaabbc,这段字符串长度为6,算上最后的\0也就是占七个字节的空间,我们求出它所对应的哈夫曼编码,a:0  b:11 c:101。把其换成哈弗曼编码储存为00011101,只占用八个二进制位,也就是一个字节,明显减少了储存这段字符串需要的内存大小。

三.实现主要步骤

1.哈夫曼树的建立以及哈夫曼编码的获取

哈夫曼树

哈夫曼树的基本概念

路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径

结点的路径长度:两结点间路径上的分支数。

树的路径长度:从树根到每一个结点的路径长度之和。记作TL

结点树目相同的二叉树中,完全二叉树是路径长度最短的二叉树。

创建图解

哈夫曼编码

哈夫曼编码的原理

哈夫曼编码的原理可以通过以下步骤来解释:

统计频率:首先,需要统计待编码数据中每个符号的出现频率。符号可以是字符、字节或其他数据单元。统计频率可以通过遍历整个数据集来完成,并记录每个符号出现的次数。

构建编码树:根据符号的频率构建一个特殊的二叉树,称为哈夫曼树(Huffman Tree)或编码树。构建编码树的方法是将频率最低的两个符号合并为一个新节点,该节点的频率为两个节点频率之和。将新节点插入到已有节点的集合中,重复这个步骤,直到只剩下一个节点,即根节点为止。在构建过程中,可以使用优先队列或最小堆来维护频率最低的节点。

分配编码:从根节点开始,沿着左子树走为0,沿着右子树走为1,将0和1分别分配给左右子节点。重复这个过程,直到遍历到每个叶子节点为止。每个叶子节点的路径上的0和1的序列就是对应符号的哈夫曼编码。

生成编码表:将每个符号及其对应的哈夫曼编码存储在一个编码表中,以备后续的编码和解码使用。

进行编码:将原始数据中的每个符号替换为其对应的哈夫曼编码,生成压缩后的编码数据。由于频率高的符号具有较短的编码,而频率低的符号具有较长的编码,所以整个编码后的数据长度会相对减小。

哈夫曼编码的优点是没有冗余和歧义性,即每个编码都不是其他编码的前缀,这种性质被称为前缀码。这使得编码和解码过程都是非常高效的。然而,对于哈夫曼编码的最佳性能,符号的频率应该是根据数据集的统计特征进行调整的。

2.将字符串进行压缩和解压

压缩过程

在这里我并没有真实地去压缩和解压一个文件,我只是将一串字符串压缩后的二进制编码进行打印,压缩的原理很简单,就是遍历我所输入的字符串,找到每个字符对应的哈夫曼编码,依次将哈夫曼编码打印出来

解压过程

解压过程的原理其实和压缩过程大差不差,也是通过遍历去实现,举个栗子,我要解压这么一段二进制编码:01110100,哈夫曼树中字符与二进制码对应为:a:0  b:11 c:101,这里我实现的过程中创建了一个start指针和一个end指针,刚开始的时候我的start指针和end指针都指向第一位,这时候就只读取一位‘0’,看一看有没有字符对应的哈夫曼编码长度也是1?有a:0,判断一下是不是我读取的,是,那我就输入字符'a',读取成功之后end指针向后移动一位,start指针指向end指针指向的位置,读取后面的二进制码,现在读取一位‘1’,重复上述操作,没有字符哈夫曼编码是1,那就让end指针后移一位,读取两位‘11’读取出来字符'b'。以此类推知道将整段二进制码都读取完。

四. 主要步骤的代码具体实现

1.哈夫曼树的建立

//在创建哈夫曼树的时候,首先需要找到最小的两个数,先“结合”在一起;
//在这里huffsort函数就是用来找出最小的两个数并将他们的值的位置s1,s2返回;
void huffsort(huffman& ht, int& s1, int& s2, int n)
{
	int min;
	int i;
//这里我的min没有进行初始化,就先找第一个值的位置赋值给min;
	for (i = 1; i <= n; i++)
	{
		if (ht[i].parent == 0)
		{
			min = i; break;
		}
	}
//我让每个父节点为0的节点权值和min作比较,如果比min还小,那就取而代之;
	for (i = min + 1; i <= n; i++)
	{
		if (ht[i].parent == 0 && ht[i].node.fre < ht[min].node.fre)
		{
			min = i;
		}
	}
//最后用s1来返回最小的那个值的位置;
	s1 = min;
//现在要找倒数第二小的那个值,原理和找最小的一样,就是把最小的那个数剔除掉就可以了;
	for (i = 1; i <= n; i++)
	{
		if (ht[i].parent == 0 && i != s1)
		{
			min = i; break;
		}
	}
	for (i = min + 1; i <= n; i++)
	{
		if (ht[i].parent == 0 && ht[i].node.fre < ht[min].node.fre && i != s1)
		{
			min = i;
		}
	}
//把倒数第二小的值的位置上用s2返回出来;
	s2 = min;
}
//下面是创建哈夫曼树的函数;
void creathuff(huffman& ht, bloo& node)
{
//bloo结构体中存放的是26个字符以及其出现频率;
	int n = 26;
	int m = 2 * n - 1; 
	int i = 1;
//这里我申请了2n块大小为huffman的内存,因为n个叶子节点的哈夫曼树;
//一共有2n-1个节点,这里我第0个节点没有存储东西,所以我就申请了2n个;
	ht = (huffman)malloc(sizeof(link) * (m + 1));
	for (i = 0; i <= 2 * n - 1; i++)
	{
//这里是初始化前26个节点,也就是26个叶子节点;
//让叶子节点的node.ch中存储对应字符,node.fre中存储频率;
		if (i <= n && i != 0)
		{
			ht[i].node.ch = node[i - 1].ch;
			ht[i].node.fre = node[i - 1].fre;
		}
//不是叶子节点的那些节点的node.ch中不存储字符,就初始化存储一个'#',node.fre初始化为0;
		else
		{
			ht[i].node.ch = '#';
			ht[i].node.fre = 0;
		}
//将所有节点的父节点,左孩子,右孩子都初始化为0;
		ht[i].parent = 0;
		ht[i].lchild = 0;
		ht[i].rchild = 0;
	}
//初始化完成之后,下一步就是创建我们的哈夫曼树了
	for (i = n + 1; i <= m; i++)
	{
		int s1, s2;  //s1 s2用于存储最小的两个节点
		huffsort(ht, s1, s2, i - 1);  //返回最小的两个值
		ht[s1].parent = i; 
		ht[s2].parent = i;
		ht[i].lchild = s1;
		ht[i].rchild = s2;
		ht[i].node.fre = ht[s1].node.fre + ht[s2].node.fre; //新节点权重是左右孩子之和
	}
}

  2.找到对应的哈夫曼编码

//获取哈夫曼编码
void huffcode(huffman& ht)
{
	for (int i = 1; i <= 26; i++)
	{
//我是将哈夫曼编码先储存在cd中的,这里我逆序储存哈夫曼编码,最后一位存取\0
//第一次读取到的数放在\0前面,第二次存在第一次的数前,以此类推
//在这里26个叶子结点的二叉树深度最多是25,因此我申请的cd的长度为26(25+\0)
		char* cd = (char*)malloc(sizeof(char) * 26);
		cd[26 - 1] = '\0';
		int start = 26 - 1;
		int c = i;
		int p = ht[c].parent;
		while (p != 0)
		{
			if (ht[p].lchild == c)
			{
				cd[--start] = '0';
			}
			if (ht[p].rchild == c)
			{
				cd[--start] = '1';
			}
			c = p; p = ht[p].parent;
		}
//用node.hufflong来存储字符的哈夫曼编码的二进制位数,以便后续统计大小和截取哈夫曼编码的长度;
		ht[i].node.hufflong = 0;
		for (int j = start; j < 26; j++)
		{
			ht[i].node.hufflong++;
		}
		ht[i].node.hufflong--;
//截取cd中的后hufflong位,就是该字符所对应的哈夫曼编码,将其保存在ht[i].code中
		ht[i].code = (char*)malloc(sizeof(char) * (ht[i].node.hufflong + 1));
		for (int j = start; j < 26; j++)
		{
			ht[i].code = &cd[start];
		}
//将每个字符的哈夫曼编码和占用二进制位数打印出来;
		printf("\n%c字符对应的哈夫曼编码为%s,占用%d个二进制位", ht[i].node.ch, ht[i].code,ht[i].node.hufflong);
	}
}

3.将原来的字符串压缩

在这里我仅仅是将压缩后的二进制码打印出来,并没有真正的去将字符串压缩。

//这里就是简单的遍历方法,读取字符串第i位,判断这是哪个字符,然后打印该字符的哈夫曼编码
void transhuff(char *str, huffman& ht, int size)
{
	for (int i = 0; i < size; i++)
	{
		for (int j = 1; j <= 26; j++)
		{
			if (str[i] == ht[j].node.ch)
			{
				printf("%s", ht[j].code);
			}
		}
	}
}

4.解压过程

解压过程需要输入你要解压的二进制码,这个二进制码可以是上述我们压缩出来的二进制码,也可以是我们自己对照着哈夫曼编码随意输入的,这一块就是将输入的二进制码翻译为字符串。

void retrans(huffman& ht, int huffsize)
{
	char* hcode;
//之前翻译出来的Huffman编码一共占huffsize个二进制位,这里我就也就来翻译这么多位二进制位
	hcode = (char*)malloc(sizeof(char) * huffsize);
	printf("\n输入你要解压的二进制码:");
	for (int i = 0; i < huffsize; i++)
	{
		scanf_s("%c", &hcode[i]);
	}
	printf("\n读取成功\n");
//从这儿开始开始翻译二进制码,这里我创建了一个start指针和一个end指针,开始时start指针和end指针都指向第一位,也就是现在我就读取一位,看看有没有字符的哈夫曼编码是一位的,如果一样,是不是我想要的这个,如果是,那就读取出来,end指针后移一位,start指针也来到end指针的位置,如果不一样,那就继续往下读,读两位,也就是只让end指针后移一位,start指针不动。这样一直到最后一位就能读取所有的字符。
	for (int end = 0, start = 0; end < huffsize; end++)
	{
		int count = 0;
		int nodelength = end - start + 1;
		for (int i = 1; i <= 26; i++)
		{
			if (nodelength == ht[i].node.hufflong)
			{
				int whetherfind = 0;
				for (int j = 0, count = 0; j < nodelength; j++)
				{
					if (hcode[start + j + 1] == ht[i].code[j])
					{
						count++;
					}
					if (count == nodelength)
					{
						printf("%c", ht[i].node.ch);
						end++;
						start = end;
						whetherfind = 1;
						break;
					}
				}
				if (whetherfind == 1) 
				{ 
					break;
				}
			}
		}
	}
}

五. 程序源代码

这里我使用的编译器为visual studio2022,这个版本编译器中输入scanf需要改为scanf_s才能够运行,如果使用的是其他的编译器,需要将我程序中的scanf_s手动将后面的_s去掉。

//用哈夫曼编码对一串小写字母字符串进行压缩
#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
typedef struct blo
{
	int fre;
	char ch;
	int hufflong = 0;
}blo, * bloo;
typedef struct link
{
	blo node;
	int parent;
	int lchild;
	int rchild;
	char *code;
}link, * huffman;
void initblo(bloo& node)
{
	node = (bloo)malloc(sizeof(blo) * 26);
	for (int i = 0; i < 26; i++)
	{
		node[i].fre = 0;
	}
	for (int i = 0; i < 26; i++)
	{
		node[i].ch = 'a' + i;
	}
}
void huffsort(huffman& ht, int& s1, int& s2, int n)
{
	int min;
	int i;
	for (i = 1; i <= n; i++)
	{
		if (ht[i].parent == 0)
		{
			min = i; break;
		}
	}
	for (i = min + 1; i <= n; i++)
	{
		if (ht[i].parent == 0 && ht[i].node.fre < ht[min].node.fre)
		{
			min = i;
		}
	}
	s1 = min;
	for (i = 1; i <= n; i++)
	{
		if (ht[i].parent == 0 && i != s1)
		{
			min = i; break;
		}
	}
	for (i = min + 1; i <= n; i++)
	{
		if (ht[i].parent == 0 && ht[i].node.fre < ht[min].node.fre && i != s1)
		{
			min = i;
		}
	}
	s2 = min;
}
void show(huffman& ht)
{
	int n = 26;
	int m = 2 * n - 1;
	for (int i = 0; i <= m; i++)
	{
		printf("第%d行:", i);
		if (0 < i <= 26) { printf("%c ", ht[i].node.ch); }
		printf("%d   ", ht[i].node.fre);
		printf("%d   ", ht[i].parent);
		printf("%d   ", ht[i].lchild);
		printf("%d   \n", ht[i].rchild);
	}
}void creathuff(huffman& ht, bloo& node)
{
	int n = 26;
	int m = 2 * n - 1;
	int i = 1;
	ht = (huffman)malloc(sizeof(link) * (m + 1));
	for (i = 0; i <= 2 * n - 1; i++)
	{
		if (i <= n && i != 0)
		{
			ht[i].node.ch = node[i - 1].ch;
			ht[i].node.fre = node[i - 1].fre;
		}
		else
		{
			ht[i].node.ch = '#';
			ht[i].node.fre = 0;
		}
		ht[i].parent = 0;
		ht[i].lchild = 0;
		ht[i].rchild = 0;
	}
	for (i = n + 1; i <= m; i++)
	{
		int s1, s2;
		huffsort(ht, s1, s2, i - 1);
		ht[s1].parent = i;
		ht[s2].parent = i;
		ht[i].lchild = s1;
		ht[i].rchild = s2;
		ht[i].node.fre = ht[s1].node.fre + ht[s2].node.fre;
	}
}
void huffcode(huffman& ht)
{
	for (int i = 1; i <= 26; i++)
	{
		char* cd = (char*)malloc(sizeof(char) * 26);
		cd[26 - 1] = '\0';
		int start = 26 - 1;
		int c = i;
		int p = ht[c].parent;
		while (p != 0)
		{
			if (ht[p].lchild == c)
			{
				cd[--start] = '0';
			}
			if (ht[p].rchild == c)
			{
				cd[--start] = '1';
			}
			c = p; p = ht[p].parent;
		}
		ht[i].node.hufflong = 0;
		for (int j = start; j < 26; j++)
		{
			ht[i].node.hufflong++;
		}
		ht[i].node.hufflong--;
	
		ht[i].code = (char*)malloc(sizeof(char) * (ht[i].node.hufflong + 1));
		for (int j = start; j < 26; j++)
		{
			ht[i].code = &cd[start];
		}
		printf("\n%c字符对应的哈夫曼编码为%s,占用%d个二进制位", ht[i].node.ch, ht[i].code,ht[i].node.hufflong);
	}
}
void tongjisize(bloo& node, huffman& ht, int& size)
{
	size = 0;
	for (int i = 0; i < 26; i++)
	{
		size = size + node[i].fre * ht[i + 1].node.hufflong;
	}
}
void transhuff(char *str, huffman& ht, int size)
{
	for (int i = 0; i < size; i++)
	{
		for (int j = 1; j <= 26; j++)
		{
			if (str[i] == ht[j].node.ch)
			{
				printf("%s", ht[j].code);
			}
		}
	}
}
void retrans(huffman& ht, int huffsize)
{
	char* hcode;
	hcode = (char*)malloc(sizeof(char) * huffsize);
	printf("\n输入你要解压的二进制码:");
	for (int i = 0; i < huffsize; i++)
	{
		scanf_s("%c", &hcode[i]);
	}

	printf("\n读取成功\n");
	for (int end = 0, start = 0; end < huffsize; end++)
	{
		int count = 0;
		int nodelength = end - start + 1;
		for (int i = 1; i <= 26; i++)
		{
			if (nodelength == ht[i].node.hufflong)
			{
				int whetherfind = 0;
				for (int j = 0, count = 0; j < nodelength; j++)
				{
					if (hcode[start + j + 1] == ht[i].code[j])
					{
						count++;
					}
					if (count == nodelength)
					{
						printf("%c", ht[i].node.ch);
						end++;
						start = end;
						whetherfind = 1;
						break;
					}
				}
				if (whetherfind == 1) 
				{ 
					break;
				}
			}
		}
	}
}
int main()
{
	bloo node;
	int size = 0;
	char* str;
	str = (char*)malloc(sizeof(char)* 1000);
	printf("输入要处理的字符串长度:");
	scanf_s("%d", &size);
	size++;
	printf("请输入要处理的字符串:");
	for (int i = 0; i < size; i++)
	{
		scanf_s("%c", &str[i]);
	}
	initblo(node);
	for (int i = 0; i < size; i++)
	{
		for (int j = 0; j < 26; j++)
		{
			if (str[i] == node[j].ch)
				node[j].fre++;
		}
	}
	huffman ht;
	creathuff(ht, node);
	show(ht);
	huffcode(ht);
	int huffsize = 0;
	tongjisize(node, ht, huffsize);
	printf("\n\n原字符串占用内存大小为%d字节", size);
	printf("\n压缩后字符串占用内存大小为%d字节", huffsize / 8 + 1);
	printf("\n\n原字符串压缩后储存为:");
	transhuff(str, ht, size);
	retrans(ht, huffsize);
	return 0;
}

六.附实验报告:(引言和源代码部分大家可以自行拿走)

1.引言

2.源代码

3.实现思路以及程序演示(图片上传自动加水印了,大家可以复制源码到编译软件上自己再截图)

首先进行代码讲解

我将代码讲解分为以下六个步骤:

1.输入字符串,统计字符串中各个字符出现的频率

这段代码首先创建一个长度为1000的字符串,我们需要输入我们要输入的字符串长度size,执行size次循环让我们输入size个字符给字符串,node用来储存字符串中字符的出现频率,初始化node,用ASCLL码将a-z的字符储存ch中并将其中的fre初始化为0,统计字符串的出现的频率储存在node中。

2.接下来是创建哈夫曼树环节

 我们创建哈夫曼树,会有26个叶子节点,因为有26个英文字母以及他们出现的频率储存在node中,所以在这里申请2*26个节点,并且空出来了第一个节点没有放东西,除了叶子结点会储存英文字符其他节点的node.ch储存‘#’。这一段比较经典,就是找两个最小的然后认一个没用过的节点为父亲,父节点权值是俩子节点权值之和。

创建完Huffman树之后用show函数将这棵树所有节点的信息打印出来。

3.创建完哈夫曼树之后来找哈夫曼编码

 下面是一个关于找哈夫曼编码的例子:

在这个哈夫曼树之中需要找6的哈夫曼编码,首先我们要看它是它父节点的左孩子还是右孩子,在这里是右孩子,于是储存一个1,在看它的父节点4,4是它父节点的左孩子,因此储存一个0,4的父节点5没有父节点,所以在这里循环终止,可以看到我们得到了10,但是我们想要的是01,因此需要逆序储存,把第一个得到的二进制数存到cd[最后一位-1]中然后依次往前存,最后存了几位就要后几位。让26个字符都进行一下这个操作,就能得到想要的哈夫曼编码,顺便可以把取得位数保存下来,即该字符的哈夫曼编码所占的二进制位的位数。

4.下一步是靠哈夫曼编码将输入的字符串转换成哈夫曼码

这里仅仅是将转换后的哈夫曼编码打印出来,在这里也可以放一个堆栈去储存哈夫曼码。打印出来的过程比较简单,只需要依次遍历这串输入的字符,看看这个字符是什么,就能找到对应的哈夫曼编码,在这里就可以将哈夫曼码入栈或者打印了。

5.接下来观察一下压缩的效果,观察压缩前后占用字节的多少

一个字节是八个二进制位,我们需要算一算一共占用多少二进制位,除以八向上取整,得到我们理论上压缩后占用的字节大小

6.下一步是进行解压

解压的过程和压缩的过程很是相似,这里也是仅仅将解压出来的字符串打印出来,同样是可以用堆栈去储存,方法也同样是把打印那一步加一个入栈。在这里简述一下读取哈夫曼码的过程,还是举个例子。压缩一串字符串aaabcddeef,对应哈夫曼编码:a为10,b为1101,c为010,d为111,e为00,f为011,压缩结果是10101011010101111110000011。首先设置一个start指针和一个end指针,让他们都指向第一位二进制位,现在start和end之间只有一个1找有没有字符哈夫曼编码是1,没有,让end往后移,现在读取两位了,10,有了,a,输出a,现在前两位我们读取过了不要了,让start和end都从第三位开始,直到将整个编码读取完。

现在代码展示结束,接下来是运行结果的展示:

首先来压缩一段长度为114的乱码

reteacehewaeryveruherisvrygohingineveythaheistheherihaeverrdctyhdatteaaacvesendsoouatastrumsalsoveoccansatbestteen

输入完要压缩的字符串之后,首先会统计每个字符出现的次数,视次数为权值创建哈夫曼树,并且将哈夫曼树的表格打印出来,表格第一列为对应的字符,因为只有哈夫曼树的叶子结点会储存字符,其他节点储存字符‘#’,第二列为权重,叶子结点的权重即为该字符在字符串出现的频率,第三列储存的是该结点的父节点,最后一个节点(第51行)为根节点,其父节点为0,第四,五列储存的是该结点的左右孩子,前26行都为叶子节点,都没有左右孩子,因此其这两行都为0。

创建完哈夫曼树之后可以根据哈夫曼树去得到该结点的哈夫曼编码,顺便可以将哈夫曼编码占用的二进制位数打印出来。

还会打印出压缩前后占用内存空间大小,我们输入了114个字符,算上\0一共是115个字节,压缩后内存的大小在上面已经解释过了。

还有就是打印出压缩后的二进制码。很明显现在我们可以选择解压我们的二进制码。现在复制打印出来的二进制码,去解译出原本的字符串:

读取结果会被打印出来,我们还可以输入一些其他的二进制码编译出其他想要的字符串比如我现在输入一下如上图,输入一段二进制码,编译出来了一段和我输入的字符串不同的字符串:Our data structure teacher is very very handsome He was also very good at teaching I can ever say that he is the best teacher I have ever seen

  • 45
    点赞
  • 79
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值