目录
1.基础题
1.1线性表
线性表这一块的知识点主要由顺序表和链表组成,所以题目也是要求分别用这两种存储结构分别实现:
分别使用顺序表和链表实现大数的乘法运算,顺序表或链表的每个节点中仅存储1位数字。要求对输入字符串有基本的容错检查,以#作为字符串的结束符号,并对比分析算法的时间和空间复杂度,顺序表和链表均可使用课本提供的模板类。
示例: 输入:12345*9876#
输出:121919220
本题的总体示意图如下:
先来看顺序表部分,总体的实现思路是直接模拟了人工竖式乘法。我们知道在手算大数乘法的时候,通常是用如下方法算的:
可以看到,5位数乘4位数的结果,最终结果可以放在一个8位的顺序表里。实际上一个m位数乘一个n位数,最终结果要么是(m+n)位,要么是(m+n-1)位。这样我们就可以先预留出(m+n)位大小的空间,因为这已经足够储存结果了。
正如上图展示的那样,两个大数相乘可以看作是其中一个大数的每一个个位数与另一个大数相乘,最后将每一次乘的结果错位写开,再依次相加最后进位即可得到最终答案。顺着这个思路,我可以写出如下代码:
这段代码就直接模拟了人工计算时的“错位存储”。
之后是进位部分。这里就比较中规中矩了,就是一个格子里的数如果大于等于0,这个格子就取10的余数,整除10的结果就加到前一位去:
现在我们就面临着一个问题。拿上图里的例子来说,一个4位数乘一个5位数,结果可能是8位的也可能是9位的,我们只好先预留出9位的空间。但是上面这个结果只用了8位啊,这就会导致有一位是空白,这样一来,直接输出的话就会产生一些问题,所以我事先定义了2个数组:
将这两个数组里的元素全部置为0。回到之前的逐位相乘,会发现,在我们预留出9个空间而只使用了8个空间时,第一个元素是0,这样一来,直接输出的话第一位就会多输出一个0,所以这个时候就需要将第一个数组里的各位“复制”到第二个数组里,而不把前面的0复制过去。所以我们先计算第一个数组前0的数目:
接下来“复制”数组的时候,就可以从第一个非0位开始复制了:
之后顺次输出第二个复制了之后的数组即可。
本题还有一个要点就是如何处理输入的字符串。思路就是用字符串处理函数,根据位数进行截取,进而得到两个要乘的数的字符串。
当然,后面还有一个容错处理,判断相乘的两个字符串是否是数字字符串:
接下来是链表部分:
链表部分有一些和顺序表是类似,甚至相同的:例如从输入的字符串里截取出两个数字、容错处理,还有实现思路,都是用一个大数中的每一位与另一个大数相乘,再把结果相加。因为之前在完成顺序表时并没有费什么力气,我想着顺序表和链表既然都属于是线性表,那么它们的算法应该是互通的(我以前听过一个词来描述这种互通性,但现在怎么也想不起来了,查了半天也没查到),所以我尝试通过一定的程序改写,将顺序表的算法改为链表(之前对一些简单的算法题,我也做过这种改写,所以觉得这次改写应该不难)。但是我失败了,当中遇到的种种困难让我放弃了这次改写,转而思考如何通过其他思路来实现。
首先是存储结构,我用的是倒序储存,例如在储存12345这个数字时,链表的逻辑示意图是:
为了实现思路,我准备了一些前置函数:
上图是头插法,插入数字字符c,依照前面的存储结果可知,相当于是对一个大数乘了10再加上c,例如对12345使用上述函数插入字符6,得到的结果就是123456,示意图如下:
上图是尾插法,即在链表的最后插入数字字符c,这里主要是为了进位。因为进位是从低位进位到高位,而依照上面的存储结构,越往后面越是高位,所以采用的是尾插法。
上图是大整数加法的实现,细节还是比较繁琐的,每次都要取两位相加,当其中一个数加完后就是把剩余一个数给加进来。当然,每加一次都要考虑进位问题。
上图是大整数乘法的实现。可以看到进位一直得依托append函数实现。
上图是最终的大数乘法实现,可以看到有了大数乘小数函数multiply和大数加法函数add的帮助,这个实现起来还是比较简洁的。
其余诸如从输入的字符串里截取数字、进位处理等等,和顺序表里的实现思路无异,这里就不赘述了。
以下是运行结果:
首先是顺序表:
上图是顺序表正常输出的结果。
上图中,可以看到由于输入了两个乘号,报错处理体现出作用了,提示我们请继续输入。
以上是链表存储,与顺序表无异。
1.2栈和队列
想要在一道题里同时考察栈和队列的知识点,这里的题目就出的很有水平:
使用队列模拟一个栈,并基于该栈实现数制的转换计算。要求构造新的栈类,其中包含两个队列,通过队列的出入操作,实现入栈和出栈功能。队列可以使用课本提供的模板类。
示例: 输入:1348,8
输出:2504
算法思路参考:两个队列实现一个栈 - 简书
总体思路图如下:
本题的实现思路其实很简单。因为怎么用栈来实现进制转换,这已经是在课上讲过的,所以本题主要的难点是在怎么用两个队列来模拟一个栈,幸好老师已经给出了思路,就是在模拟队列的出元素时,先将前面的元素放到另一个队列里,然后出队列,这样就可以模拟出栈的“后进先出”。具体思路有如下示意图:
这题里的队列用的是课本给的示例代码,然后我用它写了一个栈出来,最关键的pop函数如下实现:
从上图可以看到,在for循环里,除了最后一次之外,其余都是第一个队列中的元素出队列,再用另一个队列接收;到了最后一次,出队列后就不用接收了,后面要做的就是把放到另一个队列里的元素再放回原先的队列里即可。这样就模拟了栈pop的“最后进的第一个出”。
下面是主函数部分,还是要从输入的字符串里截取出要转换的数字和要转换的进位制,和前面第一题大同小异。
接下来将字符串数字转为整型数字:
再接下来就是用除k取余法得到转换后的数字存到栈里面,再输入(实际上就是实现了倒序存储输出)
最后是运行结果:
1.3树和二叉树
为了考察树相关的知识点,这里选用的是哈夫曼编码,算是对树的一个综合运用。
题目如下:
使用哈夫曼编码对文本文件进行加密和解密, 根据文本文件中单词字符和标点符号出现的次数,构建哈夫曼树;将文本文件保存为哈夫曼编码文件,并保存相应的编码信息;将哈夫曼编码文件还原为原始文本文件。哈夫曼编码可以使用课本提供的模板类。
示例:
哈夫曼编码过程:输入:仅包含字符和基本标点符号的文本文件。
输出:哈夫曼编码文件、哈夫曼编码信息文件。
哈夫曼解码过程:
输入:哈夫曼编码文件,哈夫曼编码信息文件。
输出:原始文本文件。
总体思路如下:
这道题其实我原创的地方很少,算法原理思路和实现过程大致在书上都有,课上也讲过,我在这里不再赘述。接下来主要讲一下我是怎么实现这些的:
从上图可知,首先是要定义出相应的存储结构。对文本的扫描结果是放在了一个TNODE类型的结构体里,一边存储数字,一边存储出现了多少次,也就是权重。哈夫曼树类型自然就是权重和双亲结点、左右孩子了。
下面是读取文件的方法,这里用的是大一时写C++课设时学的窗口句柄:
将文件读入后开始构建TNODE结构体:
根据TNODE结构体来构建哈夫曼树(思路上没有什么原创性,就不细讲了)
注意这里的选择两个权值最小的节点的实现函数,这里还是很有技巧性的:
下面是创立文件来写入编码结果:
下面是解码过程,核心方法就是逐渐压入之前创立的编码文件,创立临时cd数组,每压入一个0或1就将这一串编码同之前创立的HC编码表进行对比,找到是否有对应字符匹配,有匹配字符的话向文件里输出相应字符,并且清空cd数组以留给下一次使用;如果没有的话就保留之前存储的01字符串,继续压入字符以进行后续的比较:
主函数部分,主要就是实现对以上几个函数的调用,适当加了一点容错处理,没有用什么算法:
接下来是运行效果展示:
在运行前要先把文件夹里的几个相关文件清空,方便起见,这几个文件和源文件在一个文件夹下:
开始运行,先选择1“加密文件”,会弹出窗口来供选择编码文件(须是txt文本格式,在源文件所在文件夹下就存了一个):
选择之后开始编码,这里在每一步完成之后都设立了暂停,以保证在出错时可以迅速找到是哪一步出问题了:
继续下去,选择2“解密文件”即可完成对01文件的解密:
这时我们打开源文件所在文件夹,即可找到几个相关的文件并查看结果:
首先看下原始文本文件:
这是加密结果:
这是解码表(出现问号是因为某些字符,如感叹号在写到文件里的时候出了些问题,不过从后面的结果可以看到这不影响解密):
下面是解密结果,可以看到与原始文本一模一样,也就是说压缩和解压的流程是无损的:
1.4图
这里没有让我们自己写代码,而是在看懂课本代码算法的基础上进行调试,所以说这道题的完成没有耗去什么时间。
下面是题目要求:
调试课本提供的最短路径代码,根据学校的建筑设计节点和路径,完成两栋建筑物之间的最短路径查询功能。
示例:
输入:建筑节点和路径信息文本
输出:查询菜单
输入:建筑节点A,建筑节点B
输出:AB间的最短路径为:ACDEB
接下来是整体流程图:
首先,这题从算法原理到代码实现大部分都是参考的课上老师讲过的和课本,原创的地方主要体现在对地大未来城校园路径的设计,如果有同学连这也和我一样的话,那多半是“借鉴了”。
最短路径算法,参考自课本:
使用的是迪杰斯特拉算法,算法原理和代码实现我都已经理解了,这里对课本代码作了一些调整,例如形参的数目我减少了一个,改为使用外部数组,还有就是一处getWeight的g的大小写问题(课本上是大写,应该是错了)
下面是显示最短路径和长度的算法,也是参考的课本,不过这个函数给的示例代码里没有,是我自己照着课本敲的,敲的同时也作了一些改进:
首先,我多加了一个参数end,作为中止条件,这样这个函数显示的路径就是一个顶点到另一个顶点的一条路径,而不是此前那样一个顶点到其他所有顶点的路径都显示出来。此外我还用了一个二维数组来存储学校节点信息,这个地方是完全原创的,如果有雷同那只能是抄袭了。
然后是主函数部分,这里中规中矩:
这里传入的全部是普通的整型数字,其实我本来用的是new出来的动态数组,但是在运行程序时总是会报错,这个bug我调了很久也没调好,后来改用了普通的数组传入函数后便不在报错。我推测是释放内存时出现了问题,最后发现通过修改课本代码中的析构函数可以解决此问题,终于算是真相大白了。
最后是运行展示:
2.综合题
2.1设计思想
遇到这道题,最朴素的思想肯定是用一个什么容器或者说是结构,把这些单词给装起来,然后输入一个单词后,分别截取它的首字母和尾字母,把首字母和上一个单词的尾字母进行判断,如果相同就算是接上了,这时该加分的加分,该调整词库的调整词库,然后保存这个单词的尾字母作为下一次输入后判断的依据。在设计存储结构的同时采用的是AVL树+vector结合的形式。AVL树主要用来先行判断单词的有无,vector则用来储存分数、使用情况之类。在使用vector时也不是一股脑把单词存到一个vector容器里,而是先为26个字母分别在vector里开辟了一个位置,在这个位置下再用vcetor嵌套储存单词,有点类似于二维数组,主要是为了保证查找时的速度,避免平均查找长度过大。(对于一个5000+的词库来说平均查找长度在200左右)这是基本的设计思想,当然在具体设计的时候还是要同MFC程序结合起来分析。
2.2算法流程
先将文本文件导入词库后,放在相应的结构体下储存(词库是可以重置的)。输入单词后,分别截取首尾字母,其中首字母用于判断是否和上一个单词的尾字母重叠,重叠则视为接龙成功(这里实现了高级功能,支持计算重叠位数并加倍数分数),截取的尾字母则留存为下一次判断做准备,当然与此同时还要进行排序,三种不同的规则分别采取了三种排序方法进行排序。用户可以从提示的单词里选择(输入提示外的单词可能会由于不在词库里或已经使用过而报错),此外也可以控制显示单词的数量。完成一次接龙加分后,用户可以选择继续输入,或是退出游戏。
2.3运行结果
运行程序,首先选择词库:
从中选择六级词汇(这些词汇表是我从我复习四六级的词库里导到txt文本里的,也给了不少同学,所以其他同学的词库可能也有这里面的)
这是初始界面:
点击“开始游戏”,输入框会被激活,可以输入单词了:
上图显示了AVL树的先行判断,节省时间。
输入单词后,就会根据尾字母进行显示,可以选择三种排序方式:
(这里是按单词数排序,可以看到由于y开头的单词最少,所以上面显示的全是d开头y结尾的单词)
可以选择多展示一些单词,在右上角的框里输入数量即可:
(从此图也可以分析出各类单词在六级词库中的数量谁多谁少)
下面我们输入提示中的第一个单词deny,会显示接龙成功,右上角的得分和接龙数也会有相应变化:
按分值降序,即是分值高(长度长的单词排在前面):
升序则是短的排在前面:
最后是叠词功能的展示。例如我先输入一个red:
之前我已经在词库里加入了一个edg:
所以输入edg后,分数就变成了0+2×3=6,因为edg和red末尾有两个字母重叠了(可以看到输入edg前是0分,也就是说输入第一个单词不加分):
2.4重点问题
由于我是第一次构建MFC程序,所以肯定是遇到了不少困难,但这些不属于是数据结构范围内的,这里就不贴出来了。下面主要贴两个数据结构相关的重点问题
首先是如何存储。如何设计合适的存储结构是一个让人头疼的问题,我费尽心思终于设计出了如下的双struct结构:
相应配套的存储操作如下:
先去重:
再储存至同首字母的对应结构体下:
还有就是为了降低时间复杂度,在输入单词后加入了AVL树来进行先行判断,避免进行不必要的循环:
其他地方诸如排序算法、显示数量之类的,虽然在实现时也遇到了一些小的问题,但总体上来说实现并不难,这里就不贴出来了。
2.5算法分析
这里主要分析一下几个排序算法的时间复杂度。
首先是按分值降序排序,这里采用的是最简单的冒泡排序,时间复杂度是
接下来是按分值升序排序,这里采用的是选择排序,时间复杂度是
最后是按后续单词数最少这一规则进行排序,需要计算剩余的、还没有使用的单词库中各个首字母下的单词数再排序。这里采用的是交换排序:
值得一说的是,最后一行这里先开始一直没加,导致排序结果总是不正确。
此外就是避免无用的循环过多,在每次输入单词后都会先在AVL树中进行一遍搜索,确认词库中有此单词后才会进行后续的搜索,这一定程度上节约了运行时间。
3.课程小结
首先,我要在这里感谢张老师的悉心教导。没有张老师的帮助,这次的课设我是很难完成的。
这次的数据结构课设无疑是一次难忘的经历。我私下和同学们聊天的时候也说:“做了课设,自己敲了些代码,才感觉是真正学过这门课了。”
的确,以我上大学一年半以来的感受来说,工科和理科的一个很大的区别就在于工科是注重实践的。就拿数据结构这门课来说,正如张老师经常在课上说的那样:如果只是为了应付考试,那么刷刷题是完全够了的,因为单从理论层面来说,测绘大类教的数据结构确实不难(这里说的是测绘大类下的数据结构不难,单就这门学问而言,是非常值得研究而且富有挑战性的)。
学习数据结构也算是我第一次正式接触算法。算法这个东西给我的感受就是,看懂别人写的算法不难,但真的要自己构思出一个算法来,却又无从下手。幸亏我不是专门学计算机的,对算法方面的要求可能主要还是运用别人写好的算法来解决问题(我暂时是这么想的啊,可能不正确)。
就拿本次课设的这五道题来说,总体来说是对我自身能力的一个挑战。前三题我主要是在机房里完成的。说实话,我不是很喜欢在机房里写代码——虽然大家基本都在写代码,讨论的氛围也很好,但我还是有点不太适应。我喜欢在安静一点的环境里写代码,所以前几次上机的时候我总是戴着一个耳机,坐在角落里自己敲键盘。
第一题是分别用顺序表和链表实现两个大数的乘法运算。最难的地方,我觉得是大数乘法的链表实现。因为之前在完成顺序表时并没有费什么力气,我想着顺序表和链表既然都属于是线性表,那么它们的算法应该是互通的(我以前听过一个词来描述这种互通性,但现在怎么也想不起来了,查了半天也没查到),所以我尝试通过一定的程序改写,将顺序表的算法改为链表(之前对一些简单的算法题,我也做过这种改写,所以觉得这次改写应该不难)。但是我失败了,当中遇到的种种困难让我放弃了这次改写,转而思考如何通过其他思路来实现。最后终于得到了将大数乘法转化为十以内的数同大数相乘再求和的思想,又通过两百多行代码最终才实现了这半道题,过程真是艰辛啊。
第二题,用两个队列模拟一个栈并用此栈实现进制转换。由于张老师已经给出了算法实现思路,又给出了课本示例代码,所以这道题写起来基本上没遇到什么阻力,按部就班地就写完了。相较于之前的链表大数乘法,写完第二题让我信心大增。
第三题是实现课本上学习过的哈夫曼树。如果没有在数据结构课上学过哈夫曼树,这道题想必理解起来相当有难度。在课上充分了解了哈夫曼树为什么能实现编码的压缩和算法实现思路后,这道题的代码写起来就没有那么难了(不过也还是挺难的,花了挺长时间才写完)。
前三题写完之后,我就兴冲冲地第一个跑去验收了。这么急着去验收,我也是为了能早点和张老师交流一下,找到问题后好及时改正。还好前三题没有出现大的问题,除了把第一题顺序表的数组实现改成了顺序表实现之外,其他地方都算是顺利通过了。
第四题,是调试课本上的代码,难度不在调试代码上,而是在理解最短路径算法上。这里选择的算法是课本上的迪杰斯特拉算法,通过课上的听讲和私下看书揣摩,我基本上理解了算法原理和代码实现的各种细节。当然,在调试的过程中还是遇到了一些问题,例如new出两块动态数组后传给shortestpath函数和printpath函数后,在程序运行结束后退出时竟然会报错!这个bug我调了很久也没调好,后来改用了普通的数组传入函数后便不在报错。我推测是释放内存时出现了问题,最后发现通过修改课本代码中的析构函数可以解决此问题,终于算是真相大白了。由于这道题不是自己写代码而是调试课本代码,原创性较低,所以很多同学来问我怎么调试,我也为很多同学作了解答。但是少量同学copy了我的代码,导致我后来不得不重新对代码作了调整(其实不止是这一题,前四题都或多或少存在这样的情况,只是第四题最为严重)。
第五题才是本次课设的大BOSS,为了写好这道题,我前期做了很多准备。因为我一开始就是奔着高级要求(实现用户交互界面)去的,所以我花了不少时间去研究MFC程序的实现方法(本来也考虑了用QT的,因为网上说QT的语法更为简单,但我想着毕竟就是实现一个简单的交互程序,用MFC或者QT都只是用了一点皮毛知识,差别应该不大,而且MFC装起来简单一些,只用在Installer里安装时勾上就行,所以最后还是决定用MFC实现)。还好我只是用MFC的对话框界面实现了一些功能,难度不是很大,所以我还是把本道题的重点放在了具体算法的实现上。
首先我要面临的问题是设计合适的存储结构,由于要存储的东西不仅包括单词文本,还有它的分数值,所以我用了一个struct结构体来储存每个单词。当然这个储存思路的形成也不是一蹴而就的,也是逐渐思考形成的。实现思路我在前面的报告里已经写的很清楚了,这里就不再赘述了。
最后把几道题都写完后还出了一个小插曲:最近微软新出了visual studio 2022,于是我就把原来的vs 2019卸载了并装上了vs 2022,代码可以顺利运行。后来有同学想要借鉴我的代码,我就从vs2022将文件导出发给了他,结果那边的vs2019显示打不开。我吓了一跳,后来才了解到:vs2022使用的工具集是v143,而vs2019是v142。而高版本的vs可以将低版本vs导出的解决方案的工具集自动升级,换句话说高版本vs可以打开低版本vs导出的文件,反过来却不行。于是我只好把vs2019又重新装上,然后用头文件和源文件来新建项目,最后再导出,才有了我现在交上来的代码。
这份报告是我在期末考试后才开始写的,也只剩两三天时间写了,而且其他事情也比较多,所以留给我写报告的时间也不多了,之前很多想画的图也没时间画了(本来是打算统一用程序框图表示的,后来时间不够了,很多图都是在纸上画好后扫描成pdf再贴到报告里),比较重要的东西我也都写在报告里了,可惜的是细节方面没有时间打磨了。
最后,我还是要感谢张老师的悉心教导!没有张老师的指点而仅凭我个人,这份报告写出来肯定会逊色很多。