# 2020q3 Homework3 (dict)
contributed by < `justapig9020` >
###### tags: `sysprog2020`
> [題目](https://hackmd.io/@sysprog/2020-dict)
> [GitHub](https://github.com/justapig9020/dict)
## 程式說明
### 運行模式
此程式分為兩個模式,區別為輸入的字串保存的方式。
1. CPY ( copy )
> 在 `CPY` 模式中,輸入的字串在插入至樹中時會複製一份,因此樹葉上的字串各自獨立保存。
3. REF ( reference )
> 在 `REF` 模式中,輸入的字串會統一保存於 `pool` 中。 `pool` 在程式開始時會 `allocate` 一塊記憶體位址,樹葉實際上是指向這塊記憶體之中的對應位址,因此字串彼此相鄰。
```cpp
if (CPYmask) {
/* memory pool */
pool = (char *) malloc(poolsize * sizeof(char));
Top = pool;
}
```
### 測試程式
程式讀取給定字典檔,並透過對輸入字串前處理將每個單字獨立出來。
在不同模式中輸入的 `Buffer` 有所不同。
- CPY
> 由於字串在插入後會進行 `deep copy` ,此模式下使用拋棄式的 `buffer` ,每次讀檔都會將上次內容覆蓋掉。
```cpp
char word[WRDMAX] = "";
...
char *Top = word;
```
- REF
> 直接將輸入保存於先前 `allocate` 的 `buffer` 上。
```cpp
if (CPYmask) {
/* memory pool */
pool = (char *) malloc(poolsize * sizeof(char));
Top = pool;
}
```
接下來依序將單字插入 `ternary search tree` 並更新 `bloom filter` 。
```cpp=
while (fgets(buf, WORDMAX, fp)) {
/* do pre-processing */
while (*Top) {
if (!tst_ins_del(&root, Top, INS, REF)) { /* fail to insert */
...
}
bloom_add(bloom, Top);
...
}
...
}
```
最後進入 `for(;;)` 等待命令並做對應的處理。
## Ternary Search Tree
### 資料結構
主要資料結構 - 節點
```cpp
typedef struct tst_node {
char key;
unsigned refcnt;
struct tst_node *lokid;
struct tst_node *eqkid;
struct tst_node *hikid;
} tst_node;
```
![](https://i.imgur.com/ClTTi8y.jpg)
其中 `key` 為該節點代表的字元,`refcnt` 則是以該結點為結尾的字串被插入的次數。
另外每一節點指向另外3個節點,分別代表比較後
- 大於 `hikid`
- 等於 `eqkid`
- 小於 `lokid`
的狀況。
接下來搭配程式來了解運作方式。
### 插入
由於刪除與插入都需要走訪樹中的特定分支,因此插入與刪除被寫成同一個 `function` 。
首先我們聚焦於插入功能。
首先透過慾插入之字串走訪樹,若可以在樹中找到該字串則將對應的 `refcnt` 加一並返回該字串的位址。
```cpp=
while ((curr = *pcurr)) { /* iterate to insertion node */
diff = *p - curr->key; /* get ASCII diff for >, <, = */
if (diff == 0) { /* if char equal to node->key */
if (*p++ == 0) { /* check if word is duplicate */
if (del) { /* delete instead of insert */
...
} else
curr->refcnt++; /* increment refcnt if word exists */
return (void *) curr->eqkid; /* pointer to word / NULL on del */
}
pcurr = &(curr->eqkid); /* get next eqkid pointer address */
} else if (diff < 0) { /* if char less than node->key */
pcurr = &(curr->lokid); /* get next lokid pointer address */
} else { /* if char greater than node->key */
pcurr = &(curr->hikid); /* get next hikid pointer address */
}
...
}
```
若比對完樹上現有結點仍有字元並未被比較到(簡而言之,樹中沒有當前想加入的字串),則依序新增結點直到所有剩餘字元都加入樹中。
```cpp=
for (;;) {
/* allocate memory for node, and fill. use calloc (or include
* string.h and initialize w/memset) to avoid valgrind warning
* "Conditional jump or move depends on uninitialised value(s)"
*/
if (!(*pcurr = calloc(1, sizeof **pcurr))) {
fprintf(stderr, "error: tst_insert(), memory exhausted.\n");
return NULL;
}
curr = *pcurr;
curr->key = *p;
curr->refcnt = 1;
curr->lokid = curr->hikid = curr->eqkid = NULL;
if (!*root) /* handle assignment to root if no root */
*root = *pcurr;
if (*p++ == 0) {
...
}
pcurr = &(curr->eqkid);
}
```
:::info
第 `15` - `16` 行的判斷式似乎是多餘的。由於先前透過 `while` 迴圈走訪分支時, `root` 已是判斷條件,
```cpp
pcurr = root;
while ((curr = *pcurr))
```
換言之若 `*root == NULL` 則第一次進入 `for` 迴圈時 `pcurr` 會指向 `root` 。因此在 `calloc` 時 `root` 就會被分配到記憶體位址,並無需再進行額外的判斷了。
> 很好,等你做實驗確認後,就送 pull request 來改進程式碼!
> :notes: jserv
:::
最後根據給定的模式將字串新增至 `\0` 結點的 `eqkid`。
```cpp
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;
}
}
```
:::info
將走訪功能獨立出來,並將插入與刪除分離成兩個 `function` 。如此一來不僅不用花費分支度判斷目前模式,且我認為於架構上會比較整潔。嘗試更改[程式](https://github.com/justapig9020/dict/tree/find_next)為上述架構。
> 這是你自己的作業和開發紀錄,當然觀點是自己的,請避免說「我個人的觀點」,你本來就沒有代表誰發言,不要因電視新聞看多了,忘了直觀的漢語怎麼書寫。有疑慮就去證實,並且送 pull request 過來!
> :notes: jserv
> 已修正,以下為思考過程
> ![](https://i.imgur.com/3s4zc7T.png)
> :notes: justapig9020
:::
### 刪除
刪除也是透過 `tst_ins_del` 進行。差別在於 `del` 在走訪樹時會將走過的節點透過 `tst_stack_push` 推入 `stk` 。
```cpp
tst_stack_push(&stk, curr); /* push node on stack for del */
```
如果最後有在樹中找到慾刪除的字串,就會將對應的 `refcnt` 減一並呼叫 `tst_del_word` 進行刪除。
首先如果 `refcnt` 不為 `0` 代表此字串還有被參考,因此只返回指向目標結點的指標。
若 `refcnt` 為 `0` 則代表以無參考,需刪除字串。
```cpp
if (!victim->refcnt) { /* if last occurrence */
...
} else /* node->refcnt non-zero */
printf(" %s (refcnt: %u) not removed.\n", (char *) node->eqkid,
node->refcnt);
```
刪除字串,首由下往上先將該分支中沒有 `lokid` \ `hikid` 的節點刪除。
在 `tst` 中若兩字串在開頭有共用的子字串的話,這兩字串會共用節點直到出現分歧。後加入的字串會由分歧點開始,依據該字元大小被加入 `lokid` 或 `hikid` 為根的子樹。
換言之,當一節點沒有上述兩分支的話,意味著該節點並未被其他字串參考。由此作為判斷將沒有被參考的節點刪除。
```cpp
while (!parent->lokid
&& !parent->hikid
&& !victim->lokid
&& !victim->hikid) {
parent->eqkid = NULL;
free(victim);
victim = parent;
parent = tst_stack_pop(stk);
if (!parent) { /* last word & root node */
free(victim);
return (void *) (*root = NULL);
}
}
```
此處需注意的是, `tst_stack_pop` 便是將之前走訪時推入的節點 `pop` 出來。透過此方法,便可以反向走訪之前的節點。
當該 `stack` 為空時,意味著已經回到整顆樹的根了,此時呼叫此函數會返回 `NULL` 。
```cpp
static void *tst_stack_pop(tst_stack *s)
{
if (!s->idx)
return NULL;
void *node = s->data[--(s->idx)];
s->data[s->idx] = NULL;
return node;
}
```
當碰到第一個有分支的結點後有幾種狀況。
#### `victim->lokid` 與 `victim->hikid` 皆存在
首先,當由於 `victim->lokid->hikid` 或 `victim->hikid->lokid` 沒有子樹的狀況下,可以直接將另一棵子樹接上,並利用被接上的節點取代 `victim` ,詳細狀況如下圖所示。
1. `victim->lokid->hikid = victim->hikid`
![](https://i.imgur.com/1Ze0igv.jpg)
![](https://i.imgur.com/D09ISgl.jpg)
2. `victim->hikid->lokid = victim->lokid`
![](https://i.imgur.com/3h5tZAN.jpg)
![](https://i.imgur.com/lSGO4F9.jpg)
以情況 `1` 為例,由於`victim->lokid->key` 必定小於 `victim->hikid->key` 所以可以不透過比較直接完成串接。另一情況同理。
然而當上述兩子樹都不為空時,由於無法直接串接子樹,在此實作中選擇移除該節點的 `eqkid` ,藉此讓以移除的字串無法再次匹配。
![](https://i.imgur.com/NpRml96.jpg)
![](https://i.imgur.com/CO7XcrJ.jpg)
在刪除節點時會使用到 `macro` - `del_node` ,在此 `macro` 中會檢查 `victim` 在 `parent` 中的位置,並將要替換的子樹串接上去。
---
#### `victim->lokid` 或 `victim->hikid` 中只有一個存在
由於 `victim` 只有一棵子樹,無需額外處理直接將子樹透過 `del_node` 串接上 `parent` 即可。
#### `vintim` 沒有子樹
---
## Bloom Filter
### 新增