2018福大軟工實踐第二次做業

寫在前面


PSP表格

PSP2.1 Personal Software Process Stages 預估耗時(分鐘) 實際耗時(分鐘)
Planning 計劃 30 40
· Estimate · 估計這個任務須要多少時間 30 40
Development 開發 1750 2365
· Analysis · 需求分析 (包括學習新技術) 240 300
· Design Spec · 生成設計文檔 60 150
· Design Review · 設計複審 10 10
· Coding Standard · 代碼規範 (爲目前的開發制定合適的規範) 20 15
· Design · 具體設計 180 90
· Coding · 具體編碼 840 1320
· Code Review · 代碼複審 280 120
· Test · 測試(自我測試,修改代碼,提交修改) 120 360
Reporting 報告 135 140
· Test Repor · 測試報告 60 30
· Size Measurement · 計算工做量 15 20
· Postmortem & Process Improvement Plan · 過後總結, 並提出過程改進計劃 60 90
合計 1915 2545

解題思路

  • 本題考察的指標主要分爲4項——統計字符統計有效行數統計單詞數統計詞頻
  • 解題思路大體將這4項小問題歸爲3類來解決。html

    【1】

統計字符數:只須要統計Ascii碼,漢字不需考慮,
空格,水平製表符,換行符,均算字符。node

  • 字符方面
    • 由題意只需考慮可視字符 (Ascii碼:32~126) 以及水平製表符 (Ascii碼:9) 和換行符 (Ascii碼:10)
    • 讀取字符能夠經過文件流 (get) 來逐個字符讀取。
    • 須要注意的是換行符回車符兩者的區別——換行符爲 \n ,回車符爲 \r 。具體區別能夠參照這篇文章

統計文件的有效行數:任何包含非空白字符的行,都須要統計。python

  • 有效行數方面
    • 網上關於非空白字符的定義也有不少,向助教請教後,助教給出定義——非空白字符即Ascii碼中能夠顯示的字符
    • 因此例以下圖所示,第二行不算做有效行,總有效行數爲2

    • 綜上所述,檢測有效行只須要在每一行檢測字符時判斷是否先檢測到可視字符,再檢測到換行符便可。
    • 最後一行的判斷——僅需判斷最後一行是否讀到可視字符便可。

【2】

統計文件的單詞總數,單詞:至少以4個英文字母開頭,跟上字母數字符號,單詞以分隔符分割,不區分大小寫。ios

  • 單詞總數方面
    • 最開始有嘗試過逐字符判斷——不少邊界狀況的考慮讓代碼複雜化且效率低下。
    • 因爲在以前寫 「爬蟲」 時常常應用到正則表達式來匹配信息,在python中應用時十分方便,因此也突發奇想C++ 裏是否也存在正則表達式這樣的應用來處理字符串。
    • 也經過查找資料瞭解了一下C++下正則表達式的應用,具體應用參見這篇博客
    • 綜上所述,只需經過匹配單詞的正則表達式,而且以此來計數便可,另外推薦一個自我感受比較方便的正則表達式檢測器

【3】

統計文件中各單詞的出現次數,最終只輸出頻率最高的10個。頻率相同的單詞,優先輸出字典序靠前的單詞。git

  • 詞頻統計方面
    • 最開始是想直接採用STL的map來進行操做的,比較方便操做並且STL中的map也方便字典序排序,可是考慮到了STL中map的底層是依靠紅黑樹實現的,時間複雜度爲O(logn)。而hashmap的時間複雜度爲O(1)
    • 不過仍需考慮處理衝突的狀況,可是相對於大數據量的狀況,hashmap的效率會更高。
    • 因此詞頻統計能夠經過創建hashmap來實現,具體模型以下圖所示。
    • 每次檢測到單詞即進行hash操做一次,待文件徹底讀入後,最後再進行排序。
    • 原先有考慮過最後的詞頻的排序方法,最開始的版本是AVL樹進行排序,再輸出結果。(代碼敲到一半感受想複雜了)。其實排序的時間是O(nlog(n)),而單獨遍歷10次的時間也是 O(n) 複雜度量級的。因此就當前需求的分析考慮,詞頻統計上,我採用了遍歷的形式,消耗資源和時間在AVL樹的旋轉上 反而效率不會很高。(輸出所有單詞並排序的話就要採用排序的方法,真香)

