目录ios
Fork仓库的Github项目地址 | git@github.com:JusticeXu/WordCount.git |
---|---|
结对伙伴GIthub地址 | npc1158947015 |
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 18 |
· Estimate | · 估计这个任务须要多少时间 | 900 | 903 |
Development | 开发 | 600 | 633 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 53 |
· Design Spec | · 生成设计文档 | 20 | 17 |
· Design Review | · 设计复审 (和同事审核设计文档) | 15 | 13 |
· Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 30 | 30 |
· Coding | · 具体编码 | 360 | 400 |
· Code Review | · 代码复审 | 40 | 40 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 60 |
Reporting | 报告 | 60 | 60 |
· Test Report | · 测试报告 | 20 | 20 |
· Size Measurement | · 计算工做量 | 20 | 20 |
· Postmortem & Process Improvement Plan | · 过后总结, 并提出过程改进计划 | 20 | 20 |
合计 | 870 | 903 |
文件统计软件wordCount功能以下:c++
1.统计文件的字符数函数git
2.统计文件的单词总数函数github
3.统计文件的有效行数函数算法
4.统计文件中单词出现的次数,按字典序输出频率最高的10个单词或者词组函数编程
1.自定义输出:能输入用户指定的前n多的单词与其数量数组
最终实际的软件,除了核心的计算模块之外,还要让这个Core模块和使用它的其余模块之间要有必定的API交流安全
经过设计GUI界面进行实现数据结构
思惟导图:框架
参考资料:Google C++代码规范
原本以为本身写的代码其实算是足够规范的,可是在比较分析告终对伙伴的代码后,以为人和人的代码风格仍是存在让人诧异的差别的,所以找到了目前最为规范的Google C++代码规范(李开复鼎力推荐)拿来阅读。总体阅读下来大概花了一个小时左右。给我最大的感受是,像Google,Tencent这样的技术公司,对于总体框架安全性的追求远远超过对技术中淫技巧术的追求,还有就是对可读性的要求也很是高,下面是一些例子:
1.在任何可以使用const的状况下,都用使用const。
2. 为避免拷贝构造函数、赋值操做的滥用和编译器自动生成,可声明其为private且无需实现。
3.要尽量得对本身的代码编写注释。
4.代码总体风格要尽可能统一:每一行代码字符数不超过80,不使用非ACSII字符,使用时必须使用UTF-8格式等等
可是也有一些东西讲得和老师传授的东西不同,因此也不能彻底地照搬规范,让本身的代码美观可读,总体风格一致,那就是最好看的代码。
咱们一共实现了6个类,其中包括核心计算类core()和两个异常处理接口。
1.void generate(),用来实现总体功能的耦合;
2.Core();构造函数;
3.bool IsValid();检查每次录入的文本是否符合要求,若知足则返回true,不然返回false;
4.void Copyresult(int result[CELL], int temp[number]);将结果复制到result中;
5.bool TraceBackSolve(int pos);回溯过程是否有正确输出,如有则输出并返回true,不然返回false;
6.void show(int result[CELL]) 自定义输出用户指定的内容;
#include <iostream> #include <string> using namespace std; int CharacterCount(string sentence); int main(){ string Mystring; cout << " Type in the string you would like to use: " << endl; getline(cin, Mystring); // inputs sentence cout << " Your string is: " << Mystring << endl; cout << " The string has " << CharacterCount(Mystring) << " characters " << endl; system("pause"); return 0; } int CharacterCount(string sentence){ int length = sentence.length(); // gets lenght of sentence and assines it to int length int character = 1; for (int size = 0; length > size; size++) { if (sentence[size]>='A'&&sentence[size]<='Z') character++; else if (sentence[size]>='a'&&sentence[size]<='a') character++; else if (sentence[size]>='0'&&sentence[size]<='9') character++; else character++; } if ( sentence[0] == ' '/*space*/ ) character--; return character; }
代码自审:这一部分的代码比较简单,一开始就想到了用for循环很快就能解决问题。而后要注意当遇到空文档时,怎么保证输出结果正确,是这个功能中最难想到的一部分。
#include <iostream> #include <string> using namespace std; int WordCount(string sentence); int main() { string Mystring; cout << " Type in the string you would like to use: " << endl; getline(cin, Mystring); // inputs sentence cout << " Your string is: " << Mystring << endl; cout << " The string has " << WordCount(Mystring) << " words " << endl; system("pause"); return 0; } // counts the words int WordCount(string Sentence) { int length = Sentence.length(); // gets lenght of sentence and assines it to int length int words = 1; for (int size = 0; length > size; size++) { if (Sentence[size] == ' '/*space*/) words++; // ADDS TO words if there is a space } if ( Sentence[0] == ' '/*space*/ ) words--; return words; }
代码自审:这一部分的功能实现和字符统计相似,无非是当读到空格时,words加1。也算是很简单了。下面的功能也是,当读到\n时,行数加1。代码以下。
#include <iostream> #include <fstream> #include <string> using namespace std; int CountLines(char *filename) { ifstream ReadFile; int n=0; string tmp; ReadFile.open(filename,ios::in);//ios::in 表示以只读的方式读取文件 if(ReadFile.fail())//文件打开失败:返回0 { return 0; } else//文件存在 { while(getline(ReadFile,tmp,'\n')) { n++; } ReadFile.close(); return n; } } int main() { char filename[]="inFile.txt"; cout<<"该文件行数为:"<<CountLines(filename)<<endl; return 0; }
该项目的核心任务分为两部分,一是在文本中识别出不一样的单词,二是统计全部单词并将这些单词按照词频进行排序。能够将这个部分分为四个模块:输入模块,状态模块,统计模块,输出模块。
识别文本中的单词,我使用的是一个有限状态机模型。
假如文本中没有连词符 '-' ,那么问题十分简单,只有两个状态,一个是单词内部,一个是单词外部,相互转换的条件则是在单词内部的状态下检测到一个非字母字符,或者在单词外部的状态下检测到一个字母字符。
当文本中出现了连词符,那么状况会复杂一些,不过仍然不会太复杂。咱们增长了一个临界状态,当读入一串字母以后忽然检测到了一个连词符,则会进入到这个状态。这个状态不会持续,一旦读入下一个字符,就会根据它是字母或者非字母字符,进入到单词内部或单词外部的状态。
使用这一个3状态7过程的状态机模型,能够完美地知足需求。
添加单词:
int p_index = Hash(word); //利用单词计算哈希索引 WordIndex* pIndex = index[p_index]; while (pIndex != nullptr) { Word *pWord = pIndex->pWord; if (!strcmp(word, pWord->word)) { //在哈希索引对应的几个单词中,找到咱们须要找到的单词 pWord->num++; //若是找到了,首先把这个单词的词频+1 //接着根据词频调整单词的位置 Word *qWord = pWord->previous; while (qWord->num < pWord->num) { if (qWord == pWordHead) return; shiftWord(pWord); qWord = pWord->previous; } //而后再在同一词频下根据单词在字典中的顺序调整位置 while (strcmp(qWord->word, pWord->word) > 0) { if (qWord->num > pWord->num) return; shiftWord(pWord); qWord = pWord->previous; } return; } pIndex = pIndex->next; }
遇到首次遇到的单词,添加索引:
// Copyright[2018]<Star> #include "WordList.h" WordList::WordList() { for (int i = 0; i < MAX_INDEX_NUM; i++) index[i] = nullptr; wordNum = 0; } WordList::~WordList() { Word *temp; for (int i = 0; i < MAX_INDEX_NUM; i++) { for (Word *p = index[i]; p != nullptr;) { temp = p; p = p->next; delete temp; } } } void WordList::addWord(char word[]) { // Add the word to the WordList (or word frequency +1) int p_index = Hash(word); if (index[p_index] == nullptr) { index[p_index] = new Word(word, 1, index[p_index]); return; } Word* pWord = index[p_index]; while (pWord->next!= nullptr) { if (!strcmp(word, pWord->word)) { pWord->num++; return; } pWord = pWord->next; } if (!strcmp(word, pWord->word)) { pWord->num++; return; } pWord->next = new Word(word, 1, nullptr); //index[p_index] = pWord; wordNum++; } void WordList::outPut() { // 100 words are all output via cout if (wordNum <= 0) return; if (wordNum > 100) wordNum = 100; //开辟一片连续的空间以排序(使用最小堆) Word **word = new Word*[wordNum + 2]; word[0] = word[wordNum + 1] = nullptr; int iIndex = 0; Word *pWord; for (int iWord=1; iIndex < MAX_INDEX_NUM; iIndex++) { //将索引中的全部结点放入开辟的空间中准备排序 pWord = index[iIndex]; while (pWord != nullptr) { word[iWord] = pWord; //上滤 upFilter(word, iWord); pWord = pWord->next; //若是有一百个以上结点,能够进行取舍,由于总共只须要输出100个结点 if (++iWord > 100) break; } if (iWord > 100) break; } for (; iIndex < MAX_INDEX_NUM; iIndex++) { if(pWord==nullptr) pWord = index[iIndex]; while (pWord != nullptr) { if (word[1]->equal(pWord) == -1) { word[1] = pWord; downFilter(word, 50); //下滤 } pWord = pWord->next; } } heapSort(word, wordNum); //堆排序 //输出100个单词及词频 cout << word[2]->word << ' ' << word[2]->num; for (int i = 3; i < wordNum + 2; i++) cout << endl << word[i]->word << ' ' << word[i]->num; delete[]word; } int WordList::Hash(char* word) { int HashVal = 0; while (*word != '\0') HashVal += *word++; return HashVal & 511; } void WordList::heapSort(Word * word[], int wordNum) { //排序 int iWord; for (iWord = wordNum; iWord >= 1;) { word[iWord + 1] = word[1]; word[1] = word[iWord]; word[iWord] = nullptr; iWord--; downFilter(word, iWord >> 1); } } void WordList::downFilter(Word * word[], int middleNode) { //下滤 int iHeap = 1; Word *left, *right, *pWord = word[1]; while (iHeap <= middleNode) { left = word[iHeap << 1]; right = word[(iHeap << 1) + 1]; if (word[iHeap]->equal(left) == 1) { if (word[iHeap]->equal(right) == 1) { if (left->equal(right) == 1) { word[(iHeap << 1) + 1] = pWord; word[iHeap] = right; iHeap = (iHeap << 1) + 1; continue; } } word[iHeap << 1] = pWord; word[iHeap] = left; iHeap = iHeap << 1; continue; } if (word[iHeap]->equal(right) == 1) { word[(iHeap << 1) + 1] = pWord; word[iHeap] = right; iHeap = (iHeap << 1) + 1; continue; } break; } } void WordList::upFilter(Word * word[], int downNode) { //上滤 int iHeap = downNode; while (iHeap > 1) { if (word[iHeap]->equal(word[iHeap >> 1]) == -1) { Word *temp = word[iHeap]; word[iHeap] = word[iHeap >> 1]; word[iHeap >> 1] = temp; } else return; iHeap = iHeap >> 1; } }
输入模块&&程序大致结构:
#include<iostream> #include<fstream> #include"WordState.h" #include"WordList.h" using namespace std; void wordCount(char *fileName, WordList &wordList); //用于词频统计 void outPut(char outFile[], WordList &wordList); //用于输出结果 int main(int argc, char **argv) { WordList wordList; wordCount("wcPro.cpp", wordList); outPut("result.txt", wordList); return 0; } void wordCount(char *fileName, WordList &wordList) { char word[MAX_WORD_LEN]; WordState wordState; processType process; ifstream in; in.open(fileName); int wordPosition = 0; bool flag = true; do { flag = (word[wordPosition] = in.get()) != EOF; process = wordState.stateTransfer(word[wordPosition]); switch (process) { //根据不一样状态作出不一样的响应 case PROCESS_23: //删去最后一个字符以及前一个连词符,单词数++ word[wordPosition - 2] = 0; wordList.addWord(word); wordPosition = 0; break; case PROCESS_13: //删去最后一个字符,单词数++ word[wordPosition - 1] = 0; wordList.addWord(word); wordPosition = 0; break; case PROCESS_33: //单词外部-单词外部,wordPosition不变 break; default: wordPosition++; } } while (flag); in.close(); } void outPut(char outFile[], WordList &wordList) { ofstream outf(outFile); streambuf *default_buf = cout.rdbuf(); cout.rdbuf(outf.rdbuf()); wordList.outPut(); cout.rdbuf(default_buf); }
状态模块:
WordState wordState; //用于记录当前的状态 processType process; //这个变量用于记录状态迁移的路径 char c = 0; do { c = in.get(); //这个函数能够根据读入字符进行状态转移,并返回转移过程 process = wordState.stateTransfer(c); switch (process) { //根据不一样状态作出不一样的响应 case PROCESS_23: //临界状态->单词外部,删去最后一个连词符,单词数++ break; case PROCESS_13: //单词内部->单词外部,单词数++ break; case PROCESS_33: //单词外部-单词外部,放弃这个非字母字符 break; default: //其余状况:储存这个字母字符 } } while (c != EOF);
状态迁移:
// Copyright[2018]<Star> #include "WordState.h" WordState::WordState() { state = OUTERWORD; } processType WordState::stateTransfer(char c) { // Pass in a character, calculate the next state based on // this character and the current state // and return a process indicating how the state was migrated processType process = state << 4; // state transition code if ((c >= 'a') && (c <= 'z')) { state = INNERWORD; } else if (c == '-') { if (state == INNERWORD) state = CRITICAL; else state = OUTERWORD; } else { state = OUTERWORD; } process = process | state; return process; }
在把全部的代码编写完成后,按要求咱们要进行代码互审,个人同伴是用C语言写的代码,在功能上个人代码基本造成了覆盖,并且类的实现,C语言只能用结构体粗陋代替。所以在代码合并过程当中,我这边基本以我本身的代码为主。模块的划分我俩都和题目类似,具体按照需求分析来实现。
1.Design by Contract (契约式设计)
在Core模块中,题目附加要求能统计文件夹中最常使用前十个词组的词频,在这个功能中,咱们的基本思路是使用一个双向链表来存储所须要的数据,具体来讲,每一个结点记录了一个单词和这个单词的词频,从链表到链表尾词频依次递减,相同词频字母字母靠前的靠近链表头。每次遇到一个单词,咱们先去查找有没有对应于这个单词的结点,若是有,就让这个单词的词频加一,不然在链表的尾部添加一个这个单词的结点。而后不论哪一种状况,都要对该单词对应的结点进行调整,使它在同一词频的几个其余单词中,根据首字母大小排序,处于正确的位置。最后从链表头开始,依次遍历一百个结点,就能找到词频最高的一百个单词。
这个方法简单粗暴,但效率低下。两个缘由,一个是结点交换位置时必须依次在相邻位置进行交换,这个缺点是对于链表这种数据结构来讲没法克服;另外一个缘由是查找单词时必须从链表头开始遍历。咱们考察了不少种数据结构,可是没有一种数据结构可以既快速地处理排序,又能快速地查找结点。所以决定给这个链表,根据单词内容构建一个哈希索引。对于每个单词(其实是一个字符数组),咱们能够对全部字符进行求和,而后经过模除获得哈希索引,经过哈希索引来查找结点,大大提升了查找的速度。为了提升哈希函数的性能,咱们选取128做为模,由于任何一个数模除128均可以写成它和127进行按位与(&)运算,与运算的速度远远高于除法。
对于哈希索引成功找到结点的状况很容易能够实现的,而对于首次遇到的单词,没法根据哈希索引来找到它,那就在链表的末尾增长一个新的结点以记录这个单词,而后为它创建一个索引。
2.Information Hiding
I.用GUI对Core模块的调用(题目要求),体如今讲Core模块封装好成为一个dll库,外部只能经过头文件来查看这个类的功能和成员变量,而不能查看修改源码。
Ⅱ.Core模块中对Core类中全部的成员变量都是private,这些变量对外不可直接用。
Ⅲ.Core模块中对Core类的用来完成非对外功能的函数声明在了private下。
3.Interface Design
在C++中,接口能帮助咱们实现多态
4.Loose Coupling (松耦合)
Ⅰ.要实现松耦合,其中一点就是要实现类的单一功能原则。
在咱们的工程中,处理来自对外的命令参数这是一个类,完成了对参数的基本检测以及对参数中信息的提取。
完成来自用户的请求这是另外一个类,这个类实现了generate函数和solve函数,完成了基本功能。
除此以外,对于不一样的异常,咱们定义了不一样的异常类。
Ⅱ.类之间的相互影响要下降
这一方面咱们工程已经不能再简化了,由于功能类和输入处理类的交互点在功能类经过输入处理类提供的接口来获取信息,若是这个交互不存在的话,那么功能类完成功能也就无从谈起。虽然能够将那些选项参数后面的参数定义为公共的变量,可是这些变量仍是要通过输入处理类的处理以后才能被功能类所使用。
WordList类是我认为最有风险的一个类,它用于实现这个项目最最最主要的功能:词频统计。而为了实现这个功能,它又要调用各类子函数(如哈希函数),从函数调用图来看,它的入度和出度总和是最高的。具体的测试用例设计表格在下面给出。
13个测试用例,从最简单的对一至两个单词进行的功能测试,到最后面对高频词、长单词、超多种类单词进行性能测试,基本把能想到的部分都测了。
其中,第八个超长单词测试,咱们使用了一个22个字母组成的单词进行测试,注意到咱们代码中为单词设置的最大长度是20,因此这里产生了错误。修改的方法很简单,从新设定单词最大长度便可。
第12和第13个测试用例出错是我没有想到的,这个测试用例没法正常运行,提示说这是一个非法堆指针错误。
一开始我觉得是堆空间不够了(由于从aaa-zzz总共有一万多个不一样的单词,意味着链表有一万多个结点),我就调大了堆空间,可是没有奏效。我就只好硬着头皮使用断点调试,发现其实链表创建很成功,出错的是在测试执行完毕以后,释放空间的那段代码。为了少些几行代码,在释放链表空间的时候,我采起了简单粗暴的递归方式:在释放一个结点以前,首先调用next结点的析构函数,而后再释放本身。这就是一个递归的过程。
//错误的示例 ~Word() { delete next; next = nullptr; }
可是,如今咱们总共有一万多个链表结点,也就是要递归调用10000多层!!!栈空间确定不够了。要怪只能怪我偷懒,后来我老老实实改为了用一个for循环释放结点,测试就成功经过了。
//正确的示例 Word *p = pWordHead; while (p != nullptr) { pWordHead = p->next; delete p; p = pWordHead; }
成功的单元测试脚本运行结果以下图。一个值得注意的现象是,用例12和用例13分别是顺序多单词测试和逆序多单词测试。在顺序多单词测试中,全部的单词都按照字典顺序进入链表,不须要作多余的排序,所以仅仅用了307毫秒就完成了对一万七千多个单词的词频统计。相比之下,用例13每一个单词都按照字典逆序进入链表,这意味着每一个单词都要进行一次排序(并且这个排序很慢,是一个节点一个节点地挪动,在前面也解释了,这是链表的硬伤),最后用了24秒才完成统计,是用例12的80倍!若是不使用链表,而是使用向量的话,能够考虑使用先统计后排序的策略,向量排序是很快的,它有封装好的快速排序的算法,惋惜我既然已经走上了链表的道路,再推翻重写就有点不太愿意了。
为了寻找一份一个在内容上能形成足够压力的txt文档,我在网上找了以《巴黎圣母院》为首的四部英文名著,把它们整合成一份大小超过5M的txt文档。
这个运行时间让我十分惊讶,首先我决定对I/O进行优化。咱们的程序读取文件采用的是逐字符读取的方式,边读文件边处理,那么一个文件里面字符越多,咱们的I/O次数就越多。因而,咱们对此进行了改进。先将文件的全部内容一次性读到内存中,而后就能够关闭文件,从内存中逐字符判断了。
// 改进的I/O代码 ifstream in(fileName, ios::binary); if (!in) { // Determine if the file exists cout << fileName << "file not exists" << endl; return nullptr; } filebuf *pbuf = in.rdbuf(); // 调用buffer对象方法获取文件大小 long size = pbuf->pubseekoff(0, ios::end, ios::in); if (size == 0) { cout << fileName << "file is empty" << endl; in.close(); return nullptr; } pbuf->pubseekpos(0, ios::in); // 分配内存空间 char *ch = new char[size + 1]; pbuf->sgetn(ch, size); ch[size] = '\0'; in.close();
在进行了这一优化后,运行时间减小了5秒(是的,只减小了5秒而已)。
哈哈哈这张照片我被拍成闭着眼睛的很不爽哦,可是在结对的过程当中,我根据本身的实际体会总结了有如下几个优势和缺点:
优势:
1.结对编程当中,遇到困难了能够相互鼓励加油,若是其中一我的十分有趣的话,整个编程过程仍是十分活跃的
2.当有人观看你写代码的时候,你会比一我的独处写代码的时候更加谨慎仔细
3.遇到问题了两我的能够一同解决,甚至整个团队能够一块儿来探讨问题,效率仍是大大提升了的
缺点: 1.有时候一个的人注意力不够集中(大部分是我……),反而会拖慢总体工做的效率