数据结构课程设计报告——Huffman编码

一、 问题描述与要求

对于任意待编码的字符集(字符集中字符最少不能少于20个),并给定他们的出现概率,概率大小由编程输入。
要求:按照待编码的字符的概率生成Huffman树,并根据生成的Huffman树将待编码的字符进行Huffman编码,并输出。

二、 需求分析

根据题目要求,程序需要对字符串中每个字符出现的频率进行统计,得到统计表,用统计结果构造Huffman树,再对Huffman树进行遍历得到Huffman编码表,最后据此对待编码的明文编码成Huffman编码的密文。
以上需求涉及到字符串的预处理和统计,Huffman树的构造,二叉树的遍历,由遍历Huffman树的路径生成Huffman编码。
除此之外,还额外完成了解码功能的实现。

三、 设计

3.1 设计思想

3.1.1 数据与操作的特性

程序要从输入的字符串中统计出各字符出现的次数,其中多次涉及在已有统计表中的查找操作。
构建Huffman树时需要从上一步统计得来的表中选出最大和次大的两个元素,将两个子树合并,将合并后的根结点放回森林中,之后再在此森林中找到根节点最大和次大的两个子树再合并之后再放回。这个过程涉及树的合并,再放回去,找出最大和次大的算法。
遍历Huffman树得出Huffman编码的过程需要在遍历的同时记录在每个双亲结点处选择的方向,当遍历到叶子结点时输出路径作为Huffman编码。将明码字符和该字符的Huffman编码放入结构体数组中得到编码表
有了编码表,在对原文进行加密的过程中涉及到不断根据原码字符在编码表中查找对应的Huffman编码,查找次数和原文中字符次数相同,因此此处若采用顺序查找比较耗时,时间复杂度为O(n2)。相比之下,若采用折半查找法时间复杂度被优化为O(log2n)。所以此处采用折半查找法查找原文在编码表中对应的Huffman编码。

3.1.2 数据结构设计

根据以上分析,在统计字符串中每个字符出现的次数的过程中,每当扫描到字符串中的下一个字符,都不可避免地在已有的统计表中查找该字符,且查找非常频繁。因此可以考虑采用时间复杂度仅有O(log2n)的红黑树,可以大大提高统计中的查找效率。
从森林中找出最大和次大两个元素可以采用将两个子树合并后的新树插入原有有序序列,使其仍然是有序序列的方法。因为涉及到排序,而排序最小的时间复杂度为堆排序的O(nlog2n),如果用堆排序排好序再取结点合并后再插入的话还多一道插入的步骤,不如直接从最小堆里取结点,省掉了排序的过程,在数据量比较大的情况下,这种算法优势将非常明显。
在遍历Huffman树获得Huffman编码的过程中,考虑采用递归算法,用静态字符数组记录之前在每个双亲结点选择的方向,当抵达叶子结点时输出已经记录的路径作为该叶子结点的Huffman编码。
程序中因为涉及到多个表,所以有三个定义表结点的数据结构。字符数量统计表直接以Huffman树结点为其每个结点的结构,最小堆的数据结点也是Huffman树结点,最后译码表的结点内包含了一个HuffmanNode和一个存储编码的string。

3.1.3 算法设计

