前段時間寫的,把老師留的作業寫得詳細了些,現在把它貼上來,有錯誤歡迎指正,有需要改進的地方也歡迎提出!
1. 題目要求
1. 數據: sentencesFile.txt是英文語句集合文件。句子之間有字符‘\n’分割,sentencesFile.txt文件大小<=1GB, 其中最短句子長度為6個字符,最長句子長度超過1000Kbyte。
計算環境:機器內存為2GB,2個CPU。
要求:設計對於隨機輸入的句子X判斷sentencesFile.txt內是否存在相同句子的算法。
2. 數據: sentencesFile.txt是英文語句集合文件。句子之間有字符‘\n’分割,sentencesFile.txt文件大小<=10GB, 其中最短句子長度為6個字符,最長句子長度超過1000Kbyte。
計算環境:機器內存為2GB,2個CPU。
要求:設計對於隨機輸入的句子X判斷sentencesFile.txt內是否存在相同句子的算法。
2. 簡易分析
當文件大小為1GB的時候,這些數據可以輕易放入內存,在內存中進行處理即可。而當文件大小為10GB的時候,這些數據無法一次放入內存。但是盡管當文件大小為1GB的時候可以將它們整個放入內存,普通的內部排序方法(比如快速排序、堆排序、冒泡排序等)雖然可以實現排序的功能,但是在如此大的數據面前,效率是很低下的。所以可以選擇桶作為存儲單位,這樣可以減少排序所需做的工作。
3. 算法介紹
3.1 桶排序
桶排序可以利用函數的映射關系,將大量的數據分配到多個“桶”中,然后在桶內進行排序,這樣的話工作量會遠遠小於單純使用內部排序方法。一個合適的映射函數可以直接使桶的順序符合題目要求,這樣的話只需要在桶內進行排序即可。
桶排序的時間主要消耗在這么幾個地方:對每個句子計算映射函數,時間復雜度為o(n);對每個桶內的所有數據分別進行排序,時間復雜度有多種選擇,在3.2中介紹。
減少時間復雜度有兩種方法:一種是盡量使每個桶中的數據量相差不多,另一種是增大桶的數量。
需要注意的是,桶內的數據量很不穩定。由於txt文件是英文語句集合字符串,所以"You are"、"What is"、"I am"等出現在首位的頻率比較高,而類似"aaaaa"、"qqqqq"出現在首位的頻率一定很低(有很大的可能根本不會出現),但是如果對每個"aaaaa"、"aaaab"……都創建一個桶的話,勢必會造成空間的浪費,26的3次方為17576,但是26的4次方就已經達到了456 976 。所以需要改進一下桶排序方案。
所以要先統計句子的前幾個字節出現的頻率,再將它們進行映射並排序。老師給出的思路是3個,但是我認為如果英文句子符合詞法以及語法規則的話,使用4個字節也是完全可以的(當然這只是我自己的想法)。粗略思考一下,4個連續的除aeiou之外的字母在英文單詞中是很少見的,這樣就可以從28的4次方(在此假設包括了單引號和空格)中減去21的4次方,還可以減去很多無意義的字母排列組合,這樣估算下來我認為桶的數量可以控制在10000以內,如果將頻率低的關鍵字合並的話,會進一步減少桶的數量。使用4個字節還可以通過增大桶的數量來減少時間復雜度(雖然代價是空間復雜度),所以我認為還可以選擇統計句子前四個字節出現的頻率。然后就可以通過在桶間和在桶內的方法對大數據進行排序。
3.2 桶內排序方法的選用
需要為桶內的排序挑選一個適當的排序方法。我認為使用快速排序比較好。因為句子長度從6個字符到超過1000kb,句子本身就比較無序、隨機,所以不適合選用堆排序、歸並排序、插入排序、冒泡排序。待排記錄的關鍵字只有前6位在一個明顯有限的范圍內,而第6位之后的關鍵字不能保證。而且程序對穩定性沒有要求,通過以上的要求,並結合各種排序方法的優缺點,選擇對桶內的數據使用快速排序的方法。此排序方法平均時間復雜度為o(nlogn),最好情況為o(nlogn),最壞情況為o(n2),是不穩定的,不適合序列局部或整體有序的情況,所以我認為在本程序的情況下快速排序是最好的選擇。
3.3 使用索引
可以對比較大的文件建立一個索引表,以加快查找速度。數據庫中就使用到了索引,可以用來對數據庫表中一列或多列的值進行排序,大大提升了數據庫性能。
無論主文件是否按關鍵字有序,索引表中的索引項總是按關鍵字順序排列,這時可以使用折半查找法在索引中檢索。需要注意的是,對於數據量比較大的文件,需要視情況而定使用多級索引。
對於英文句子,可以分段建立索引。比如一級索引為前三個字節,而二級索引為第三到第十個字節。
假設使用了三級索引。可以這樣設計,比如一級索引可以依照前三個字節划分:
編號 |
index |
0 |
0 |
1 |
597 |
2 |
1124 |
3 |
1806 |
…… |
其中某個編號(假設為編號0)又對應着如下的二級索引。因為字符串長度不等,所以此索引會對應好幾種情況,比如遇到句子結尾或者直接讀取磁盤文件等:
編號 |
index |
數目 |
標記 |
0 |
0 |
1 |
1 |
1 |
143 |
3 |
3 |
2 |
266 |
2 |
2 |
3 |
379 |
0 |
1 |
…… |
其中數目為下一級索引(或磁盤文件中的剩余句子)中的數目,而標記各自有着不同的含義。0 表示句子結束;
1 4個字節,下一級索引;
2 8個字節,下一級索引;
3 讀取磁盤文件。
而最后的一級索引便可以這樣設計:
編號 |
index |
指向的文件行 |
0 |
0 |
41299 |
1 |
1 |
1078 |
2 |
2 |
91107 |
3 |
3 |
63702 |
…… |
這樣可以保證每次訪問磁盤的次數為1。
4. 詳細設計
4.1 思考題一
大概流程如下(圖就不貼了,直接寫文字):
開始->統計句子前三個字節出現的頻率->映射到按順序存放的各個桶->合並數據量較少的桶->划分為文件->分別在每個文件中排序->結束
偽碼:
while(!EOF) { line = readline(file); hash(line); } if(the number in some barrels are little) merge(barrels); for every barrel { saveToFile(file); quicksort(file); }
首先統計句子前三個字節出現的頻率。需要設計N 個按順序排放的桶,這些桶依照前三個字節進行划分。需要注意的是在英文句子中不是任意三個連起來的字符都是有意義的(比如”aaa”,”bbb”這樣就是無意義的,絕大多數情況之下也不可能出現),所以桶的數量應該不會太過龐大,並且后面還會說到,對於字節出現頻率低的幾個桶,完全可以把它們合並起來。
具體做法就是遍歷這些英文句子,然后對它們進行映射並且把它們按桶的順序分割成小文件。映射之后前三個字節的順序就已經排好了。比如遍歷之后可能是這樣:
編號 |
所代表的字節 |
數量 |
索引 |
0 |
"aba" |
215 |
0 |
1 |
"abb" |
310 |
215 |
…… |
|||
15 |
"att" |
871 |
1846 |
16 |
"atu" |
109 |
2717 |
…… |
但是關鍵字出現的頻率很不穩定。因為句首常用詞出現的頻率肯定遠大於生僻單詞出現的頻率,而且對於桶排序,在各桶中數據量差不多的情況下,效率可以得到提高。所以可以將幾個數據量較少的桶(可以根據數量和索引決定)合並起來,然后再進行下面的步驟。
比如說以下這種情況:
編號 |
所代表的字節 |
數量 |
索引 |
…… |
|||
40 |
"cab" |
93 |
17829 |
41 |
"cac" |
159 |
17922 |
42 |
"cad" |
54 |
18081 |
…… |
由表格可以看出,40-42這三個桶內數據量比較少,所以完全可以合並到一起,它們的索引就是“cab”的索引,即為17829。
最后可以得出類似這樣的一個表格,而且也依照順序划分為了一些小文件。其中每個編號代表一個文件,文件的內容就是以這個編號所代表的字節開頭的語句集合:
編號 |
所代表的字節 |
數量 |
索引 |
0 |
"aba"、"abb"、"abc" |
679 |
0 |
1 |
"abe"、"abo"、"abr"、"aca" |
801 |
679 |
2 |
"ace" |
586 |
1480 |
3 |
"ach"、"ack" |
732 |
2066 |
4 |
|||
…… |
可以看出,經過合並之后,每個桶的數據量比較近似,可以進行接下來的步驟。
現在已經划分出了多個小文件,這時就可以對這些小文件分別進行排序了,在內存允許的情況下完全可以使用多線程來加快速度,因為它們互相都是獨立的。上面也介紹到,對於每個桶中的數據,可以使用快速排序的方法。每個桶中的數據排好序之后,整個英文語句集合也就排好了序。比如說,可能是以下的情況:
桶0中:
A bottle of……
A boy is……
A little girl……
……
桶1中:
Abolish this……
Abolished……
……
……
可以看到每個桶中的英文句子都是有序的,桶也是有序的,這樣就算排好了順序。如果目的只是判斷某個句子在文件中是否存在,那么排好序的小文件就沒有必要再寫入一個大的結果文件了,可以省去一次寫數據的工作。總體來說,本方法需要讀兩次全部的數據,寫一次全部的數據。
使用這種方法的話,如果需要查找某一個句子,可以先根據這個句子的前三個字節查找這個句子在哪個桶,然后到對應的桶中再去查找,這樣可以節省很多時間。
4.2 思考題二
當句子文件為10GB的時候,上面的方法就變的不是很合適了,開銷很大。而另一個需要注意的問題是,讀寫磁盤與讀寫內存比起來,所花的時間肯定更多,所以查找時需要盡量減少讀寫磁盤的次數以提高程序的效率。可以把10GB大小的文件分割成好幾部分(比如說分割成10份1GB大小的文件),每部分文件分別排序(使用上一題中的方法)。雖然這樣可以排序成功,但是查找的時候就比較費力氣。這時可以使用索引,即把位置等信息存入內存,而數據信息留在文件中,查找的時候可以先讀內存,從索引中查找然后再到磁盤上查找文件中的內容即可。
由於排序過程與思考題一比較類似,這里不再贅述。由於此題中句子文件比思考題一大了10倍,所以比思考題一多了索引的內容。
索引的設計參照3.3。
假設有一個句子:
He is responsible for the administration of justice.
按照3.3所設計的索引,大概面貌應該是下圖這個樣子:
而在索引中添加了如3.3中所述的標記后,可以保證每次訪問磁盤的次數為1。
5. 總結
對於大數據來說,最重要的是算法的選用,一個合適的算法可以省去大量的時間。本題最重要的一個方法就是“分而治之”。
在完成這個大報告的過程中,我也了解到了針對不同的操作大數據的需求,可以有不同的方法,還了解了一些以前未曾了解過的概念。比如敗者樹、trie樹、倒排索引、MapReduce等。大數據在當今時代變得越來越重要,以后在這方面還是需要多加了解。