福大軟工1816 · 第二次作業
1. Github地址
2. PSP表格
PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
Planning | 計划 | 30 | 60 |
· Estimate | · 估計這個任務需要多少時間 | 30 | 60 |
Development | 開發 | 240 | 700 |
· Analysis | · 需求分析 (包括學習新技術) | 30 | 120 |
· Design Spec | · 生成設計文檔 | 20 | 30 |
· Design Review | · 設計復審 | 20 | 30 |
· Coding Standard | · 代碼規范 (為目前的開發制定合適的規范) | 20 | 20 |
· Design | · 具體設計 | 30 | 30 |
· Coding | · 具體編碼 | 50 | 240 |
· Code Review | · 代碼復審 | 30 | 30 |
· Test | · 測試(自我測試,修改代碼,提交修改) | 40 | 200 |
Reporting | 報告 | 100 | 300 |
· Test Repor | · 測試報告 | 10 | 70 |
· Size Measurement | · 計算工作量 | 10 | 30 |
· Postmortem & Process Improvement Plan | · 事后總結, 並提出過程改進計划 | 80 | 210 |
合計 | 370 | 1060 |
3. 解題思路描述
-
統計文件的字符數:
以二進制模式打開文件,逐個統計字符
-
統計文件的單詞總數:
單獨處理每一行,設置兩個下標標識出一個詞的界限,再判斷是否合法
-
統計文件的有效行數:
單獨處理每一行,判斷是否有可顯示的字符
-
統計文件中各單詞的出現次數,最終只輸出頻率最高的10個。頻率相同的單詞,優先輸出字典序靠前的單詞:
- 對於合法的單詞轉小寫后→維護一個
map<string,int>
- 有關排序,偶然看到了陳伯濤同學的博客的博客,值得思考!本來我的做法是重載<號直接sort,但這樣的復雜度是
O(N*Log(N))
,我們只需要提取出前十個,如果直接去找這前十個的話,復雜度是O(N*10)
。如果使用堆排序,在初始化堆的過程中,也需要O(N*Log(N))
的時間。所以最后我使用的是直接去找這前十個。另外還有陳同學沒有像我一樣蠢蠢的把map<string, int>的東西再轉到一個vector中進行排序,而是直接使用迭代器進行排序,這樣能避免拷貝的時間,后來我也照着這一點更改了我的代碼。
- 對於合法的單詞轉小寫后→維護一個
-
按照字典序輸出到文件result.txt:
C/C++的IO操作
在實際過程中發現windows下文本編輯時一次回車=兩個字符(\r + \n)
,使用文本的形式打開文件是看不到那個\r
的,如需統計字符個數,需要在二進制形式下打開
查找的資料:
- C++的argc、argv
- C++的IO流
- 回車和換行的區別
- Google C++命名規范
- 《構建之法》個人項目部分
C++: std::ifstream::open
4. 設計實現過程
應作業要求,代碼組織如下
結構說明:
- 在
*.h
中聲明相關函數,如CountChar.h
中有一個int CountChar()
的函數聲明 - 在
*.h
中聲明的函數統一定義在function.cpp
中 - 主程序WordCount.cpp引用
*.h
,直接使用其中的函數即可
測試模塊設計
測試文件以Windows下文本編輯器VS Code進行編輯,測試文件如圖
主要對CountChar()
、CountLine()
、CountWord()
三個函數進行了測試
5.性能分析以及改進
版本1的程序過程:
更改main函數,讓程序多次循環讀取文件
進行效能分析
我們可以看到,比較花費時間的是ShowResult函數、文件操作的Open函數、GetWordCountMap函數。首先我打開了兩次文件(一次是文本模式,一次是二進制模式),還有輸出結果的時候需要以寫的形式打開文件,Open操作比較多可以理解。ShowResult函數以及GetWordCountMap函數中涉及對Map的操作,需要多加一個Log復雜度進去,且GetWordCountMap會在兩個函數中被調用,這個結果也可以理解。
更詳細的性能報告
可以看到文件IO的耗時比較大,因此我考慮只在二進制模式下打開文件一次,得到所有信息的方法。
於是就有了版本2
版本2的程序過程:
版本2主要改進內容:
- 只讀取一次文件(在獲取詞頻字典的過程中順路獲得其他信息,並修改標志位)
- 取消排序算法,改用直接選取前10大
在版本2中,我自己封裝了一個GetLine
方法,既可以統計字符,也可以返回該行的其他字符(非\r \n
),同時在舍棄了排序,采取了直接選取前10大的方法,這樣在數據量較大的時候會取得不錯的性能
版本2的性能分析
可以看到相同的數據,版本2的CPU開銷從29504下降到了23413(效率提升20.6%),Open函數的開銷也從10654下降到了8501(效率提升20.2%)
6.代碼說明
- 總體思想是使用二進制模式打開文件,首先獲取詞頻統計字典,在獲取這個字典的過程中一次性得到所有信息(字符數、行數、詞頻),並修改g_has_got_map、g_has_got_lines、g_has_got_char三個標志位為True,這樣在調用CountChar()、CountLines()函數時就可以直接返回值。
- 維護字符數、有效行數的工作在自定義的
GetLine()
中實現 - 考慮到其他程序在調用過程中未必有統計詞頻的功能,我在程序中設置全局變量
g_has_got_map
、g_has_got_lines
、g_has_got_charaters
三個全局變量,如果程序在調用過程中已經求得了詞頻、有效行、字符數的時候就直接將結果返回,沒有得到的時候再重新計算。這樣可以減少計算量。
GetWordCountMap()函數的實現
GetFirstTenWords()函數的實現
CountChar()函數的實現
CountLine()函數的實現
自定義GetLine()函數的實現
main()函數的調用
7.個人收獲
- 通過
link2005
這個錯誤,才明白C++工程中不能在頭文件中定義函數,函數的定義要放在CPP文件中;還有如果要定義全局變量的話,在一個CPP中定義好全局變量,在其他需要使用全局變量的CPP使用extern進行引用 - 看了鄒欣老師的《構建之法》的前面一部分,才意識一個復雜軟件的誕生需要復雜的過程。之前沒有使用過完整的構建方法,不知道寫一個軟件還需要進行單元測試、回歸測試等步驟,也沒有使用過VS強大的性能分析工具。通過這個簡單的個人項目學到了通過VS測試軟件的基本方法。
- 初步掌握了Git,我不是實驗班的,而且之前並沒有使用Git的需要,雖然學過但由於不經常使用慢慢又忘了。
- 課上聽到柯老師的建議,要有科研精神,比如算法改進后,不要只給出絕對值,而要給出一個相對提升的百分比。
- 在我按照助教老師的要求測試GB級文件時,建立詞頻統計字典花了4分鍾,相比之下VS Code打開這個文件並做好顯示大概只需要10So(╥﹏╥)o,他們開掛了嗎?好吧,一定是我的算法可改進的空間還有很大。
- 通過這道題目,讓我意識到我的工程化思維不夠。之前參加算法競賽,因為時間有限,一般考慮的是怎么快速的A了這道題目,不會使用工程化的思想來解決這個問題,反正怎么快怎么來,還封裝成模塊?不可能有這樣的時間的好嘛。但,平心靜氣來講,我相信一個龐大的軟件,一定需要完整工程化體系的支持。反思為什么國外的企業基本沒有什么加班的現象但國內的加班現象這么嚴重?我並不覺得是國內程序員笨,而是想比國內,國外多家公司有成熟的工程化體系(Apple、Facebook、Amazon、Microsoft等)。