福大软工1816 · 第二次作业 - 个人项目
一.github地址
二.PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 10 | 30 |
Development | 开发 | 770 | 1700 |
· Analysis | · 需求分析 (包括学习新技术) | 120 | 360 |
· Design Spec | · 生成设计文档 | 60 | 60 |
· Design Review | · 设计复审 | 30 | 0 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | 20 |
· Design | · 具体设计 | 60 | 120 |
· Coding | · 具体编码 | 360 | 720 |
· Code Review | · 代码复审 | 60 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 360 |
Reporting | 报告 | 75 | 110 |
· Test Repor | · 测试报告 | 30 | 60 |
· Size Measurement | · 计算工作量 | 15 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 855 | 1840 |
三.解题思路
- 统计文件字符数
调用fstream逐个读取字符,中间使用noskipws来防止换行符和空格被丢弃,每读一个字符就按照ascii表的可视字符码值进行筛选,同时另外判断‘\n’和'\t'。 - 统计非空行数
line初始值设为0,设定一个初值为0的标志位flag,当读到非空字符时将其置1,当flag为1状态时读到‘\n’就line++并重置标志位,当所有输入完成之后还需要特别判断一次flag状态防止漏掉最后一行(没有回车的情况)。 - 统计单词数量
建立一个三单元的临时数组temp[3],同时用len来计数,只要是非字母出现在前4个就把len清0,如果字母恰好出现4次则一次读取4个字符到事先创建的结构体链表里同时对wordc++,之后在读到分隔符之前逐个读入,同时相应修改结构体内对应的变量。 - 统计十个高词频词
在将单词读入结构体链表中时修改结构体中的end(该前缀作为一个完整单词的次数)next[key](指向子结点的指针数组)ch(前缀字符串),从而建立一棵多叉树。之后对该多叉树进行遍历,当当前结点非空且end!=0的情况下,按照权值(end)大小和字典序进行运算符重载,把结构体压入优先队列中,在递归遍历之后取十次队列头得到高频词的相关信息(如果不到10个则有几个输出几个,以minfreq.empty()作为外层条件)。
四.代码实现过程
下面只截取部分代码用作功能实现介绍,部分缺省代码以注释形式体现,如果因此产生歧义还望指正。因为基础差完成时间较晚所以代码还没完成功能模块化,美观性较差。
读取文本部分
char getword;//用于读入字符 ifstream infile("D://readtxt.txt"); infile >> noskipws;//保存空格和换行符 while (!infile.eof()) { infile >> getword; if (infile.fail())//防止尾部重读,eof到下一次循环才返回1 break; charcount(getword);//调用行数和字符数计算模块 }
字符和非空白行计数部分
int flag=0;//该行出现非空字符则置1 void charcount(char getword)//统计字符数和行数 { if (getword == '\n')//计算行数 { if (flag)//判断是否为空白行 { line++; } flag = 0; charc++; } if (getword == '\t')//判断是否为制表符 charc++; if (getword >= ' '&&getword <= '~')//筛选ascii可视字符部分 { charc++; if (getword != ' ')//只要是非空可视字符就说明不是空白行 flag = 1; } } int main() { /*省略循环读取部分代码*/ if (flag)//读取结束后单独处理最后一行没换行符的情况 line++; /*省略后续其他部分代码*/ }
单词判断和计数部分
//单词预处理 if (getword >= 'A' && getword <= 'Z')//大小写切换 getword += 32; if (getword >= 'a' && getword <= 'z')//如果为小写即如果为字母 { len++;//初值为0的单词长度计数 } else if (len < 4)//如果字符非字母且之前还没形成单词则重新计数 { len = 0; continue; } else if (getword >= '0'&&getword <= '9')//如果之前已经形成单词则可以接纳数字 { len++; } else //说明遇到分隔符开启下一次计数 { len = 0; continue; } if (len == 4)//说明已经成为单词 { lastch = ""; wordc++;//wordcount计数 } if (len < 4)//当前为字母但还没形成单词继续循环 continue;
构建多叉树的结构体(用于存储单词方便后续频度统计)
struct wordtreeNode //字典树结构体 { wordtreeNode* next[36];//指向各子树的指针,包含0-9a-z int end;//标记该结点为结尾的单词频度 string ch;//存储该结点为止的字符串 wordtreeNode() : end(0)//构造函数赋初值 { for (int i = 0; i < 36; i++) { next[i] = NULL; } } }; bool operator < (wordtreeNode a, wordtreeNode b)//运算符重载,用于优先队列排序 { if (a.end == b.end) return a.ch > b.ch;//字典序 else return a.end < b.end;//频度从大到小 }
构建多叉树(存储单词)
wordtreeNode* root = NULL; wordtreeNode* head = NULL; root = new wordtreeNode();//初始化一棵树 int key;//链表子节点键值 int len = 0;//len=0开始计数 char temp[3];//用来存放判断单词过程的中间字符 char getword;//用于读入字符 string lastch = "";//临时存储前一个节点的字符串 ifstream infile("D://readtxt.txt"); infile >> noskipws;//保存空格和换行符 while (!infile.eof()) { infile >> getword; /*省略读入部分代码*/ if (getword >= 'a' && getword <= 'z')//若为字母则存入临时数组 { len++; if (len <= 3)//临时数组只放前三个 temp[len - 1] = getword; } /*省略部分判断代码*/ else //说明遇到分隔符开启下一次计数 { head->end++;//单词结束可以构成一个单词,频数++但是最后一个单词后面没有分隔符要特殊处理 head = root;//指针回到根部 len = 0; continue; } if (len == 4)//恰4个字母的时候做第一次单独的读入 { lastch = ""; //插入单词树根部 head = root;//指针指向根部 for (int i = 0; i <= 2; i++)//加入临时数组中的三个节点 { if (temp[i] <= '9'&&temp[i] >= '0')//key作为next[]的下标代表0~9和a~z key = temp[i] - '0'; else key = temp[i] - 'a' + 10; if (head->next[key] == NULL)//若指向空间为空 { head->next[key] = new wordtreeNode();//按照键值创建节点 } head = head->next[key];//移动指针到当前字母的节点 head->ch = lastch + temp[i];//在之前节点前缀字符串基础上加上当前字母 lastch = head->ch;//保存当前前缀 } } if (len < 4)//不是单词继续循环 continue; //是单词则加入当前节点 if (getword <= '9'&&getword >= '0')//除一次性读入前4个字母外,后续都为逐个读入 key = getword - '0'; else key = getword - 'a' + 10; if (head->next[key] == NULL) { head->next[key] = new wordtreeNode(); } head = head->next[key]; head->ch = lastch + getword; lastch = head->ch; } if (len >= 4)//循环读入后特殊处理最后一个单词后没有分隔符的情况 head->end++;
遍历多叉树对词频排序(创建优先队列)
priority_queue <wordtreeNode> minfreq;//优先队列用于计算最小词频 void wordFreqsort(struct wordtreeNode *root)//词频统计 { int i = 0; wordtreeNode *p, *tmp;//需要设置一个tmp用于往下遍历,p要用于保存当前节点 p = root; for (i = 0; i <= 35; i++) { if (p->next[i] != NULL)//下个节点非空则移动指针 { tmp = p->next[i]; if (tmp->end != 0)//如果是单词尾结点压入队列 minfreq.push(*tmp); wordFreqsort(tmp);//递归遍历 } } }
输出词频前十的单词及频数
int j = 0;//用于控制输出单词数 wordtreeNode *t = new wordtreeNode; while (!minfreq.empty())//pop十次,若不到十次则有几次pop几次 { if (j == 10) break; *t = minfreq.top(); //输出到文件 outfile << "<" << t->ch << ">: " << t->end << endl; minfreq.pop();//出队列一个单词 j++; }
五.性能分析
参考张扬同学的意见在主函数开了个比较大的循环以保证足够的数据量,使用vs2017自带的性能探查器得到结果如下。
六.功能测试
对于带空格、换行符、制表符等其他符号的常规样本
输入
MostfreqWORD666 MostfreqWORD666 MostfreqWORD666 not4aword MostfreqWORD666)_(((*265couldbeWord%$#444couldbewORD sixgodflowerLUwater 6845LUCKydoG55?? qqnumberww 8*889qq8971wwwwww a letITbe SEcfreqwoRd2 SEcfreqwoRd2 SEcfreqwoRd2 SEc8freqwoRd2 BADBOY BEDBOY BBDBOY QQQ AWsome42 saFE&sound nic458 nie89jqwej itSaw0rd
输出
characters: 1866350 words: 137400 lines: 57250 <mostfreqword666>: 4 <secfreqword2>: 3 <couldbeword>: 2 <aword>: 1 <awsome42>: 1 <badboy>: 1 <bbdboy>: 1 <bedboy>: 1 <freqword2>: 1 <itsaw0rd>: 1
对于空的样本
输出
characters: 0 words: 0 lines: 0
对于只有3行换行符的样本
输入
输出
characters: 3 words: 0 lines: 0
对于只有5个制表符的样本
输入
输出
characters: 5 words: 0 lines: 0
对于只有8个空格的样本
输入
输出
characters: 8 words: 0 lines: 0
对于只有一行非分隔符的普通字符样本
输入
this is a123 653simple lin0e
输出
characters: 28 words: 2 lines: 1 <simple>: 1 <this>: 1
对于单词数不到十个的样本
输入
oneword 2word2two threeword3
输出
characters: 28 words: 3 lines: 2 <oneword>: 1 <threeword3>: 1 <word2two>: 1
对于有部分相同前缀字符串的样本
输入
wwwwwwwwhat wwwhat what wwwhat?456 wwwwwwwhatthe whatsgood
输出
characters: 59 words: 6 lines: 3 <wwwhat>: 2 <what>: 1 <whatsgood>: 1 <wwwwwwwhatthe>: 1 <wwwwwwwwhat>: 1
对于不存在的文件
输出//报错部分还没设计所以控制台没输出提示
characters: 0 words: 0 lines: 0
对于字符都不构成单词的样本
输入
I'ma f@ke W0rd Im S0rry man^_^
输出
characters: 30 words: 0 lines: 2
七.实践心得
自己的代码能力基础相比很大一部分同学来说较差,应多加练习。在修改代码debug的过程中应当尝试在关键处设置测试量输出,会提高不少效率。初拿到题目的时候自己最直接的想法是全部存储到数组中,但是和舍友交流之后感觉这个做法太浪费空间,从舍友那边得到启发要用多叉树的结构来存储单词,于是上网查询和学习了相关知识。可见在完成项目的过程中,参考他人意见和积极学习也是很有意义的。