Github项目地址→ https://github.com/fifixpy/personal-project
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | 690 | 940 |
· Analysis | · 需求分析 (包括学习新技术) | 30 | 120 |
· Design Spec | · 生成设计文档 | 30 | 30 |
· Design Review | · 设计复审 | 30 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 10 |
· Design | · 具体设计 | 60 | 90 |
· Coding | · 具体编码 | 360 | 360 |
· Code Review | · 代码复审 | 30 | 180 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 480 |
Reporting | 报告 | 120 | 60 |
· Test Repor | · 测试报告 | 60 | 30 |
· Size Measurement | · 计算工作量 | 30 | 15 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 15 |
| | 合计 | 840|1300
解题思路
-
题目要求如下
① 统计字符数
② 文件的单词总数(至少以4个英文字母开头,跟上字母数字符号,单词以分隔符分割,不区分大小写)
③ 文件的有效行数(任何包含非空白字符的行)
④ 文件个单词的出现次数(输出频率最高的十个) -
暑假做了不少文件读写还有命令行传参的例子,在复习c++的文件输入输出没花什么时间,直接进入正题。
-
对于问题一和问题三,思路简洁明了,最暴力的做法当然是直接按字符读取一遍再按行读取一遍,但是在按行读取时发现getline在读取连续换行符会出错因此改用按字符读取,于是改用字符读取。最后的思路是按字符读取,设置一个标志位判断两个' '符之间是否有有效字符来统计有效行。需要注意的是判断最后一行是否是有效行。
-
问题二四思路也很暴力清晰,在离开了数据结构之后我对算法性能的追求几近没有,最直接的思路就是按字符串读取文本→判断有效单词→map存储词频→排序输出。
写完后发现我又看错了题目,询问助教后我才明白跟上字母数字符号的符号是指字母符号和数字符号,所有除了空白符以外的字符都是分隔符。
又要重新改代码,最后的思路还是以字符串读取,先遍历整个字符串,储存字符串中分割符的位置。然后再从头读这个字符串,在满足有效单词的要求下,分隔符截取字符串再放到map中。然后map转存在vector里调用sort函数快排。
更新:改进快排算法,选择遍历存词频的vector十遍及其以下。选择输出词频最高的那个词存储。
设计实现过程
代码文件组织
031602444
|- src
|- WordCount.sln
|- WordCount
|- CountAndSort.cpp
|- CountAndSort.h
|- CountCharacters.cpp
|- CountCharacters.h
|- CountLines.cpp
|- CountLines.h
|- CountWords.cpp
|- CountWords.h
|- main.cpp
|- WordCount.vcxproj
关系图
关键函数流程图
统计行数
统计词频
关键代码
int lines = 0;
char c;
int lineflag = 0;
while (f.get(c))//统计行数
{
if (c != ' ' && c != ' ' && c != '
')
{
lineflag = 1;
}
else if (c == '
'&&lineflag == 1) {
lines++;
lineflag = 0;
}
}
if (lineflag == 1)lines++;
void CountAndSort(char* filemm,vector<pair<string, int>>& v)//统计词数词频
{
map<string, int> mapp;
string s;
vector<int> ans;//存分隔符位置
ifstream f;
f.open(filemm, ios::in);
while (f >> s) //一次读取一个字符串,读取字符串不包括换行和空格和制表符
{
ans.clear();
for (int i = 0; i < s.size(); i++)
{
if (s[i] >= 65 && s[i] <= 90)
{
s[i] += 32;
}
if (s[i] < 48 || (s[i] > 57 && s[i] < 65) || (s[i] > 90 && s[i] < 97) || s[i]>122)
{
ans.push_back(i);
}
}
if (ans.size() == 0)//如果分割符数目等于0,就是只有一个字符串
{
//如果从该符号起四个字符都是字母
if ((s[0] >= 97 && s[0] <= 122) && (s[1] >= 97 && s[1] <= 122) && (s[2] >= 97 && s[2] <= 122) && (s[3] >= 97 && s[3] <= 122))
{
mapp[s]++;
}
continue;
}
//否则就有一个以上的分隔符
if ((s[0] >= 97 && s[0] <= 122) && (s[1] >= 97 && s[1] <= 122) && (s[2] >= 97 && s[2] <= 122) && (s[3] >= 97 && s[3] <= 122))
{
string temp(s.substr(0, ans[0]));//满足前四个字符是字母,截取
mapp[temp]++;
}
for (int i = 0; i < ans.size(); i++)
{
//满足分隔符后四个字符是字母
if ((s[ans[i] + 1] >= 97 && s[ans[i] + 1] <= 122) && (s[ans[i] + 2] >= 97 && s[ans[i] + 2] <= 122) &&
(s[ans[i] + 3] >= 97 && s[ans[i] + 3] <= 122) && (s[ans[i] + 4] >= 97 && s[ans[i] + 4] <= 122))
{
string temp(s.substr(ans[i] + 1, ans[i + 1] - ans[i] - 1));
mapp[temp]++;
}
}
}
- 思路都是很容易就能想到的。也想过快排所有单词跟只输出十个最高频单词的性价比,
但是想了一会觉得太麻烦就干脆直接进行下一步。看了一会其他同学的博客作业感觉给我点时间好好研究下应该能学到不少。 - 博客更新:改进快排算法,选择遍历存词频的vector十遍及其以下。选择输出词频最高的那个词存储。
核心代码如下
vector<pair<string, int>> v(mapp.begin(), mapp.end());//词频排序
for (int i = 0; i < mapp.size(); i++)
{
if (i==10) break;
int max = 0;
string maxword;
int enflag = 0;
for (vector<pair<string, int>>::iterator vec = v.begin(); vec != v.end(); vec++)
{
if (vec->second > max)
{
max = vec->second;//存下当前最大数单词
maxword = vec->first;
}
else if (vec->second == max)//字典序
{
if (vec->first < maxword)
{
max = vec->second;//存下当前最大数单词
maxword = vec->first;
}
}
}
if(max) x.push_back(make_pair(maxword, max));//存入
for (vector<pair<string, int>>::iterator vec = v.begin(); vec != v.end(); vec++)
{
if (vec->first == maxword)
{
vec->second = -1;//如果是输出过的单词就将其词频置-1
break;
}
}
}
- 接口的设计我一开始还没懂要怎么用,只是把函数拿出来分成.h和.cpp文件简单封装了一下。等到单元测试的时候我发现CountAndSort函数被我仅仅用于来输出,在测试词频和排序时不好使用,于是干脆将输出和计算函数拆开,修改了一下CountAndSort函数接口,直接引用容器放到函数计算。
测试样例
性能改进
- 将main函数循环10000次,用时10.138秒,由于封装时重新读取文件占了大部分时间外(这也不可避免),把输出形式从改成printf时间降低到7.128秒。消耗最大的函数是CountCharacters也就是统计词频函数,占用11.69%。
性能分析图
单元测试
- 设计了十个单元测试分别是:
统计字符个数、统计单词个数、大小写单词测试、按词频输出、词频相同保持字典序输出、最多输出十个单词、综合统计行数、文本只有空白符时统计有效行数、综合排序、空文件。
部分单元测试代码
namespace SortCountTest1//按字典序输出
{
TEST_CLASS(UnitTest1)
{
public:
TEST_METHOD(TestMethod1)
{
char f[] = "D://test//SortCountTest1.txt";
vector<pair<string, int>> v;
CountAndSort(f, v);
vector<pair<string, int>>::iterator vec = v.begin();
Assert::IsTrue((vec)->first == "aaaa" && (vec + 1)->first == "bbbb" &&
(vec + 2)->first == "cccc" && (vec + 3)->first == "dddd");
}
};
}
namespace SortCountTest2//最多输出十个
{
TEST_CLASS(UnitTest1)
{
public:
TEST_METHOD(TestMethod1)
{
char f[] = "D://test//SortCountTest2.txt";
vector<pair<string, int>> v;
CountAndSort(f, v);
int num = Display(v);
Assert::IsTrue(num == 10);
}
};
}
- 在单元测试花的时间非常多,一是学习单元测试的使用,二是改进函数接口便于单元测试,再就是通过单元测试发现了bug重新修改代码。
- 之前看《构建之法》没理解单元测试干嘛用的后来知道干嘛用的还是不能理解为什么要花时间写单元测试。经过这次艰难的测试,才发现单元测试的性价比在于花少量时间写测试代码而节约大量测试时间。我觉得我收获不少,首先是学会了写单元测试,也应该会在以后必要的时候用上单元测试。
单元测试结果
代码覆盖率结果
主函数里有一些异常处理没有覆盖到。关于异常处理的单元测试如何使用还待解决。
异常处理说明
异常处理代码
//输入文件为空
if (argv[1] == NULL)
{
printf("请输入文件路径
");
return -1;
}
//输入多个文件
if (argc > 2)
{
printf("输入文件过多
");
return -1;
}
//输入文件不是txt形式
int len = strlen(argv[1]);
if (!(*(argv[1] + len - 1 == 't')&&
*(argv[1] + len - 2 == 'x')&&
*(argv[1] + len - 3 == 't')))
{
printf("输入文件不是txt形式
");
return -1;
}
//输入无效文件
ifstream f;
f.open(argv[1], ios::in);
if (!f)
{
printf("无法打开文件
");
return -1;
}
f.close();
心得体会
-
这次的作业对我来说花在想思路、完成大致代码的时间不算很多,甚至优化的时间也没占多大比例(因为我菜),想想大多数时间都有点浪费,一直回头返工的感觉。比如说几次理解错题意,回头改代码,单元测试时又发现bug,回头改代码。在代码复审和测试方面花了比预计多得多的时间。以后应该会在设计和设计复审阶段花更多的时间。
-
暑假看《构建之法》前几章没怎么读懂,真正实践并强迫自己使用这些测试规范方法才有点恍然大悟的感觉。完成了第一次个人项目,如果说只是打代码,其实跟以前做的作业没什么不同,这次作业告诉我原来除了得到正确结果外还有很多东西要做。
-
在算法上还要好好努力。
-
花了接近一天的时间学习使用git,大概会用了。