整个程序划分为输入输出的处理,字符统计,建立Huffman树,最小堆四大模块。
首先此程序不能处理中文,因为中文和英文字母不同,中文字符占两个字符,中英混合将给后续的统计和存储带来很大困难。因此该程序只处理英文字符串。所以,需要对输入的字符串进行遍历的同时检查是否含有非法字符。
由于建立Huffman树的过程涉及到多次在Huffman类和最小堆MinHeap类之间的数据交换,即把子树从最小堆中取出,合并后再放入的操作。最小堆采用结构体数组实现,堆内的每个结点无法装下整个合并后的子树,也不能只装合并后的新根节点,因为之后还会取出用于构造新的树。因此考虑使最小堆内的每个结点不存放子树本身,而是存放指向统计表中结点的指针。由此很方便地实现子树的取出,合并,再放回最小堆。
Huffman树被建立之后,用递归的方法对Huffman树进行遍历,得出Huffman编码。在函数中定义一个静态字符数组存储Huffman编码,使得下一层递归可以直接接着上一层递归继续编码,遇到叶子结点则在存储Huffman编码的字符串末尾加结束符号‘\n’,并赋给编码表中的相应单元。
结束递归之后,再用间接排序法(又称索引表法,表排序法)对存放Huffman编码表的结构体数组按照字母顺序降序进行排列,方便之后的折半查找。之所以用索引表法是因为结构体数组体积较大,而排序又涉及到结构体变量的不断赋值和移动,时间开销大。所以用间接排序法,排序过程中移动的是结构体变量在结构体数组中的下标,排序完后按照下标一次性将结构体变量移到新表中,构成有序表。
由于编码的过程涉及到不断根据原文字符到编码表中找对应的Huffman编码的过程,且次数非常频繁,因此此处不采用顺序查找法而是采用折半查找法,大大优化了时间复杂度。
解码过程直接基于Huffman树,而不是在编码表中根据密码查原文。解码时,密码是1则Huffman树中的指针指向当前节点的左子节点,密码是0则指向当前节点的右节点,直到抵达叶子结点,输出该叶子结点的字符,指针重置为根节点,如此循环,直到密码结束。

3.2 设计表示

3.2.1 函数调用关系图

在这里插入图片描述

3.2.2 类视图

在这里插入图片描述

3.3 详细设计

3.3.1 结点数据结构设计

由于程序中涉及到字符的统计,给每个字符编码,构建编码表。因此涉及到两结构体作为表的元组。程序中有Huffman结点和编码表结点两个结构体。
Huffman树结点的结构用伪代码可以表示成如下形式:

Huffman树结点
{
	单个原文字符;
	权值;
	左结点指针;
	右结点指针;
	构造函数和运算符重载;
}

Huffman结点是Huffman树的结点结构和字符统计结果表的元素。
tableNode是Huffman编码表中每个元组的结构,其类结构用伪代码表示如下

tableNode
{
	字符;
	权重;
	Huffman编码;
	等号重载;
}

3.3.2 算法详细设计

因为堆是完全二叉树,所以堆采用数组的形式存储会比较方便。如果一个结点的下标为i,那么其左子树下标为2i+1,右子树为2i+2,这样很方便能找到每个结点的子树。最小堆的建立的基本思想是将结点一个一个插入堆,插入的同时做调整,最终把所有结点都插入,形成最小堆。从堆中取元素也类似,最小堆的堆顶元素最小,取走之后删除该结点,然后再做调整。
我们采用从上到下逐步调整形成堆的方法,调用下滑调整算法,siftDown,将以它们为根的子树调整为最小堆,从局部到整体,将最小堆逐步扩大,直到整个树成为最小堆。
下面的代码截图包含了详细的注释,可以较为清楚的描述此算法。

由数组建立最小堆:

MinHeap::MinHeap(HuffmanNode* arr, int n)
{//由数组建立最小堆
	maxHeapSize = (DefaultSize < n) ? n : DefaultSize;
	heap = new HuffmanNode*[n];//分配空间
	if (heap == NULL)
	{
		cout << "空间分配失败" << endl;
		exit(1);
	}
	for (int i = 0;i < n;++i)//初始化堆
	{
		heap[i] = &arr[i];//将统计结果表中每个元素的地址放入堆中
	}
	currentSize = n;
	int currentPos = (currentSize - 2) / 2;//设置开始调整的位置为最后分支节点
	while (currentPos >= 0)
	{
		siftDown(currentPos, currentSize - 1);//检查并调整这棵子树
		currentPos--;
	}
}

向下调整算法siftDown

void MinHeap::siftDown(int start, int m)
{
	/*从节点start开始到m为止,自下向上比较,如果子女的值小于父结点的值,
	则关键码小的上浮,继续向下层比较,这样将一个集合局部调整为最小堆*/
	int i = start, j = 2 * i + 1;
	HuffmanNode* temp = heap[i];
	while (j <= m)
	{
		if (j<m && heap[j]->weight>heap[j + 1]->weight)
			j++;//让j指向两子女中的最小者

		if (temp->weight <= heap[j]->weight)
			break;//小则不做调整
		else
		{//否则小者上浮,i,j下降
			heap[i] = heap[j];
			i = j;
			j = 2 * j + 1;
		}
	}
	heap[i] = temp;
}

