福大软工1816 · 第二次作业 - 个人项目

GitHub仓库地址:https://github.com/qwe8/personal-project


PSP表格


PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 20 30
· Estimate · 估计这个任务需要多少时间 20 20
Development 开发 120 660
· Analysis · 需求分析 (包括学习新技术) 40 120
· Design Spec · 生成设计文档 20 45
· Design Review · 设计复审 10 10
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 10 10
· Design · 具体设计 20 30
· Coding · 具体编码 60 120
· Code Review · 代码复审 30 50
· Test · 测试(自我测试,修改代码,提交修改) 60 120
Reporting 报告 30 80
· Test Repor · 测试报告 10 30
· Size Measurement · 计算工作量 10 10
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 20 60
|       | 	合计  |450 |895

需求分析


对程序的功能进行需求分析如下:

统计文件的字符数,只需要统计Ascii码。
统计文件的单词总数,单词:至少以4个英文字母开头,跟上大小写字母数字符号,单词以分隔符分割,不区分大小写。
统计文件的有效行数:任何包含非空白字符的行,都需要统计。
统计文件中各单词的出现次数:输出频率最高的10个。频率相同的单词,优先输出字典序靠前的单词。
按照字典序输出到文件result.txt:例如,windows95,windows98和windows2000同时出现时,则先输出windows2000
输出的单词统一为小写格式

算法设计:


统计字符数和行数直接遍历,判断是否是单词也可以模拟一下,重点在于统计单词的次数和输出词频最多的单词。考虑如何统计单词的次数,很明显可以想到利用map,将单词放入map中++,但考虑到map的复杂度非常大(虽然纸面上是log级的,但map里存放字符串在常数会非常大,这是因为字符串的排序并不是O(1)的,会T得你怀疑人生),所以用map来统计单词次数不太好。
那么能用的数据结构有什么呢?目前我所知道的有两种,一是hash,二是字典树,首先这两种的时间复杂度肯定都完爆map,我选择用字典树来做而不选择hash,原因有四:

  • 1 hash存在一定的碰撞率,要想降低碰撞率,可以增大空间,或者双重hash等等,但双重hash编码会变得复杂麻烦,时间加倍。
  • 2 空间不知道开多大,题目并没有说单词数最多有多少,所以你空间开小了碰撞率急剧上升,空间开大了又变得臃肿,浪费。
  • 3 hash的时间复杂度插入是O(n),字典树也是O(n)(n为单词长度),但hash的时间常数很大,因为其用到了乘法,还有取模,注意取模的复杂度相当于log,当然hash取值是O(1)的,字典树是O(n),但综合复杂度是字典树更优。
  • 4 感觉写hash的人应该蛮多的,所以写写字典树。

以上分析都是我以为。
考虑要记录词频最多的10个单词,可以利用堆来存储,我使用stl的SET加pair来处理,首先SET是是一个可以自动排序的容器,插入删除查询时间复杂度为log级别,根据题目要求要排序两个东西,一个是单词词频,还有一个是字符串的字典序,可以将这两个东西放到pair当中,再将pair放入SET中,而SET里我们只需要放10个数据,多的删除即可。
算法到这里就设计完成了,字典树时间复杂度为O(N) (N为树大小,也差不多为单词字符数),记录词频最多10个单词时间复杂度为O(M*log(10)),由于没有用到复杂的运算,所以常数比较小。

计算模块实现过程


  • 1 统计文件字符数:直接遍历到文件结束即可。
  • 2 统计文件有效行数:整行读入,在遍历一遍数组,看是否存在非空字符。
  • 3 统计文件的单词数:遍历输入的字符串,如果遇到连续一段合法字母或数字,将他们取出来,在判断开头是否存在4个字母,如果是单词的话,将这个单词插入到字典树中,然后单词数++即可。
    字典树工作大概如下:
    树上的节点定义为://gs为以这个字符为结尾的单词个数

    插入代词:

    如果插入四个词,aa,aa,ab,bc(当然,这四个不属于所要求的单词,这里只是举个例子),那么树就会变成:

    那么我们就可以通过遍历这棵树来找出所有单词的出现次数。
  • 4 统计词频最大10个单词: 遍历字典树,遇到节点gs不为0就加入SET中,由于SET的排序是从小到大的,而我们要词频大的优先,所以我们只需要把gs变成负数即可,如果SET中数据个数大于10个,就删除最后一个。

