prefix-search
contributed by <f74034067
>, <catpig1630
>
TODO:
嘗試導入 Bloom Filter,實作快速字串搜尋機制。如果字串根本不存在於資料庫中,我們根本不需走訪 ternary search tree
功能描述
Trie (字典樹) & Ternary search tree (TST)
rex662624 的解說十分詳盡完整
Trie
trie 與 binary search tree 不同在於,不是把整個字串包在同一個node 中 ,而是根據 node 的位置決定代表的字串為何。也就是一個看結果( 最終 node ),一個看過程(經過的node)。因此利用 trie,可以快速的找出有相同 prefix 的字串。以下將舉例示範 trie 如何儲存 data。
一開始,trie tree 起始於一個空的root - ◎
若今插入一個字串 and
- 首先將 and 拆解成 a, n ,d
- 因為 a 第一次出現,所以在 root 建立一個新children node ,value= a
- 接下來從node "a" 出發,剩下 n,d
- n 也是第一次出現,因此在 a 建立一個新 children node ,value= n
- d 比照 4 作法。
若此時插入一個字串 ant
- ant 拆解成 a , n , t,從 root 出發
- node "a" 已經存在,因此不再在 root 創造新的 children
- 再來看 n , node "a" 往 children 看發現 node "n"已存在,因此也不創造 children
- 再來看 t 發現 n 的 children 中沒有此 node,因此創造新 children "t"
如此再插入 bear ,bea 就會產生像這樣子的 tree
- 有 value 的就是有 string 存在的 node
- 因此 search "ant" 會 return 值 0 ,表示 trie 內有此string
- 而 search "be" 會 retrun NULL , 因為並沒有值在 node "e"。
以上,我們注意到 Trie 中,若有儲存的字串有許多相同的 prefix ,那麼 trie 可以節省大量儲存空間,因為 Trie 中每個字串都是經由 character by character 方法進行儲存,所以具有相同 prefix 的部份都會有共同的 node 。
相對的,若是資料非常大量,而且幾乎沒有相同的 prefix 將會非常浪費儲存空間。因為 trie 在 memory 的 儲存方式其實是像下圖這樣,每個 node 建立時,都必須同時創造26個值為NULL的子節點。(下圖的 universal set ={a,b,c,d,e}5所以只創造了 5 個)
Image Not Showing
Possible Reasons
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →
Ternary search tree
因此用此 Ternary search tree ,就能解決上面的問題。這種樹的每個節點最多只會有3個 children,分別代表小於,等於,大於。以下範例解釋 Ternary search tree 。
插入一個字串 and
- 與 trie 概念相同,但是在root 是直接擺 "a" 而不需要一個null 。
接著插入單字ant
- 我們先比對 a ,發現 a 和 root 的字母相同(a=a)
- 因此 pointer 往下,來到 n 節點,發現 n 節點和 ant 的第二個字母相同(n=n)
- 因此 pointer 再往下,來到 d 節點,發現 d 節點和 ant 的第三個字母不同,而且 t>d
- 因此 d 節點的 right child 成為 t ,成功將 ant 放入原本的 TST 中
接著插入單字age
- 我們先比對 a ,發現 a 和 root 的字母相同(a=a)
- 因此 pointer 往下,來到 n 節點,發現 n 節點和 age 的第二個字母不同,而且 g<n
- 因此 n 節點的 left child 成為 g ,成功將 age 放入原本的 TST 中
Ternary Search Tree 此網址提供了用動畫表示的方式,可以清楚看到如何加入與刪除 string 的流程。
從 Ternary Search Tree 中,我們可以發現幾種特性。
- 解決 trie tree 中大量佔用記憶體的情況,因為 TST 中,每個 node 最多只會有3個 child
- 但尋找時間在有些 case 中 , 會比 trie 慢 ,而這是因為 TST 插入的順序會影響比較的次數。例如以下兩張圖,上圖是用
aa->ac->ae->af->ag->ah
的次序插入,而下圖是 af->ae->ac->aa->ag->ah
的順序。試想今天要尋找 ah ,則上圖要比較6次,下圖則是只要比較3次。而若是建立 trie ,只需要2次。
Image Not Showing
Possible Reasons
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →
Image Not Showing
Possible Reasons
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →
程式碼修正
修改 Makefile,$ make bench
進行效能評比
參考 raygoah 的作法
修改 FIXME
- 首先修改
test_ref.c
標注 FIXME
的地方
CPY 的版本由於 strdup(*s) 會先用 malloc() 配置與參數 s 字串相同大小的空間,然後將 s 字串的內容複製到該空間,所以每次回傳的地址都不相同。
REF 的版本中,每次傳入 tst_ins_del() 的 *p 都相同,curr -> eqkid 當然也就都指向同一個位址。
修正 REF 版本 searching 功能
參考 tina0405 的作法
修正 REF 版本 delete 功能
- 修改
tst.c
中 tst_del_word() 的最後一個參數,將原本寫死的 1 改為 cpy
修正 REF 版本 quit 功能
- quit 功能會出錯則是因為
test_ref.c
中 tst_free_all(root) 會 free 到 REF 版本中宣告的二維陣列的位址,若改成 tst_free(root) 就不會出錯了
比較 REF 與 CPY 的效能
先來測試一下兩種版本的效能,以執行 s Tai
100次為例
- CPY 優於 REF
在 REF 版本,使用陣列儲存 ternary search tree 中的某些節點,cache 的 spatial locality 特性反而有機會在搜尋中將許多尚不需用到的 data 搬到 cache 中,造成 cache-misses 較高,並且由於陣列宣告成固定大小,還會產生 memory fragmentation 的問題。
實做 memory pool
參考 ChiuYiTang 的作法
- 只紀錄兩個指標,一個 pPool 指向整個 memory pool 的開頭,一個 pTop 則指向下一次要分配出的記憶體位址。
參考 rex662624 將 insert 的時間抓出來比較
- $ make plot

memory pool 的好處在於省去了頻繁的 malloc() 與 free(),可以看到在建立 ternary search tree 的效能上 REF 略優於 CPY。
Prefix 缺陷
搜尋城市名問題
可以利用 fscanf 裡類似正規表達式的特殊寫法直接找出字串裡的程式名稱和國家名稱
把原本的%s 改成 %256[^,], %256[^\n]\n
其中[^\n]代表的是不包含 \n 字元,詳細說明可以參考 fprintf 的 man page
刪除不存在的字串會變成輸入
發現 prefix search 查詢 A 的時候會 segmentation fault
reference
afcidk
bauuuu1021
2017重做同學
tst_traverse_fn
TingL7的解說十分詳盡完整
- callback function
- 根據維基百科定義:a callback is any executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at a given time.
- 在 c 語言中, callback function 就是將 function pointer 當作參數傳入另一個 function 中執行。
tst_traverse_fn
function
- 就是一個遞迴的 callback function , 它將 fn() 當作參數。
- 這個函式的功能就是走訪整個樹的節點,並且在 leaves 執行使用者自訂函式 fn()。
- 應用:
- 計算 leaves 數量
結果
- 效能評比
可以比較兩者的走訪速度。
結果
- 印出資料庫中所有的國家、城市
結果