向上调整算法:

void MinHeap::siftUp(int start)
{
	/*从结点start开始到结点0为止,自下向上进行比较,如果子女结点的值小于父结点的值
	则相互交换,这样将集合重新调整为最小堆*/
	int j = start, i = (j - 1) / 2;//i指向start的双亲结点
	HuffmanNode* temp = heap[j];
	while (j > 0)//沿着start的双亲结点向上直达根
	{
		if (heap[i]->weight <= temp->weight)
			break;//父结点值比插入的结点小,满足最小堆,不调整
		else
		{
			heap[j] = heap[i];//父子结点的值交换

			//上移一层
			j = i;
			i = (i - 1) / 2;
		}
	}
	heap[j] = temp;
}

插入算法:

void MinHeap::Insert(HuffmanNode*& x)
{
	if (currentSize == maxHeapSize)
	{
		cout << "堆满" << endl;
	}
	heap[currentSize] = x;//插入
	siftUp(currentSize);//调整
	++currentSize;//堆计数+1
}

取堆顶

void MinHeap::GetMin(HuffmanNode*& x)
{
	if (!currentSize)
	{
		cout << "堆空" << endl;
	}
	x = heap[0];
	heap[0] = heap[currentSize - 1];//用堆的最后一个最大的结点填补取走的
	currentSize--;
	siftDown(0, currentSize - 1);//调整
}

通常,从最小堆删除具有最小关键码记录的操作是将最小堆的堆顶元素,即完全二叉树的顺序表示的第0号元素删去。在把这个元素取走后,一般以堆的最后一个结点填补取走的堆顶元素,并将堆的实际元素个数减1。但是用最后一个元素取代堆顶元素将破坏堆,需要调用siftDown算法从堆顶向下进行调整。
Huffman树的构造思想是:用于构造树的n个结点组成森林,从中取前两个最小的结点组成新树,新树的根节点是两个叶结点权值之和,再将新树放回森林,再从森林中取根节点最小的两棵子树用同样的方法组成新树,再放回,循环往复直到森林中只剩一棵树为止。

HuffmanTree::HuffmanTree(string& s)//通过数组构造哈夫曼树
{
	int n = s.size();
	countResult(s, n);
	MinHeap heap(StatisticalResult,n);//建立装结点的最小堆
	HuffmanNode* parent = NULL;
	HuffmanNode* first; //存放根权值最小的子树树根
	HuffmanNode* second;//存放根权值次小的子树树根
	HuffmanCodeTable = new tableNode[n];
	tableSize = n;
	if (n == 1)//只有一个字符进行编码的情况
	{
		root = StatisticalResult;
		return;
	}
	for (int i = 0;i < n - 1;++i)//有n个结点,所以做n-1次合并
	{
		heap.GetMin(first);//选择根的权值最小的子树
		heap.GetMin(second);//选择根的权值次小的子树

		mergeTree(first, second, parent);//合并树
		heap.Insert(parent);//再插入堆
	}
	root = parent;
}

通过对树遍历得出Huffman编码,采用递归方法实现。

void HuffmanTree::GetHuffmanCode()
{
   _GetHuffmanCode(this->root, 0);//进入递归
   sortCodeTable();//对编码表排序,方便后面编码的时候折半查找
}

void  HuffmanTree::_GetHuffmanCode(HuffmanNode* root, int depth)
{
   static char code[512];
   static int k = 0;
   tableNode temp;//临时

   //如果有左子树
   if (root->leftChild != NULL)
   {
   	code[depth] = '0';
   	code[depth + 1] = '\0';
   	_GetHuffmanCode(root->leftChild, depth + 1);
   }
   //如果有右子树
   if (root->rightChild != NULL)
   {
   	code[depth] = '1';
   	code[depth + 1] = '\0';
   	_GetHuffmanCode(root->rightChild, depth + 1);
   }
   else//都没有就是叶结点
   {
   	temp.c = root->c;
   	temp.weight = root->weight;
   	temp.code = code;
   	HuffmanCodeTable[k++] = temp;//存入表
   }
}

