1. 写在前面
今天是新生周的最后一天,意味着自己班级里的小朋友的接触也要告一段落了。心中也有那么一丝丝的不舍。好在自己终于完成了软工的第二次作业,也算是给自己一个安慰吧。开学的这一周让我知道了,软工实践是如何“充实”自己的大学生活的。在这里也要感谢一下班里的陈俞辛、董钧昊、蔡宇航同学,感谢他们在我遇到问题的时候给予我的帮助!
PSP表格
psp2.1 | personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 50 |
Estimate | 估计这个任务需要多少时间 | 1000 | 1680 |
Development | 开发 | 180 | 70 |
Analysis | 需求分析(包括学习新技术) | 180 | 150 |
Design Spec | 生成设计文档 | 20 | 20 |
Design Review | 设计复审 | 20 | 15 |
Coding Standrd | 代码规范(为目前的开发制定合适的规范) | 15 | 10 |
Design | 具体设计 | 30 | 25 |
Coding | 具体编辑 | 150 | 250 |
Code Review | 代码复审 | 30 | 30 |
Test | 测试(自我测试,修改代码,提交修改) | 30 | 40 |
Reporting | 报告 | 60 | 50 |
Test Repor | 测试报告 | 20 | 15 |
Size Measurement | 计算工作量 | 30 | 20 |
Postmortem&Process Improvement Plan | 事后总结,并提出过程改进计划 | 20 | 15 |
- | 合计 | 665 | 710 |
2. 解题思路
- 本次作业的四个要求
- 统计字符
- 统计有效行数
- 统计单词数
- 统计词频
2.1 统计字符
统计字符数:只需要统计Ascii码,汉字不需考虑,
空格,水平制表符,换行符,均算字符。
- ==只需考虑可视字符 (ASCII:32-126) 、水平制表符 (ASCII:9) 、换行符 (ASCII:10) 。==
- 首先通过定义一个
fstream
对象来打开文件,使用get()
方法来获取字符,用eof()
方法来判断文件是否已经读完。fstream
的使用参见:参考博客一 - 监测到满足要求的字符就令计数器
cnt
自增加一,并定义一个string
变量来存储==该文件中的所有字符。==
2.2 统计有效行数
统计文件的有效行数:任何包含非空白字符的行,都需要统计。
- 首先想到的就是换行符
\n
,但是==检测到到换行符不代表就是有效行,没有换行符也不代表不是有效行。==
- 所以我采用一个
flag
变量,检测到有效字符置flag
为1,否则置0,然后对文档的所有字符进行扫描。 - 当
flag
为1且检测到换行符,行数加一。扫描完整个文件后,检测flag
的值,如果为1,代表最后一行是“没有换行符的”有效行,行数自增加一。
2.3 统计单词数
统计文件的单词总数,单词:至少以4个英文字母开头,跟上字母数字符号,单词以分隔符分割,不区分大小写。
- 这里我采用了多重循环检测的方式,判断是否为有效单词。如果是的话就将它插入哈希表,以供之后的统计词频功能使用。
- 后来查阅资料的时候了解到了正则表达式匹配的方法,可惜没有时间来实现,但是以后自己也会学习相关的知识。
2.4 统计词频
统计文件中各单词的出现次数,最终只输出频率最高的10个。频率相同的单词,优先输出字典序靠前的单词。
- 由于是按照字典序来排序并统计出现次数,所以我定义了一个结构体。
struct node {
string name;//名
int times;//出现频次
node *next;
node(string n, int number)
{
name = n;
times = number;
next = NULL;
}
};
name
用来存放单词字符,times
用来存放出现次数,next
用来连接节点,实现开散列。- 根据单词的字母组成来构造哈希函数如下
int hash = ((w[0] - 96)) + ((w[1] - 96) * 26) + ((w[2] - 96) * 26 * 26);
- 根据哈希值为每个检测到的单词新建一个节点,并插入到开散列中。
- 调用插入函数
insert()
,当该单词的哈希值对应的散列单元存在节点,则插入到该单元下的链表中,并使times++
。 - 如果不存在节点直接接在散列单元上即可。
- 所有单词插入完毕后。扫描十次散列表,每次都选出
times
最大的一个节点,并将它删除。得益于散列函数,不必排序。
3. 实现过程
- 通过这次作业让我学到,好的封装可以大大提高编码效率!
- 因为自己着急打代码的原因,导致封装花了很多时间!
3.1 类的概述
- 为了实现上述功能,我写了四个类:
char_counter
:int char_count();
:负责统计字符数。
file
:负责存储文件的相关信息,比如文件名,文件中的所有字符。line_counter
:int lines_counter();
:统计有效行数
word_operater
:void insert();
:单词插入哈希表。int words_counter();
:统计单词数。void file_rank();
:负责统计词频。
3.2 GitHub仓库组织
031602523
|- src
|-WordCount.sln
|-WordCount
|-Char_counter.cpp
|-Line_counter.cpp
|-Word_operater.cpp
|-main.cpp
|-char_cnt.h
|-file.h
|-line_cnt.h
|-pre.h
|-word_op.h
|-stdafx.h
|-targetver.h
|-stdafx.cpp
|-unittest1.cpp
3.3 类之间的关系
3.4 程序流程图
4.关键代码
- 我觉得单词提取和排序代码是重中之重,再此贴上两段程序的代码。
4.1 单词提取
int Word_operater::words_counter(ifstream &f, Files &fn)
{
int flag = 0;
string thisword = "";
string temp = fn.get_alstring();
int len = temp.length();
int cnt = 0;
for (int i = 0; i < len; i++)
{
if ((temp[i] >= 65 && temp[i] <= 90) || (temp[i] >= 97 && temp[i] <= 122))//找到第一个字母 判断是不是单词
{
flag = 0;
for (int j = i; j <= i + 3; j++)
{
if (temp[j] <= 64 || (temp[j] >= 91 && temp[j] <= 96) || temp[j] >= 123 || len - i < 4)
{
flag = 1;
break;
}
}
if (flag == 0)//如果是单词就提取单词到thisword
{
thisword = "";
for (; i < len && ((temp[i] >= 65 && temp[i] <= 90) || (temp[i] >= 97 && temp[i] <= 122) || (temp[i] >= 48 && temp[i] <= 57)); i++)
{
if (temp[i] >= 65 && temp[i] <= 90)
temp[i] += 32;
thisword += temp[i];
}
cnt++;
insert(thisword);
}
else//如果不是单词就跳到下一个单词的第一个字母
{
for (; (temp[i] >= 65 && temp[i] <= 90) || (temp[i] >= 97 && temp[i] <= 122) || (temp[i] >= 48 && temp[i] <= 57); i++) {}
}
}
else if (temp[i] >= 48 && temp[i] <= 57)
{
for (; (temp[i] >= 65 && temp[i] <= 90) || (temp[i] >= 97 && temp[i] <= 122) || (temp[i] >= 48 && temp[i] <= 57); i++) {}
}
}
fn.set_alstring(temp);
return cnt;
}
4.2 词频统计
void Word_operater::file_rank(Files &fn, Word_operater &wn, ofstream &outfile)//统计词频
{
int num;
int flag = 0;//判断出现次数最大的结点是不是表首 0不是 1是
node *max, *q, *p, *front_max;
front_max = new node("", 0);
for (int j = 0; j < 10 && j < wn.get_wrdcnt(); j++)//遍历10次哈希表
{
max = new node("", 0);//初始化max
for (int i = 0; i <= 18279; i++)
{
if (this->hash_table[i]->next == NULL) continue;//空表跳过
else//非空表
{
q = p = this->hash_table[i];
while (p->next != NULL)
{
if (p->times > max->times || (p->times == max->times&&p->name < max->name))
{
if (p == this->hash_table[i])
{
flag = 1;//表示该单词在表头
num = i;
}
else flag = 0;//表示该单词在表中
max = p;
front_max = q;
}
q = p;
p = p->next;
}
}
}
if (max->times != 0)
{
//cout << "<" << max->name << ">:" << max->times << endl;//输出一个结果
wn.word_times[j] = max->times;
wn.word_str[j] = max->name;
//cout << wn.word_times[j] << " " << wn.word_str[j] << endl;
outfile << "<" << max->name << ">:" << max->times << endl;//输出一个结果
}
else break;//如果max没有被替换,则此时哈希表是空的,不需要输出
if (flag == 1) this->hash_table[num] = max->next;//如果频次最大的单词在表首,替换表首指针
else front_max->next = max->next;//如果频次最大的单词在表中,删除结点
}
return;
}
5. 分析与测试
- 这是我本次作业的最大收获之一,除了学会了封装,还初步学会了
vs
的一些功能。
5.1 单元测试
- 这里给出一个测试点的代码。
TEST_METHOD(TestMethod1)
{
ifstream f;
Files file_input;
int u = 1;
Char_counter cc;
Line_counter lc;
Word_operater wo;
ofstream outfile;
string std[10];
int std1[10];
int a, b, c;
file_input.set_filename("input1.txt");
f.open("input1.txt", ios::in);
if (!f.is_open())
{
cout << "Warning! can't open this file!" << endl;
}
a = cc.char_count(f, file_input);
b = lc.lines_counter(f, file_input);
c = wo.words_counter(f, file_input);
cc.set_chrcnt(a);
lc.set_lnecnt(b);
wo.set_wrdcnt(c);
wo.file_rank(file_input, wo, outfile);
Assert::AreEqual(1560, a);
Assert::AreEqual(29, b);
Assert::AreEqual(98, c);
std[0] = "gwsw9c4";
std[1] = "iqbl9b8";
std[2] = "jrim";
std[3] = "bvjb";
std[4] = "dfcmb7";
std[5] = "does9x";
std[6] = "eshwh6";
std[7] = "gkcu";
std[8] = "jawe5jh";
std[9] = "jseb50l";
std1[0] = 9;
std1[1] = 6;
std1[2] = 6;
std1[3] = 5;
std1[4] = 4;
std1[5] = 4;
std1[6] = 4;
std1[7] = 4;
std1[8] = 3;
std1[9] = 3;
//int *p1 = wo.get_w_times();
//string *p2 = wo.get_word_str();
for (int i = 0; i < 10; i++)
{
Assert::AreEqual(std[i],wo.word_str[i]);
Assert::AreEqual(std1[i], wo.word_times[i]);
}
//Assert::AreEqual(1560, a);
//Assert::AreEqual(29, b);
//Assert::AreEqual(98, c);
}
};
5.2 性能分析
- 选择一份文本循环进行10000次测试。主要的时间损耗在单词匹配和输出。
5.3 代码覆盖率
- 测试所得代码覆盖率为
95%
.
作业心得
- 本次作业虽然说不难,但是每一个部分都需要精心的设计与准备。由于自己担任了新生班导的工作,在时间上有些力不从心。我尽全力挤出了一天可用的时间来完成作业,终于在快接近ddl的今天完成了最后的要求。可能是自己的时间安排不合理,没有一个完整的规划,也要提高自己的效率。最后发表一下自己的感慨啊:软工实践真的可以充实自己的每一天。
- 因为自己好久都没有编码,对C++也不是那么的熟悉,许多东西也是从博客中慢慢学习,请教同学,慢慢的掌握与熟悉。这个过程,让自己复习了许多变得生疏的知识,也提醒着自己有很多知识的盲区。日后也要复习并学习一些相关知识,备战最后的团队作业!
- 看了邹欣老师的构建之法,也拜读了一些大牛的博客,觉得这次作业的目的不是完成那些功能,而是学习软件工程中的重要思想,比如封装,这个是我的亲身经历,好的封装一定可以提高编程时的效率。面向对象的思想,也让编码看起来不那么枯燥,比如我想要统计字符数,我就实例化一个“字符统计机”把东西都给他,他就会马上给我结果。是不是觉得很生动哈哈哈。同时本次作业的第二个目的,我觉得是让我们熟悉
VS
这个“宇宙第一ide”,以前觉得VS
与dev-c++
没事么不同,这样用来才发现其中的不同。 - 在阅读构建之法的时候,邹欣老师提出了一系列程序员存在的问题。反思自身,发现这些问题在自己身上也有出现。就比如编码前,不去思考,直接上去就打代码。没有构思的过程,写出的程序很乱,可读性也不好,在封装的时候也花费了我很多的时间。在以后的作业中,我会专门用一些时间来规划整个程序的架构,这样写起代码也会得心应手很多。正如柯老师说的那样“最不会打代码的人,才着急去打代码”。