• 福大软工1816 · 第二次作业


    1. Github项目地址。

    Github项目地址



    2. 给出PSP表格。

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



    3. 解题思路描述。即刚开始拿到题目后,如何思考,如何找资料的过程。

    解题思路

    1. 本任务的要求是统计给定文件中的字节数、单词数、行数、最高频的10个单词
    2. 从main函数的argv中读出传入参数作为文件名。
    3. 在文件读入时,为了能读出换行符' ',我选择采用按字节读入的方法。读入的多个字符按行拼装成一个string,所有行(string)组成一个vector返回,记为vector file_lines。
    4. 对file_lines进行遍历,在遍历过程中可以简单地统计出字节数、单词数、行数。统计单词数可以采用正则表达式,我没用正则表达式,而是自己手写判断规则,以获得更高的效率。
    5. 遍历file_lines时,以单词作为key,出现次数为value建立map<string,int>。要统计N个单词中最高频的n个单词,有两种做法:

    (1)种是使用诸如快排、堆排的排序算法进行排序,取出前n个。(其中,堆排比快排在极端情况下表现更好)。由于涉及到对字符串的排序,移动字符串较为耗时,可以使用引用或指针代替。时间复杂度O(N*log(N))

    (2)对序列遍历n次,可以取出排名前n个的单词。时间复杂度O(N*n)

    本题目n=10,在N<100的情况下(1)更好,N>100情况下(2)更好。在N<100时,虽然(2)比(1)更耗时,但由于问题规模较小,时间也不会多多少,是可接受的。权衡二者,我采用第(2)中方法统计最高频的10个单词。

    1. 最后将结果输出即可。



    4. 设计实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?单元测试是怎么设计的?

    • 由于作业要求,我要按照以下格式组织代码
    031602502
    |- src
       |- WordCount.sln
       |- WordCount
           |- pch.h
           |- myIO.h
           |- Statistics.h
           |- pch.cpp
           |- myIO.cpp
           |- Statistics.cpp
           |- WordCount.cpp
           |- WordCount.vcxproj
       |- WordCountUnitTest
           |- stdafx.h
           |- targetver.h
           |- stdafx.cpp
           |- unittest1.cpp
    
    • main在WordCount.cpp中
    • 用于统计的类Statistics,封装了getCharNumber、getWordNumber、getLineNumber、getTopWords方法;
    • myIO命名空间,包含了用于IO的GetFileNameFromArgs、ReadFileLines、Output方法。

    过程

    计算文本有效行数

    计算最高频的10个词

    单元测试

    • 在运行单元测试前,确保先以Debug x86方式运行过程序,确保WordCount/Debug目录下有对应的.obj文件
    • 新建本机测试项目,引用WordCount项目,手动设置测试项目的附加依赖项(到.obj文件的相对路径),将被测试项目产生的所有 obj 文件路径写到附加依赖项中。
    • 对于Statistics类的测试

    1.采用unittest1.cpp中写的的DataGenerator类随机生成文本内容和标准结果。
    构造测试数据,是以字符为最小单位。用随机的字符组成随机的单词,加上随机的分隔符。若干个随机单词拼成一行。若干行拼成整个文本内容。为保证单词重复出现,设计新单词有20%的概率从以前出现的单词中取。此外,增大了空格在分割符中的使用比例。构造测试数据的过程中,可以得到字符数、单词数、行数、最高频词。将标准结果与通过Statistics类计算出来的数据进行比较,以此来进行单元测试。可通过改变以下宏变量来修改随机测试的参数。

    #define UNIT_TEST_CASE 20
    #define MAX_LINE_NUM 50
    #define MIN_LINE_NUM 0
    #define MAX_LINE_LEN 20
    #define MIN_LINE_LEN 0
    #define MAX_WORD_LEN 8
    #define MIN_WORD_LEN 0
    

    DataGenerator类定义

    测试代码

               srand(unsigned int(time(0)));
    			DataGenerator dg;
    			for(int i=0;i < UNIT_TEST_CASE;i++)
    			{
    				vector<string> lines = dg.generateData();
    				Statistics st(lines);
    				vector<map<string, int>::iterator> st_top10 = st.getTopWords(10);
    				vector<map<string, int>::iterator> dg_top10 = dg.getStdTop10();
    				dg.outputDataToFile("input.txt");    //输出随机测试用例到文件
    				dg.outputStdAnsToFile("std_ans.txt");//输出测试标准答案到文件
    				Assert::AreEqual(dg.getStdCharNum(), st.getCharNumber());
    				Assert::AreEqual(dg.getStdWordNum(), st.getWordNumber());
    				Assert::AreEqual(dg.getStdLineNum(), st.getLineNumber());
    				for (unsigned int i=0;i< dg_top10.size();i++)
    				{
    					Assert::AreEqual(dg_top10[i]->first, st_top10[i]->first);
    					Assert::AreEqual(dg_top10[i]->second, st_top10[i]->second);
    				}
    			}
    

    其中一个测试的例子

    2.对于一些特定的数据,可以采取手动录入,人工比较的方式。

    vector<string> lines;
    //空文件
    lines.clear();
    lines.push_back(string(""));
    Assert::AreEqual(0, Statistics(lines).getCharNumber());
    Assert::AreEqual(0, Statistics(lines).getWordNumber());
    Assert::AreEqual(0, Statistics(lines).getLineNumber());
    //------------------------------------------------------------
    //全是空行
    lines.clear();
    lines.push_back(string("
    "));
    lines.push_back(string("
    "));
    lines.push_back(string("
    "));
    Assert::AreEqual(3, Statistics(lines).getCharNumber());
    Assert::AreEqual(0, Statistics(lines).getWordNumber());
    Assert::AreEqual(0, Statistics(lines).getLineNumber());
    //------------------------------------------------------------
    //最后一行没有换行
    lines.clear();
    lines.push_back(string("asdf123
    "));
    lines.push_back(string("a v4w"));
    Assert::AreEqual(13, Statistics(lines).getCharNumber());
    Assert::AreEqual(1, Statistics(lines).getWordNumber());
    Assert::AreEqual(2, Statistics(lines).getLineNumber());
    //------------------------------------------------------------
    //最后一行有换行
    lines.clear();
    lines.push_back(string("asdf123
    "));
    lines.push_back(string("a v4w
    "));
    Assert::AreEqual(14, Statistics(lines).getCharNumber());
    Assert::AreEqual(1, Statistics(lines).getWordNumber());
    Assert::AreEqual(2, Statistics(lines).getLineNumber());
    
    • 对于myIO命名空间内函数的测试

    主要是测试异常处理。手工构造数据,进行测试。

    测试结果

    覆盖率



    5. 记录在改进程序性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。

    • 数据量较小时,所花费的时间大部分在IO上。若要提高性能,可以用C语言的IO方法(如scanf、printf、fopen、fclose)代替C++的IO的方法(如cin、cout、ifstream、ofstream)

    • 数据量较大时,IO占用的时间比例就较小。为了提高性能:

    1. 调用Statistics类的方法进行统计功能时,采用引用或指针的方式传入文本内容,避免对较为庞大文本内容进行再一次拷贝,避免时间和空间上的浪费。

    2. 由于计算字节数、单词数、行数均可在一次遍历统计中完成,所以我一个变量来标记是否进行过统计,若执行过,则直接返回计算结果,避免重复计算。

    3. 在统计最高频的10个单词时,我采用遍历10次来取出最高频的10个词的方法代替排序的方法。虽然在数据量N较小时,使用排序的方法时间会更少(O(Nlog(N))),但是由于本题是统计前n=10的词,在数据量N较大时,采用遍历的方法更好(O(Nn))。在数据量N较小时,遍历的方法花略微多一点时间,但这是可接受的。

    4. 采用map的方式组织单词和对应的出现次数。map的实现是采用红黑树(一种非严格意义上的平衡二叉树),数据的插入、删除、查找均只要O(logn)。使用map的方式组织数据,数据插入完成后,单词就已按照字典序排好。

    5. 统计最高频的10个词时,统计结果以vector<map<string,int>::iterator >&返回,避免对map中的key-value再次拷贝。要注意的是要还原统计过程中通过iterator对value值进行的修改。

    对于以下文本内容,重复执行main1000次

    运行结果


    改进

    重新运行分析,测试1000遍结果

    使用C风格IO

    分析:

    文本内容较少时,消耗最大的是用于IO的函数,这时采用C风格的IO函数(fopen、fprintf、fclose等)代替C++风格的IO函数(ifstream和ofstream)可以显著提高性能。

    当文本内容较多时,IO占用的时间比例减少,消耗最大的是用于计算的函数。
    我改进map的插入方式(value类型为int,初始值为0,使用直接map[key]++代替先查找再自增),避免2次查找。
    改变string插入方式,由string.insert(string.end(), char)改为string += char
    通过以上调整,重新测试。性能提升30%



    6. 代码说明。展示出项目关键代码,并解释思路与注释说明。

    关键代码说明:

    1.统计功能,对字节数、单词数、行数进行统计。

    • 去除只含有空白字符的空行采用的方法是:判断一行中每个字符ascll码值是否大于32,若大于,则为非空字符,否则为空字符。

    • 统计单词的方法是:按照字符遍历,将字符拼装成单词,以分隔符或者EOF为界限。

    • 计算完成后,用calculated标记计算状态,避免对相同的数据重复计算。

    void Statistics::calc()//统计功能,可不必手动调用
    {
    	for (unsigned int i = 0; i < m_file_lines.size(); i++)
    	{
    		const string &thisline = m_file_lines[i];
    		m_char_num += thisline.size();
    
    		bool is_a_line = false;
    		for (unsigned int k = 0; k < thisline.size(); k++)//判断是否为空白行
    		{
    			if (thisline[k] > 32)//大于32为非空白字符
    			{
    				is_a_line = true;
    				break;
    			}
    		}
    		if (!is_a_line)
    			continue;
    
    		m_line_num++;
    		unsigned int j = 0;
    		while (j <= thisline.size() - 1)//统计单词个数
    		{
    			char c = thisline[j];
    			if (isLetter(c))
    			{
    				string word;
    				word.insert(word.end(), c);
    				j++;
    				if (j <= thisline.size() - 3)
    				{
    					while (j <= thisline.size() - 1 && (isLetter(thisline[j]) || isNumber(thisline[j])))
    					{
    						word.insert(word.end(), thisline[j]);
    						j++;
    					}
    				}
    				if (word.size() >= 4 && isLetter(word[0]) && isLetter(word[1]) && isLetter(word[2]) && isLetter(word[3]))
    				{
    					m_word_num++;
    					if (m_wd_mp.find(word) == m_wd_mp.end())
    						m_wd_mp[word] = 1;
    					else
    						m_wd_mp[word]++;
    				}
    			}
    			else if (isNumber(c))
    			{
    				for (j++; j <= thisline.size() - 1 && (isLetter(thisline[j]) || isNumber(thisline[j])); j++);
    			}
    			else
    			{
    				j++;
    			}
    		}
    	}
    	calculated = true;
    
    	/*for (auto it = m_wd_mp.begin(); it != m_wd_mp.end(); it++)
    	{
    		cout << it->first << " " << it->second << endl;
    	}*/
    }
    

    2.获取出现次数排名前几的单词

    • 在执行本函数之前,要求对先执行统计功能。通过calculated进行判断。

    • 为避免越界,遍历次数n应取需求量(本题为10)与不同单词个数的最小值。

    • 进行n次遍历,每次取出当前出现次数最大的单词对应的迭代器,并通过迭代器将出现次数置为相反数,方便取下一个数。

    • 全部取出后,通过取出的迭代器对上一步修改的数据进行还原。

    vector<map<string, int>::iterator> &Statistics::getTopWords(unsigned int top_num)//获取出现次数排名前几的单词
    {
    	if (!calculated)
    		calc();
    	top_num = min(m_wd_mp.size(), top_num);
    	for (unsigned int i = 0; i < top_num; i++)
    	{
    		string maxstr;
    		map<string, int>::iterator maxit = m_wd_mp.begin();
    		for (map<string, int>::iterator it = m_wd_mp.begin(); it != m_wd_mp.end(); it++)
    		{
    			if (it->second > maxit->second)
    			{
    				maxit = it;
    			}
    		}
    		m_top_words.push_back(maxit);
    		maxit->second = -maxit->second;
    	}
    	for (unsigned int i = 0; i < m_top_words.size(); i++)
    	{
    		m_top_words[i]->second = -m_top_words[i]->second;
    	}
    	return m_top_words;
    }
    



    7.异常处理

    参数错误(参数过多)异常

    输入文件打开异常(无法打开输入文件)

    输出文件打开异常(无法打开输出文件)

    异常处理

    对异常处理的单元测试



    8. 结合在构建之法中学习到的相关内容与个人项目的实践经历,撰写解决项目的心路历程与收获。

    现在可以理解为什么那么多的同学会退这门课了。这门课要花很多的时间,周末的空闲时间几乎全在做这个了。但我不后悔选这门课,付出总是与回报成正比的,第一次的作业就让我学到很多。从开始的通过argc、argv读取命令行参数,到后来的SLT map的使用,iterator的使用;从函数封装到单元测试......此外,还有对变量、函数良好的命名规则,引用、指针的使用。以前虽然对这些略微了解,但是缺乏实践,在一些地方一知半解。通过这次作业,我更加深入的了解了这些知识。

    给我印象深刻的是,在和一起同学讨论交流实验的过程中,我们发现:比如fun(a(), b(), c()),调用函数fun(),函数a()、b()、c()执行的先后顺序是c -> b -> a,而不是我们所想象的a -> b -> c。这与函数默认参数的定义顺序有相似性(定义函数默认参数值时,只允许从后往前定义)。会按照c -> b -> a的顺序执行,或许与程序的栈结构有关。

    总体感受用一句话形容:虽然苦,但乐在其中。

  • 相关阅读:
    通过身份证号获取性别
    网线的制作方法
    duilib中的V和H布局中滚动条问题
    duilib中控件拖拽功能的实现方法(附源码)
    仿酷狗音乐播放器已开源!
    关于仿酷狗音乐播放器开源:寻求一套音乐播放器素材,让仿酷狗开源
    Richedit使用大全
    Redrain个人维护并使用的DuiLib和UiLib库源码下载地址
    vc编译器 msvcr.dll、msvcp.dll的含义和相关错误的处理
    duilib List控件,横向滚动时列表项不移动或者移动错位的bug的修复
  • 原文地址:https://www.cnblogs.com/cbattle/p/9607664.html
Copyright © 2020-2023  润新知