整个功能被我封装成一个类:

各个函数之间的依赖关系:

计算模块接口部分的性能改进


经行性能测试时,样例输入文件里有一百万个字符,两十万个单词,其中有十万个不同的单词。花费时间是4.95秒


可以看到main函数占了99%的执行时间,在main函数中调用的几个函数中,CountWord()占用了31.98%的执行时间,CountMxWord()占用了29.53%的执行时间。

从算法的复杂度而言,这个程序的时间复杂度已经达到了我当前知识的最优了,要优化只能从一些小东西上入手,比如把c++的string改成char,cout改成print,少用c++的东西,因为比较慢。

计算模块部分单元测试展示


单元测试我构建了十一个样例,主要都是测试其功能的,下面展示部分测试:


		TEST_METHOD(Testword)
		{

			A->init();
			A->MYscanf("2.in");
			A->eft_word = A->CountWord();
			Assert::AreEqual(A->eft_word,(int) 3);
			// 测试统计单词数是否正确
		}
		TEST_METHOD(Testmxword1)
		{
			A->init();
			A->MYscanf("3.in");
			A->eft_word = A->CountWord();
			A->CountMxWord();
			A->it = A->qur.begin();
			Assert::AreEqual(-A->it->first, (int)2);
			Assert::AreEqual(A->it->second,(string) "qweee");
			// 测试第一个最大词频单词
		}

		TEST_METHOD(Testmxword5)
		{
			A->init();
			A->MYscanf("11.in");
			A->eft_char = A->CountChar();
			Assert::AreEqual(A->eft_char, (int)1000000);
			A->eft_word = A->CountWord();
			Assert::AreEqual(A->eft_word, (int)200000);
			A->CountMxWord();
			A->it = A->qur.begin();
			Assert::AreEqual(-A->it->first, (int)2);
			Assert::AreEqual(A->it->second, (string) "aaaa");
			// 输入一百万个字符,二十万个单词,其中有十万种不同单词,用于测试性能
		}

展示出来的第三个样例也就是我上面性能测试的样例

全部单元测试运行结果如下:

代码覆盖率:

代码大部分都覆盖到了,就差个文件名出错时的代码没覆盖到。

异常处理


目前我只处理了三种情况,一种是输入错误文件名或文件不存在的情况,如下:

                //测试单元
		TEST_METHOD(Test4)
		{
			A->init();
			A->MYscanf("6.in");
			A->eft_char = A->CountChar();
			Assert::AreEqual(A->eft_char, (int)0);
			A->eft_word = A->CountWord();
			Assert::AreEqual(A->eft_word, (int)0);
			// 文件不存在。
		}
               处理代码:
		fp=freopen(s, "r", stdin);
		if (fp == NULL) {
			cout << "Error:Cannot open the file";
			error = 1;
			return;
		}

还有两种,分别是命令行参数过少或过多,处理如下:


int ifargcisok(int argc){
    if(argc<=10){
        printf("Error:Please input filename.\n");
        return 0;
    }else if(argc>2){
        printf("Error:Please do not enter too many parameters.\n");
        return 0;
    }else return 1;
}

总结和感想


一开始做这个项目感觉这个项目很简单,没想到做起来如此之麻烦,和刷题真的是天差地别,刷题你不需要考虑什么封装啊,什么可读性啊,只要求运行速度足够快,代码足够简洁,能A就行,但做这种项目要考虑很多东西,还有各种测试,各种分析。我在这方面真的欠缺很多,比如看到刚看到题目,我想好了要用什么样的方法,然后我就直接莽过去写了,其实有很多地方没有考虑到,比如要如何封装,异常处理啊,等到全写完后才回过头来发现一些东西还没有覆盖到,然后又是各种添加新代码。
git是个很好用的开源社区,好久没用了,导致上传代码发生各种错误,还需继续学习。
通过这次个人项目,我学到了很多,也明白了项目和acm是有比较大的区别的,我将继续学习,为以后做大型的项目打下良好的基础。

posted on 2018-09-10 12:12  QWE8  阅读(210)  评论(0编辑  收藏  举报

导航