我毫不猶豫的把這個演算法稱為“超酷”,雖然我自己發明了它,但我還是覺得它相當的酷,而且它很適合我演算法系列的主題,所以無論如何要把它寫下來。
當涉及到尋找單詞字謎時,用的比較頻繁的方法是字謎字典,簡單得說,對單詞的字母進行排序,以提供一個單詞與所有字謎共同點的唯一索引。另外一種方法是為單詞裡的每個字母生成字母頻率直方圖。(這兩種方法實際上或多或少相同。)這些方法查詢確切的單字字謎字串非常高效 – 如果使用雜湊表,複雜度為O(1)。
然而,如果問題是查詢字謎的子集(包含一個字串裡字母的一個子集的單詞),仍然是相當低效的,通常需要在O(n)時間內暴力搜尋整個字典,或者查詢每個有序字串的子串,複雜度與輸入字串的字母長度有關,為O(2^l)。查詢字謎子集顯然更有趣,因為它能查詢多字字謎,可以應用在拼字遊戲上。
不管怎樣,我們先生成能唯一表示一組字母的直方圖,再努力觀察,我們可以生成一個樹結構來更有效得查詢字謎子集。為了構建這樣的樹,我們按照如下幾個簡單的步驟:
假設我們有如下資訊:
- 一個詞典或單詞字典來填充樹
- 詞典中單詞的字母表
- 一個正在構建的樹
- 當前節點
詞典裡的每個單詞:
1.為該單詞生成字母頻率直方圖。
2.設當前節點為樹的根節點。
3.每個字母表裡的字元:
獲取當前字元在當前單詞裡的頻率,記為f
設定當前節點為當前節點的第f個子節點,如果它不存在的話就建立
4.將當前單詞新增到當前(葉)節點上的單詞列表
以下是這個簡單過程的結果,它是一棵固定高度的樹,27個節點深,所有單詞都在葉節點中,並且樹的每個層級對應字母表裡的字元。下面是個簡略的例子(譯註:原部落格圖片遺失,從WIKI上找了張替圖):
一旦樹建立好後,我們可以如下方式查詢輸入字串的字謎集合:
假設我們有如下資訊:
由上述流程所構建的樹
上面使用過的字母表
一個邊界集合,初始化為空
1.初始化時邊界集合只包含樹的根節點
2.生成輸入字串的字母頻率直方圖
3.對字母表中的每個字元:
1.獲取當前字元在輸入字串裡的頻率,記為f
2.對邊界集合裡的每個節點,新增標號為0到f的子節點到新的邊界集合中
4.當前邊界集合中包含的葉節點,包含所有輸入字串的字謎子集
至少對我來說,對該演算法進行執行期分析比較困難。直觀的看,它在實踐中比任何一種蠻力演算法要快很多,但我無法量化為大O表示法。作為一個上限,它不可能比O(n)的效率低,最壞也比蠻力演算法少一個常數因子。作為下限值,邊界集合中只有一個節點,那查詢時間就與字母表長度成正比,為O(1)。平均情況下,依賴輸入字串所選擇的字典的子集有多大。以輸出的大小來量化的話,需要O(m)的操作。如果有人知道如何確定執行時更準確的範圍的話,請在評論中讓我知曉。
這個演算法有個缺點就是,需要大量的記憶體開銷。我用python來實現,並匯入/usr/share/dict/words,在本機上這大約是2MB的大小,但需要佔用記憶體300MB。使用Pickle模組序列化到磁碟,輸出檔案的大小超過30MB,使用gzip壓縮後下降到大約7MB。我懷疑記憶體大的部分原因是python字典的最小尺寸。我將使用列表來實現,如果我能夠做到更高效,屆時我會更新這篇文章。
這裡是上述所生成樹的資料,可能你會感興趣:
總單詞數:234,936
葉節點:215,366
內部節點:1,874,748
由此我們可以看出,內部節點的平均基數是非常低的,不會大於1。下面資料有助於澄清:
Tier | Number of nodes |
---|---|
0 | 1 |
1 | 7 |
2 | 25 |
3 | 85 |
4 | 203 |
5 | 707 |
6 | 1145 |
7 | 1886 |
8 | 3479 |
9 | 8156 |
10 | 8853 |
11 | 10835 |
12 | 19632 |
13 | 28470 |
14 | 47635 |
15 | 73424 |
16 | 92618 |
17 | 94770 |
18 | 125018 |
19 | 156406 |
20 | 182305 |
21 | 195484 |
22 | 200031 |
23 | 203923 |
24 | 205649 |
25 | 214001 |
靠近樹的頂部節點的基數非常高,但樹很快變平,最後四層樹只佔總結點的一半。這暗示了一個可能的空間優化:刪除樹的最後幾層,將它們的葉子節點連在一起。當進行查詢時,檢查所選的節點,保證它們是輸入字串的字謎集合。
* 我可能只是重新發現了電腦科學領域30年前就被提及的論文。但驚喜的是,通過搜尋尚未找到誰正在使用該演算法,或者有其它方法比蠻力演算法更有效。
修訂:最初的實現程式碼在這。
修訂:使用列表來重新實現我的python程式碼,幾乎節約了一半記憶體。有機會我會貼出pickled後的樹和原始碼。
修訂:更多更新在這。
(譯註:字謎問題可簡化為字串編碼和索引問題,如Tea編碼為A1E1T1,編碼雜湊後,同編碼單詞有Ate,Eat等。文章寫於2007年,文中演算法不是最優解,只是提供了一種使用多路查詢樹的思路,類似資料結構有Trie,DAG,Suffix Tree等等。)