設計實現過程

代碼文件組織

爲了獨立需求中的三項功能,因此我在代碼文件上的組織也將這三項功能封裝到不一樣的cpp文件中,而且在頭文件中聲明各自的函數。 github

  • work_2.h——包含頭文件、數據結構以及用到函數的聲明。
  • Count_chrs.cpp——統計字符數模塊(也包含行數的統計)
  • Count_words.cpp——統計單詞數模塊(結果計入hashmap)
  • Rank_words.cpp——詞頻字典序導出模塊
    各個模塊能夠分開進行單元測試,也能夠合併一塊兒做爲最終的輸出結果。正則表達式

  • Rank_words.cpp包含3個函數用於實現hashmap
    • hash_index用於實現hash值的計算,讓哈希節點儘可能分散,在理想狀況達到O(1) 時間複雜度。
    • hash_insert用於實現hash節點的插入(頭插法)
    • rank_word 用於排名而且導出前10詞頻
031602509
|- src
   |- WordCount.sln
   |- UnitTest1
   |- WordCount
       |- Count_chrs.cpp
       |- Count_words.cpp
       |- Rank_words.cpp
       |- WordCount.cpp
       |- WordCount.vcxproj
       |- pch.cpp
       |- pch.h
       |- work_2.h

具體文件組織以下所示編程


實現流程圖

整個需求完成的流程圖以下所示。
ubuntu

  • 我的認爲比較關鍵的部分在單詞判斷與統計
  • 具體一點說就是用正則表達式的模板匹配符合要求的單詞,而且記錄而且統計次數。
  • 詳細正則表達式判斷過程在下文中以流程圖形式展現windows

    關鍵函數

    正則表達式的判斷過程以下所示:
  • 敲重點!!!查閱了一些文檔,發現VS2017不支持零寬斷言判斷,因此使用正則表達式須要額外增長分隔符的判斷

具體代碼以下所示:(性能改進後)

int C_words(istream &fl, Words &wn, Wordnode **l)
{
    int count = 0;
    int flag = 0;
    regex pattern(".[a-zA-Z]{3}[a-zA-Z0-9]*");      //設定正則表達式模板
    smatch result;                                  //smatch類存放string結果
    //cout << regex_search(wn.all_string, result, pattern)<<endl;
    string::const_iterator start = wn.all_string.begin();   //字符串起始迭代器
    string::const_iterator end = wn.all_string.end();       //字符串末尾迭代器
    string temp_str;
    while (regex_search(start, end, result, pattern))       //循環搜索匹配模板的單詞
    {
        flag = 0;
        //cout<<"successfully match";
        temp_str = result[0]; 

        if (!((temp_str[0] <= 90 && temp_str[0] >= 65) || (temp_str[0] <= 122 && temp_str[0] >= 97)))//首字符判斷
        {
            if (temp_str[0] >= 48 && temp_str[0] <= 57)                                             //數字首字符判斷
                flag = 1;
            temp_str.erase(0, 1);
            if (!(temp_str.size()>=4&&((temp_str[3] <= 90 && temp_str[3] >= 65) || (temp_str[3] <= 122 && temp_str[3] >= 97))))
            {
                flag = 1;
            }
        }
        if (flag == 0)
        {
            transform(temp_str.begin(), temp_str.end(), temp_str.begin(), ::tolower);//轉換爲小寫單詞
            hash_insert(l, temp_str);                           //哈希節點插入
            count++;
        }
        start = result[0].second;                           //檢測下一單詞
    }
    //cout << endl;
    return count;
}
}