我在题目要求之外还额外实现了一个解码功能,解码算法的主要思想是:指向树结点的指针根据Huffman编码来选路,从根节点开始,如果当前编码是0就移向左子树,如果是1就移向右子树,当移到叶子结点时,输出叶子结点上的字符,同时该指针回到整棵树的根节点。如此循环往复,直到Huffman编码结束。
代码如下:

void HuffmanTree::decode(string& codedStr)
{
	string::const_iterator iter= codedStr.begin();//迭代器
	HuffmanNode* curNode = root;
	string decodedStr;
	while(iter != codedStr.end())//遍历整个密码字符串
	{
		if (*iter == '0')//如果当前编码为0,就移向左子树
			curNode = curNode->leftChild;

		if (*iter == '1')//如果当前编码为1,就移向右子树
			curNode = curNode->rightChild;

		if (curNode->rightChild == NULL || curNode->leftChild == NULL)
		{//如果是叶子结点,就把明文字符加到明文字符串里
			decodedStr += curNode->c;
			curNode = root;//指针置回根结点
		}
		iter++;
	}
	cout << decodedStr<<endl;//循环完了就直接输出
}

四、 调试分析

这个程序的编写可谓费尽周折,远远超出了我想象的难度,我本来以为一天就能把程序写出来,结果写了四天。中间经历过两次代码重构,遇到了很多问题,经过很长时间的调试才解决。
印象中遇到的第一个比较大的问题是发现无法构建树,经过单步调试,监视根节点变量发现,构建出来的树只有两层。原因是当时我的最小堆直接存放的是树结点,这样树在合并之后,放入堆的就只有根节点,而下面的结点全部丢失了。我把堆中存放的对象从树结点结构体改成指向树节点的指针,解决了此问题。
另一个问题出在析构函数上。我添加了析构函数程序报错“访问权限冲突”。我监视变量值得同时单步调试发现,当析构函数delete到Huffman树的叶子结点的时候,删除叶结点会导致其父结点中指向其兄弟结点的指针改变,从而使兄弟结点丢失。到目前为止,这个问题仍然未解决,但不影响程序进行Huffman编码。我认为这可能和存放叶子结点的变量存放在用new生成的数组中有关,而用于连接它们的非叶结点却是单独用new申请的有关。
在撰写报告时偶然发现解码模块有不能解码出明文的最后一个字符的BUG,因为之前最后一个字符一直是英文句号而没有被发现。通过自行输入一个单词进行编码再进行译码发现最后一个字符没有译出来。经过分析得出问题出在if语句的位置上,也即是循环中if的顺序问题。如果密码字符串中的指针后移和判断是否为叶子结点跨越了循环,那么会出现密码字符串指针满足了退出循环的条件(到达了密码末尾),此时Huffman树中的指针也指向根节点了,然而记录明文的语句在下一个循环中,还没来得及记录明文字符,循环就结束了,导致最后一个明文字符记录不上。调换三个if的顺序,将判断是否为叶子结点的if语句放在每次循环的最后,让树结点指针移动之后立即判断有没有到达叶子结点,这些做完后密码字符串中的指针再后移。由此解决了此问题。

五、 用户手册

在这里插入图片描述

如上图,按照程序提示输入一段不带中文字符的字符串,即任何字符都必须只占一个char字符类型,不能含有全角字符。否则程序将会让你重新输入。该字符串输入之后必须以#结尾,表示结束输入。
比如我输入Data Structure# 则出现如下结果:
在这里插入图片描述

其中,上面Huffman编码表中的第一个空白实际上是空格。
然后再将生成的密文用Ctrl+C复制之后用Ctrl+V粘贴输入,程序经过解码将获得明文。
也可以复制密文的一部分,这里复制密文的前12位,也就是对应原文的“Data”这一部分,让程序解码,看看是不是解码结果是不是“Data”。
在这里插入图片描述

结果正好是101110000100对应的“Data”,说明解码正确。

六、 测试数据及测试结果

