# 2017q3 Homework2 (prefix-search)
contributed by <`changyuanhua`>
## 開發環境
* 輸入指令 ` lscpu `
```
Architecture: x86_64
CPU 作業模式: 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 4
On-line CPU(s) list: 0-3
每核心執行緒數:1
每通訊端核心數:4
Socket(s): 1
NUMA 節點: 1
供應商識別號: GenuineIntel
CPU 家族: 6
型號: 94
Model name: Intel(R) Core(TM) i5-6300HQ CPU @ 2.30GHz
製程: 3
CPU MHz: 799.890
CPU max MHz: 3200.0000
CPU min MHz: 800.0000
BogoMIPS: 4608.00
虛擬: VT-x
L1d 快取: 32K
L1i 快取: 32K
L2 快取: 256K
L3 快取: 6144K
NUMA node0 CPU(s): 0-3
```
## Trie 介紹
Trie ,又叫做前綴樹或是字典樹,是一個動態配置或關聯性配置的資料結構,用來保存關聯數組,常用在 string。 而當運用在 string 時, Trie 中每個節點由一個字元組成。
* Trie vs BST: Trie 不會直接將 key 儲存在節點中,反而是它在樹中的位置定義了它所關聯的 key。
* Trie樹的基本性質可以歸納為:
* 根節點不包含字元,除了根節點外的每個節點只包含一個字元。
* 從根節點到某一個節點,路徑上經過的字符連接起來,為該節點對應的字符串。
* 每個節點的所有子節點都有相同的前綴字串。
* 不是所有的節點都有對應的值,只有葉子節點和部分內部節點所對應的 key 才有相關的值。
* Trie 的應用: 主要在 bioinformatics , information retrieval
* Trie 的建立:
* 建立一個根節點,根節點必為空
```
@
```
* 插入單字 tea
1.先把 tea 拆分成 t, e, a
2.第一次出現前綴 t ,因此在根節點下建立新的子節點,節點為 t
3.從節點 t 開始看,e 也是第一次出現,因此在節點 t 下創造一個新的節點 e ......
```
@
/
t
/
e
/
a
```
* 再插入單字 to
1.節點 t 存在,就不再建立
2.節點 t 下不存在 o ,因此創造一個新的節點 o
```
@
/
t
/ \
e o
/
a
```
* 再插入 inn
* tea , to , in , inn 分別對應的value值為 0,1,2,3
* 執行 get(i) 時,會 return NULL,因為搜尋的末節點並不存在 value 值
* 執行 get(in) 時,會 return 2
* 執行get(ina)時,也會return NULL,因為 in 節點後並沒有 a 的節點位置
```
@
/ \
t i
/ \ \
e o(1) n(2)
/ \
a(0) n(3)
```
* 歸納:
* key 標註在節點中,而值標註在節點之下
* 可看出每一個值都對應一個完整的英文單詞,並且可以將 Trie 看作是一個確定有限狀態自動機。
* Trie 樹中,字符串 tea , to 都有相同的前綴 (prefix)為 “t” ,因此可以發現如果我們要存儲的字符串大部分都具有相同的前綴(prefix)時,那麼該 Trie 樹結構可以節省大量內存空間,因為 Trie 樹中每個單詞都是通過 character by character 方法進行存儲,所以具有相同前綴單詞是共享前綴節點的。
* 相反,如果 Trie 樹中存在大量字符串,並且這些字符串基本上沒有公共前綴,那麼相應的 Trie 樹將非常消耗內存空間,因此可得 Trie 的缺點是空指針耗費內存空間。
reference: [trie](https://zh.wikipedia.org/wiki/Trie)
## Ternary Search Tree (TST) 介紹
Trie 樹的結構,它的實現簡單但空間效率低。如果要支持26個英文字母,每個節點就要保存26個指針,假如還要加入標點符號、區分大小寫,內存用量就會急劇上升,以至於不可行。
因為 trie 的節點數組中保存的空指針,佔用了太多內存,因此考慮改用其他數據結構去代替,像是用 hash map。但是管理成千上萬個 hash map 不是什麼好主意,而且它還會使數據的相對順序信息丟失,所以使用 Ternary Tree 替代 trie。
* Binary Search Tree 查詢與建表時間是 O(log2n),但是在最糟的情況下時間複雜度可能轉為 O(n),除了要儲存的字串外不須另外分配空間。
* Trie 的時間複雜度是 O(n),能夠實現前者難以做到的 prefix-search,缺點是會有大量的空指針表,造成空間開銷過大。
* Ternary Search Tree,是一種 trie(亦稱 prefix-tree) 的結構,並且它結合字典樹的時間效率和 BST 的空間效率優點。
* 每個節點 (Node) 最多有三個子分支,以及一個 Key
* Key 用來記錄 Keys 中的其中一個字元 (包括 EndOfString)。
* low :用來指向比當前的 Node 的 Key 小 的 Node。
* equal :用來指向與當前的 Node 的 Key 一樣大 的 Node。
* high :用來指向比當前的 Node 的 Key 大 的 Node。
* Ternary Search Tree 的應用:
* spell check: Ternary Search Tree 可以當作字典來存儲所有的單詞。一旦在編輯器中輸入單詞,可以在Ternary Search Tree中並行搜索單詞以檢查正確的拼寫。
* Auto-completion: 使用搜索引擎進行搜索時,它會提供自動完成(Auto-complete)功能,讓用戶更加容易查找到相關的信息;假如:我們在Google中輸入ternar,它會提示與ternar的相關搜索信息。
![](https://images0.cnblogs.com/blog/183411/201212/30214221-c8318b20976d4b4da3f68bad9d1ffaf1.png)
Google根據我們的輸入ternar,提示了ternary,ternary search tree等等搜索信息,自動完成(Auto-complete)功能的實現的核心思想三叉搜索樹。
對於Web應用程序來說,自動完成(Auto-complete)的繁重處理工作絕大部分要交給服務器去完成。很多時候,自動完成(Auto-complete)的備選項數目巨大,不適宜一下子全都下載到客戶端。相反,三叉樹搜索是保存在服務器上的,客戶端把用戶已經輸入的單詞前綴送到服務器上作查詢,然後服務器根據三叉搜索樹算法獲取相應數據列表,最後把候選的數據列表返回給客戶端。
![](https://images0.cnblogs.com/blog/183411/201212/30214224-1fdcd259d4fa4b398d2a5a1fa50edb30.png)
reference: [Ternary Search Tree 的應用]
(http://www.cnblogs.com/rush/archive/2012/12/30/2839996.html)
* Ternary Search Tree 的建立
* 插入 food ,拆分成 f , o , o , d ,`將 f 放入根節點`
1. 建立 root
2. 發現沒有字元在 root 中,設定為節點 f
3. 建立新的節點
4. 發現沒有字元在節點中,設定為節點 o
5. ......
![](https://i.imgur.com/nnzPEh6.png)
* 插入單字 foot
1. 先比對 f,發現根節點與 foot 的 f 相同
2. 因此 pointer 往下,到節點 o ,發現與 foot 的 o 相同
3. 因此 pointer 往下,到節點 o ,發現與 foot 的 o 相同
4. 因此 pointer 再往下,到節點 d ,發現與 foot 的 t 不同,建立新的節點
5. 因為 **t>d** ,因此節點 d 的 **右子樹** 為 節點 t
![](https://i.imgur.com/Jh6OphM.png)
* 插入單字 for
![](https://i.imgur.com/3pkpg3E.png)
* 插入單字 foal
1. 先比對 f,發現根節點與 foal 的 f 相同
2. 因此 pointer 往下,到節點 o ,發現與 foal 的 o 相同
3. 因此 pointer 再往下,到節點 o ,發現與 foal 的 a 不同,建立新的節點
4. 因為 **a<o** ,因此節點 o 的 **左子樹** 為 節點 a
5. ......
![](https://i.imgur.com/n4Llpve.png)
* 插入單字 dog
![](https://i.imgur.com/Z6o6n9Z.png)
reference: [Ternary Search Tree (Trie with BST of children)](https://www.cs.usfca.edu/~galles/visualization/TST.html)
reference: [twngbm 共筆](https://hackmd.io/s/rJKkryGpb)
## prefix-search 實做
* 編譯指令: `make`
* 執行指令: `./test_cpy`
### 修改 test_ref.c
發現把 `CPY` 改為 `REF` ,仍然可以使正確編譯,但是執行結果是錯誤的
* quit : 回報錯誤訊息
* search : 只有找到輸入字串本身,如下
```
choice: s
find words matching prefix (at least 1 char): Tain
Tain - searched prefix in 0.000011 sec
suggest[0] : Tain
suggest[1] : Tain
suggest[2] : Tain
suggest[3] : Tain
suggest[4] : Tain
suggest[5] : Tain
suggest[6] : Tain
suggest[7] : Tain
suggest[8] : Tain
suggest[9] : Tain
suggest[10] : Tain
suggest[11] : Tain
```
#### quit 解決方法
在 `tst.c` 檔中,有兩個 function
* `tst_free( )` : free the ternary search tree rooted at p, data storage **external** ,推斷專屬為 `REF` 所使用
* `tst_free_all( )` : free the ternary search tree rooted at p, data storage **internal**. ,推斷專屬為 `CPY` 所使用
因此將 `tesr_ref.c` 檔中的 `tst_free_all( )` 修改為 `tst_free( )` ,而後 quit 可以正確執行。
```clike=
case 'q':
tst_free(root);
/* tst_free_all(root);*/
return 0;
break;
```
#### search 解決方法
在 `tesr_ref.c` 檔中,有著下列的程式碼以及註解:
```clike=
/* FIXME: insert reference to each string */
res = tst_ins_del(&root, &p, INS, REF);
```
因此至 `tst.c` 檔中,觀看 `tst_ins_del()` 函式,下列為 `tst_ins_del()` 的程式碼
```clike=
void *tst_ins_del(tst_node **root, char *const *s, const int del, const int cpy)
{
/*......*/
if (*p++ == 0) {
if (cpy) { /* allocate storage for 's' */
const char *eqdata = strdup(*s);
if (!eqdata)
return NULL;
curr->eqkid = (tst_node *) eqdata;
return (void *) eqdata;
} else { /* save pointer to 's' (allocated elsewhere) */
curr->eqkid = (tst_node *) *s;
return (void *) *s;
}
}
/*......*/
}
```
從上面的程式碼可以發現,當 cpy 為 0 時,curr->eqkid 就會直接存 *s ,也就是將字串 (s) 直接存進 leaf 。
從上面的註解得知:
* CPY 不是零時,會直接指派一個空間給 s,利用 strdup() 預先指派好一塊記憶體空間放置複製的字串了
* CPY 是零時,會讓 s 存一個指標。
而在 `tesr_ref.c` 檔中,有著下列的程式碼:
```clike=
p = word;
tst_ins_del(&root, &p, INS, REF)
```
從上面兩段程式碼可以得知 *s 對映到 word 這個 pointer to character ,也就是說所有 insert 操作的節點都存到以 word 為起點的連續記憶體位址的其中一個位址。
解決方法: 利用 `strdup()` ,其會先用 malloc() 配置與 word 相同的空間 ,存入到此位址,然後反回位址值。如此就可以讓他們的位址皆不同。程式碼如下:
```clike=
p = strdup(word);
tst_ins_del(&root, &p, INS, REF)
```
結果 `./tesr_ref` 與 `./tesr_cpy` 相同:
```
choice: s
find words matching prefix (at least 1 char): Tain
Tain - searched prefix in 0.000011 sec
suggest[0] : Tain,
suggest[1] : Tainan,
suggest[2] : Taino,
suggest[3] : Tainter
suggest[4] : Taintrux,
```
reference: [st9007a 共筆](https://hackmd.io/s/SkeKQEq3Z)