分析與改進

性能分析

  • 性能分析上選擇一份文本循環進行10000次測試。主要的時間損耗在正則表達式的匹配上以及文件輸出上。
  • 具體以下圖所示
  • 總時間消耗了23.071

  • 能夠看到主要性能消耗在單詞檢測上,個人改進思路是修改正則表達式,同時減小可避免的判斷。爲此個人改進以下:
    • 修改正則表達式——減小額外沒必要要判斷的性能損耗
    • 修改hashmap——取餘運算與散列長度均選取指數來減小查找時間

修改後結果以下圖所示

  • 時間降到了19.1
  • 性能上獲得了些許提高,提高了21%

單元測試

設定了12個單元測試用於測試代碼,具體以下所示。

單元測試內容 測試模塊 輸出結果 測試效果
給定一個字符串 字符統計 字符數 經過
給定論文部份內容A 單詞統計 單詞數 經過
給定論文部份內容B 詞頻統計 詞頻前10排名 經過
非法參數 容錯檢測 錯誤提示 經過
輸入文件異常 容錯檢測 錯誤提示 經過
輸出文件異常 容錯檢測 錯誤提示 經過
給定部分文本內容與部分無效行 有效行判斷 有效行數 經過
給定相近字符串 單詞統計、詞頻統計 詞頻前10排名、單詞數 修改代碼後經過
給定存在大小寫區別的字符串 單詞統計、詞頻統計 詞頻前10排名、單詞數 經過
給出「File123」與「123File」 單詞統計、詞頻統計 詞頻前10排名、單詞數 修改代碼後經過
給出多個不合規範字符串 單詞統計 單詞數 經過
給出相似亂碼文檔 單詞統計、詞頻統計、有效行統計、字符統計 所有需求 經過

  • 這裏提供6和7兩個測試單元的代碼
  • 分別是測試相近字符串和大小寫區分字符串。
namespace WordCount_Test
{       
    TEST_CLASS(UnitTest1)
    {
    public:
                TEST_METHOD(TestMethod6)
        {
            // TODO: 在此輸入測試代碼
            File fnew;                 //控制文件模塊
            Words wnew;                //控制單詞模塊
            Wordnode *log[HASH_LENGTH] = { NULL };  //哈希散列指針數組
            strcpy_s(fnew.file_name, "F:/VS_project/WordCount/WordCount_Test/test/test6.txt");      //獲取文件名
            //cout << fnew.file_name << endl;
            ifstream f;
            f.open(fnew.file_name, ios::in);        //打開文件
            if (!f.is_open())                       //檢測文件是否存在
            {
                cout << "can't open this file!" << endl;
            }

            fnew.count_chars = C_chars(f, fnew, wnew);
            fnew.count_words = C_words(f, wnew, log);   //計算單詞數(插入哈希節點)
            rank_word(log, wnew);                       //詞頻排名
        //單詞需按字典序排列纔可,依次檢測排序。
            Assert::AreEqual(wnew.word_rank[1], string("ubuntu14"));
            Assert::AreEqual(wnew.count_rank[1], 1);
            Assert::AreEqual(wnew.word_rank[2], string("ubuntu16"));
            Assert::AreEqual(wnew.count_rank[2], 1);
            Assert::AreEqual(wnew.word_rank[3], string("windows"));
            Assert::AreEqual(wnew.count_rank[3], 1);
            Assert::AreEqual(wnew.word_rank[4], string("windows2000"));
            Assert::AreEqual(wnew.count_rank[4], 1);
            Assert::AreEqual(wnew.word_rank[5], string("windows97"));
            Assert::AreEqual(wnew.count_rank[5], 1);
            Assert::AreEqual(wnew.word_rank[6], string("windows98"));
            Assert::AreEqual(wnew.count_rank[6], 1);
        }
        TEST_METHOD(TestMethod7)
        {
            // TODO: 在此輸入測試代碼
            File fnew;                 //控制文件模塊
            Words wnew;                //控制單詞模塊
            Wordnode *log[HASH_LENGTH] = { NULL };  //哈希散列指針數組
            strcpy_s(fnew.file_name, "F:/VS_project/WordCount/WordCount_Test/test/test7.txt");      //獲取文件名
            //cout << fnew.file_name << endl;
            ifstream f;
            f.open(fnew.file_name, ios::in);        //打開文件
            if (!f.is_open())                       //檢測文件是否存在
            {
                cout << "can't open this file!" << endl;
            }

            fnew.count_chars = C_chars(f, fnew, wnew);
            fnew.count_words = C_words(f, wnew, log);   //計算單詞數(插入哈希節點)
            rank_word(log, wnew);                       //詞頻排名
        //大寫「ABCD」和小寫「abcd」應被當作同一詞彙統計
            Assert::AreEqual(wnew.word_rank[1], string("abcd"));
            Assert::AreEqual(wnew.count_rank[1], 2);
        }
    };

}

