結對編程—WordsCount

Part1 Github項目地址

Fork倉庫的Github項目地址 git@github.com:JusticeXu/WordCount.git
結對夥伴GIthub地址 npc1158947015

Part2 PSP表格

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

Part3 需求分析

文件統計軟件wordCount功能以下:c++

Core計算模塊

​ 1.統計文件的字符數函數git

​ 2.統計文件的單詞總數函數github

​ 3.統計文件的有效行數函數算法

​ 4.統計文件中單詞出現的次數,按字典序輸出頻率最高的10個單詞或者詞組函數編程

個性輸出模塊

​ 1.自定義輸出:能輸入用戶指定的前n多的單詞與其數量數組

最終實際的軟件,除了核心的計算模塊之外,還要讓這個Core模塊和使用它的其餘模塊之間要有必定的API交流安全

經過設計GUI界面進行實現數據結構

思惟導圖:框架

Part 4 代碼框架與接口

代碼規範

參考資料: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秒而已)。

Part 5 結對過程

哈哈哈這張照片我被拍成閉着眼睛的很不爽哦,可是在結對的過程當中,我根據本身的實際體會總結了有如下幾個優勢和缺點:

優勢:

1.結對編程當中,遇到困難了能夠相互鼓勵加油,若是其中一我的十分有趣的話,整個編程過程仍是十分活躍的

2.當有人觀看你寫代碼的時候,你會比一我的獨處寫代碼的時候更加謹慎仔細

3.遇到問題了兩我的能夠一同解決,甚至整個團隊能夠一塊兒來探討問題,效率仍是大大提升了的

缺點: 1.有時候一個的人注意力不夠集中(大部分是我……),反而會拖慢總體工做的效率

相關文章
相關標籤/搜索