福大软工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. 在文件读入时,为了能读出换行符'\n',我选择采用按字节读入的方法。读入的多个字符按行拼装成一个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("\n"));
lines.push_back(string("\n"));
lines.push_back(string("\n"));
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\n"));
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\n"));
lines.push_back(string("a v4w\n"));
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的顺序执行,或许与程序的栈结构有关。

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

posted @ 2018-09-10 13:46  cbattle  阅读(507)  评论(2编辑  收藏  举报