代碼覆蓋率

之前就有了解VS有自帶的代碼覆蓋率檢測,此次做業實現時發現代碼覆蓋率結果須要VS企業版 纔有提供,最後查閱了這篇博客。給VS2017裝了一個小插件OpenCppCoverage才能夠運行。
這裏簡單給出一個小教程 (查閱不少資料都沒有很好的使用方法)

代碼覆蓋率結果以下圖所示

除了圖示的WordCount.cpp覆蓋率不高之外,其他的代碼覆蓋率都十分高,總覆蓋率爲 91% ,仔細看函數內部結構發現,該cpp中存在多處異常檢測與提示,再加上本次測試給定的參數正確,因此這也是代碼覆蓋率不高的緣由。(測試正常輸出)


計算模塊部分異常處理說明

  • 輸入參數異常處理
    • 用於處理無參數或者2個以上參數的狀況。

  • 文件名異常處理
    • 用於處理不合規範或者不存在的輸入文件名的狀況

  • 輸出文件異常處理
    • 用於處理輸出文件沒法建立的狀況。

對應單元測試以下

TEST_METHOD(Exception_input)
        {
            File fnew;
            int flag_input_exception = 0;
            strcpy_s(fnew.file_name, "../UnitTest1/test/test11.txt");//輸入文件名異常
            ifstream f;
            if (!f.is_open())
            {
                flag_input_exception = 1;                           //輸入異常標誌
            }
            Assert::AreEqual(flag_input_exception, 1);
        }
        TEST_METHOD(Exception_output)
        {
            File fnew;
            int flag_output_exception = 0;
            strcpy_s(fnew.file_name, "../UnitTest1/test/   ");//輸出文件異常
            ofstream fo;
            fo.open(fnew.file_name  , ios::out);            //輸出文件
            if (!fo.is_open())                          //輸出文件合法性檢查
            {
                flag_output_exception = 1;              //輸出異常標誌
            }
            Assert::AreEqual(flag_output_exception, 1);
        }


