軟件工程個人作業 詞頻統計
---恢復內容開始---
軟件工程個人作業
——單詞頻率統計
曾子軒
School of information,USTC
一、項目介紹與分析(作業要求:http://www.cnblogs.com/denghp83/p/8627840.html)
項目介紹:
1. 統計文件的字符數
2. 統計文件的單詞總數
3. 統計文件的總行數
4. 統計文件中各單詞的出現次數,對給定文件夾及其遞歸子文件夾下的所有文件進行統計
5. 統計詞組的頻率,在Linux系統下,進行性能分析,過程寫到blog中
項目分析:
1、前三個要求限定了對單個字符的讀取和處理
2、第四個要求限定了對文件夾進行遍歷、單詞字符串的判別、處理和存儲
3、第五個要求限定了對前後字符串關系的存儲
數據結構的選定:
1、因為對單詞的判定是至少以四個字母開始,而之後的位數不定,為了體現各個不同前綴之間的區別,也是為了分散各個單詞,所以采用通過前四個字母算出地址碼(不同前綴對應不同地址)的方法將數據分散 在26^4=456976個單元中(命名為HashSet[456976])。
2、為了存儲不定數目單詞,又因為每一個單詞節點數據較多,所以采用鏈表形式,且將帶頭結點的鏈表首地址存放在數組單元中。
為了將詞組更高效的存儲,將跟在這個單詞後面的單詞的指針存儲在前面這個單詞結構體所指向列表(自行編寫的變長數組)裏。
WordNode
NextWord
3、在最後遍歷時,因為只需要頻率最大的十個單詞、詞組,所以采用數組的插入排序法,考慮到大多數單詞/詞組的出現次數只會比這個TopTen數組的最小出現次數小,所以從第十位開始比較、移動。
TopWordStru
TopPhraseStru
二、項目進展
各部分估計用時與實際用時:
實際中,為了充分地考慮到問題可能導致的各種bug,我對關鍵函數在草紙上刪刪改改寫滿了不只10頁,當然結果也是顯著的,在細致分析的函數中只出現過一個bug。
三、樣例分析
主要對一些偏難怪的單詞進行調用分析。
1、以只有3個字母開頭的(不算單詞)。
2、空文件也算一行,不能簡單地根據‘\n‘進行判斷。
3、相同詞頻的按照ASCII順序進行排列。
4、詞組打印時,打印出兩個單詞各自的最小的ASCII出現。
文件1: 文件2: 文件3: 結果:
接下來對助教提供的大小為176MB的文件夾進行讀取分析:
在windows cmd命令下,處理只花費了15s左右,這樣的速度是相當可觀的,然而遺憾是正確性:
最後的答案因為在提交前的改動而產生了錯誤,焦躁的內心讓一個本該完美的項目黯然失色,也讓一周以來的勞苦失去意義。
四、代碼優化
1、減少堆棧調用
我之前在一些調用次數極多的函數中還多次調用子函數,這樣堆棧訪問頗為頻繁。之後我直接將這個小子函數的代碼拷貝入調用它的函數,消耗時間一下子減少了七個百分點。
修改前: 修改後:
2、對可以預知次數的循環函數進行拆分
這是一種比較過分的優化,目的是消除計數器count自加、空間消耗的影響。實際上這樣的影響甚微,反倒讓代碼難讀,以致最後優化階段出現失誤,得不償失。
3、對If函數進行細分、優化
如果一個if判斷中包含多個或(||),不妨分成幾個if嵌套,這樣可以讓操作分流,減少一些數據的判斷次數,實際上這樣的操作讓我的代碼快了數秒(因為調用次數實在太多)。
4、對sizeof的優化
因為數據結構的選擇,讓運行過程中充滿了動態內存申請、分配,而每一次分配都計算一次sizeof顯然是很不值得的,當然可以先通過編譯器跑一次,然後作為宏定義值替換,但這樣移植性受到限制,更好的做法是在函數中作 為靜態變量調用。
五、項目經驗與反思
1、關於本次程序本身
1) 分配不均。根據前四個單詞算出地址碼是最簡單的一種操作,但也不適應用實際中一些較大的應用,因為單詞的出現頻率並不是均勻的,比如以e開頭的單詞就明顯多於以i、j、k字母開頭的單詞,當然我們這 樣設計的程序是最適應於未知情況的。
2、關於過程的體驗
1) 編碼前細致入微的思考。本次工程很得益於實際編碼前周全的思考,讓最重要的代碼很流暢地通過,以致最後出錯的也不是這一部分的代碼。在接下來的團隊項目中,我也將繼續這一設計理念,在功能確定 後,實際編碼前細致地考慮每一步的實現,考慮可能出現的漏洞。
2) deadline的急躁。為了進行盡可能的所謂的優化,就跳過了嚴密的分析階段,結果把程序改錯而逾期,無力回天。
3) 程序數據結構與接口定義清晰、參數沒有歧義。在編寫稍微大一點的程序的時候,就算是一人編寫,也需要註意這個問題,否則編寫到最後很可能一團糟,幸好本次較早地意識到這個問題,代碼編寫較為流 暢。
六、跨平臺的思考與實踐
在實現了Windows平臺上的詞頻統計,便轉到Linux平臺上測試,其中程序中最重要的變化莫過於文件的遍歷,因為Linux系統是不支持io.h的(但支持stdio.h)。
一些比較重要的註意點歸納 參考(https://www.jianshu.com/p/f2fcd4628c12)
open系統調用
open系統調用建立了一條從到文件或設備的訪問路徑,該調用將得到與該文件相關聯的文件描述符(file discriptor)
#include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> int open(const char *path, int oflags); int open(const char *path, int oflags, mode_t mode);
write系統調用
write系統調用把緩沖區buf中的前n個bytes寫入與文件描述符fd相關的文件中。
#include <stdio.h> size_t write(int fd, const void *buf, size_t nbytes)
read系統調用
read系統調用從與文件描述符fd相關聯的文件中讀入nbytes字節的數據,並把它們放到buf中。
#include <unistd.h> size_t read(int fd, void *buf, size_t nbytes);
close系統調用
close調用終止文件描述符fd與其對應文件之間的關聯。文件描述符被釋放並能夠重新使用。close調用成功時候返回0,出錯時返回-1。
#include <unistd.h> int close(int fd);
lseek系統調用
lseek系統調用對文件描述符的讀寫指針位置進行設置> 參數whence定義該偏移量offset的用法,可取下列值
#include <unistd.h> #include <sys/types.h> off_t lseek(int fd, off_t offset, int whence);
fstat stat lstat系統調用
fstat系列調用返回與打開的文件描述符相關聯的文件的狀態信息,該信息將被寫入buf中。
stat和lstat返回的使通過文件名查詢到的狀態信息。它們產生相同效果,但當文件是符號鏈接時,lstat返回的是該符號鏈接本身的信息,而stat返回的使該鏈接指向文件的信息。
#include <unistd.h> #include <sys/types.h> #include <sys/stat.h> int fstat(int fd, struct stat *buf); int stat(const char *path, struct stat *buf); int lstat(const char *paht, struct stat *buf);
軟件工程個人作業 詞頻統計