6.1 边界测试

测试数据:a#
测试目的:让程序对单个字符进行编码,看是否会出现问题。
正确输出:编码表中a对应的值为空白,也就是a没有编码。
在这里插入图片描述

这很正常,因为根节点就是叶子结点,找到它根本不用分支。但是对单独一个字符进行Huffman编码也是无意义的。

测试数据:ab#
测试目的:只有两个字符的字符串能否被正常编码?
正确输出:
在这里插入图片描述
单个字符也能顺利解码。

6.2 压力测试

测试数据:
China University of Geosciences (CUG), founded in 1952, is a national key university affiliated with the Ministry of Education. It is also listed in the National “211 Project”, the 985 Innovation Platform for Advantageous Disciplines and the “Double First-class Plan”. CUG, featuring geosciences, is a comprehensive university that also offers a variety of degree programs in science, engineering, literature, management, economics, law, education and arts. Its Geology and Geological Resources & Engineering have both been ranked as national number one disciplines. Its Earth Science, Engineering, Environmental Studies & Ecology, Materials Science, Chemistry, and Computer Science have entered the top 1% of global ESI (Essential Science Indicators), with Earth Science in the top 1 of the list.
CUG has two campuses in Wuhan. The main campus is the Nanwang Mountain Campus, located in the heart of the Wuhan East Lake National Innovation Demonstration Zone, which is popularly known as China Optics Valley. The Future City Campus is located in the east of Wuhan and is 27 km from the main campus. These two picturesque campuses cover a combined area of 1,435,545 m2. They are ideal places to study, work, and enjoy life. CUG also boasts four field training centers: Zhoukoudian in Beijing, Beidaihe in Hebei Province, Zigui in Hubei Province, and Badong in Hubei Province.
CUG has established a complete education system. As of November 2019, 28,635 full-time students, including 18,092 undergraduate students, 7,774 master’s students, 1,764 doctoral students, and 1,005 international students have enrolled in its subsidiary 23 schools and 86 research institutes. CUG currently has a faculty of 1,876 full-time teachers, among which there are 520 professors (11 of which are members of the Chinese Academy of Sciences) and 927 associate professors.
CUG is focused on fostering high-quality talent. Among its over 300,000 graduates, many have gone on to become scientific and technological elites, statesmen, business leaders and athletes. And they have made great contributions to the nation and society, represented by former Premier WEN Jiabao and 39 members of the Chinese Academy of Sciences and Chinese Academy of Engineering.
CUG has strengthened exchanges and cooperation with international universities. It has signed friendly cooperation agreements with more than 100 universities from the United States, France, Australia, Russia and other countries. CUG has actively carried out academic, scientific and cultural exchanges with universities around the world. There are about 1,000 international students from more than 90 countries studying at CUG. It also sponsors more than 900 teachers and students to study abroad or conduct international exchanges, and invites more than 400 international experts to visit, lecture, and teach at CUG every year. In 2012, CUG initiated and co-established the International University Consortium in Earth Science (IUCES) with 11 other world-renowned universities. IUCES is committed to promoting the common development of geosciences education and scientific research through resource sharing, exchange and cooperation among its member institutions. In addition, CUG has partnered with Bryant University from USA, Alfred University from USA, and Veliko Turnovo University from Bulgaria in establishing three Confucius institutes on their campuses.#
测试目的:测试在明文字符数非常多的情况下,程序能否顺利编码并且解码,是否出现内存泄漏的问题。
测试结果:
在这里插入图片描述

可见,在密码中截取一段进行解码,程序能够正确解码出对应的结果。
(存放在剪切板之中的是密码的全文,由于控制台一次性接受的字符数量有限,所以只有一小部分被粘贴了上去。)

七、 此程序的亮点

  1. 采用最小堆,折半查找,间接排序节省时间开销。其中创建Huffman树的时间复杂度被优化为O(nlog2n),编码中根据字符查询对应的编码的时间复杂度被优化为O(log2n)
  2. 额外实现解码功能。
  3. 使用C++编写,模块之间低耦合,便于维护。

八、 源代码

数据结构课程设计_哈夫曼编码.zip

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值