心得與收穫

  • 我的感受本次做業難點並非在編程自己,如構建之法中提到的——「軟件=程序+軟件工程」,本次做業也並非單純地跑通一個程序,更核心的思想我以爲是掌握軟件工程編程的要素——比方說此次做業中的接口設計也是爲了應變實踐中、現實生活中多變的需求,所以也須要軟件設計更具備可維護性以便後續修改。
  • 關於查閱資料方面,在實現正則表達式匹配方式時,我本身也查閱了一些資料——原先在ubuntu上有試着實現正則表達式匹配,每一個字符串匹配的速度都很慢,基本上都是換用boost庫中正則表達式匹配來進行的,我當時總以爲標準庫中的regex_search的底層實現不行,本次做業的時候由於VS2017的編譯boost庫的問題,只能採用標準庫。
  • 在查閱了一些資料和實踐之後,我發現之前效率低是g++4.7版本問題, C++11相對於g++4.7仍是太新了一些,不過本次在VS2017平臺上測試的效果很好,換用boost庫也沒有必要,再加上VS2017也支持C++17,我認爲在實現regex_search上效率也會更高。
  • 之前太懶不肯意去查資料.具體信息能夠看下這裏
  • 我花了不少的時間理解構建之法第三章第二節內容,我也仔細去思考了其中提到的關於軟件工程師的幾個誤區。我在分析當前這個問題的時候,是否也存在着這些問題。我本身自己也存在一個習慣——思考完整個實現過程包括內部細節再開始敲代碼。有時候這個習慣能讓個人代碼會更加具備條理,可是更多的時候卻由於這個習慣浪費了大量時間。書中提到分清主次依賴的方法我的感受十分有效,相對於這份做業來講主要的依賴問題是需求的三個功能,次要依賴問題纔是文件讀寫、優化效率。固然,這裏的次要並非表明不重要,咱們仍需多對次要問題多上一些心。
  • 這裏還想簡單闡述一下我的的想法,更泛化來講,我所認爲的主要依賴問題強調的是軟件的需求,這也是最重要的,影響着一個軟件的核心——功能性;次要依賴問題則更多的是注重軟件的可維護性和效率。如今從新再認真翻讀一遍 《構建之法》 ,也比暑假看的時候有了更多的體會,這大概就是本次做業最大的收穫吧。
  • 以前寫第一次做業的時候有想過天天晚上就花2個小時來完成,可是作這份做業的時候,幾乎整個晚上都在敲代碼和調試bug當中,也多是過久沒有敲代碼有些生疏吧。如今看來,天天晚上2個小時徹底不夠,如今決定調整到天天3小時給本身鼓掌下!


9.17更新

思考錯因

  • 在助教學姐公佈的測試結果中有報錯TLE,仔細思考了一下緣由,由於是每一個點都是TLE,因此我主要的考慮方向仍是死循環這一方面是否有問題。
  • 最後在跑了幾回由cbattle同窗提供的測試數據,發現運行時間相差不大,這裏還要感謝個人一個沒有參加軟工實踐課程的舍友,發現了輸出界面的差別—— system("pause") 致使了命令行窗口沒有正確中止。我認爲這也就是本次TLE的緣由。

    改進

  • 若是是要讓結果正確的話,其實修改掉system("pause") 徹底就足夠了,可是本次仍是想在原來的基礎上改進。
  • 由於考量到測試用例都是數據量大的文件,而原先應用的正則表達式匹配單詞在處理大文件上速度明顯就慢下來了,原原不如用底層直接字符判斷速度快。
  • 因爲原先的實現方式是正則表達式匹配,因此也把整個文本讀入來進行全文匹配單詞,可是發現這樣的方式在實現底層匹配時候不是特別方便。因而改進成vector_string 的形式來解決,具體實現方式以下流程圖所示: .

  • 單詞匹配幾個要注意到的坑點cbattle同窗也和耐心地告訴我了,感受有點像在武林高手那邊求得一本祕籍。單詞匹配的流程圖以下所示:

改進效果

  • 以完成一次長文本的匹配爲例子,下發給出時間與結果。

  • 成功減小了在單詞匹配上消耗的時間,總時間爲3.303s

小貼士

  • 檢測TLE的問題除了在時間限制上面,我發現還有多是一部分同窗在結尾用了相似於system("pause")之類的致使等待時間過長。
  • 我周邊有部分同窗有刪除.vs文件後沒法編譯,查閱一部分資料發現具體與x64的兼容性有關,這種狀況能夠手動勾選x86進行編譯,具體以下圖所示。

參考博客

[1] https://www.cnblogs.com/SivilTaram/p/software_pretraining_cpp.html#part6.%E6%95%88%E8%83%BD%E5%B7%A5%E5%85%B7%E4%BB%8B%E7%BB%8D

[2] http://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html

相關文章
相關標籤/搜索