# 2017q1 Homework3 (prefix-search) contributed by <`river85511`> ### Reviewed by `ZixinYang` - 觀察程式碼的時候, 不要用語法來註記內容, 如 ( while 的部份, for 的部份... ), 應直接以該段程式碼的用意來註記。 - 程式解析清楚的, 但缺乏自己動手改寫的過程, 也沒有缺乏程式效能的分析。 - forward declaration 除了讓標頭檔的內容方便閱讀以外, 也可以減少一改寫 structure 的內容, 有 include 該個標頭檔的檔案都要重新編譯一次所需的時間。 --- ## 開發環境 ``` Lubuntu 16.04 4.10.0-37-generic Intel(R) Core(TM) i5-2520M CPU @ 2.50GHz L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 3072K ``` ## Ternary Search Tree (TST) ### TST - Introduction * TST * 是一種 [trie](https://en.wikipedia.org/wiki/Trie)(亦稱 prefix-tree) 的結構 * Keys 是一串字串,e.g. `ann`。 * 每個節點 (Node) 最多有三個子分支,以及一個 Key * `Key` 用來記錄 Keys 中的其中一個字元 (包括 `EndOfString`)。 * `low` ,用來指向比當前的 Node 的 Key 小 的 Node。 * `equal`,用來指向與當前的 Node 的 Key 一樣大 的 Node。 * `high` ,用來指向比當前的 Node 的 Key 大 的 Node。 * 觀察在 `tst.c` 中 `tst_node` ``` C /** ternary search tree node. */ typedef struct tst_node { char key; unsigned refcnt; struct tst_node *lokid; struct tst_node *eqkid; struct tst_node *hikid; } tst_node; ``` ``` o |-key |-refcnt ------------+------------ |lokid |eqkid |hikid o o o ``` * 發現 * 跟 [Trie](https://www.cs.bu.edu/teaching/c/tree/trie/) 中介紹的結構以外,還多了 `refcnt` * `refcnt` 是用來記錄一個 Keys 出現的次數。 * 舉例來說,若把 `cat` 插入 TST 中,會是如此的結構: ``` o |-c |-0 ------------+------------ |lokid |eqkid |hikid o o o |-a |-0 ----+---- | | | note: any of the lokid or hikid nodes o can also have pointers to nodes |-t for words that "cat" or "ca" is |-0 a partial prefix to. ----+---- | | | o |-0 |-1 <== the refcnt is only relevant to the final node ----+---- | | | NULL o NULL "cat" ``` * 發現: * 在記錄 `cat` 中的 `t` 的 `tst_node` 之後多了一個 記錄 `0` 的 `Key` 的 `tst_node` * 此 `key` 為 `0` 的 `tst_node` 用來表示 `cat` 這個字串的 `EndOfString`,以及記錄 `cat` 這個字串的出現的次數 (因此 `refcnt` 為 `1`)。 --- ## Ternary Search Tree Operation * 三種操作 (Operation) 為: * **搜尋** (Search) * **插入** (Insert) * **刪除** (Delete) ### TST - Search Operation * 兩種搜尋方式 * 找完整的字串 * 找字首 (prefix) * 「找完整的字串」 在 `test_cpy.c` 中相對應的程式碼 ``` C case 'f': res = tst_search(root, word); if (res) printf(" found %s in %.6f sec.\n", (char *) res, t2 - t1); else printf(" %s not found.\n", word); break; ``` * 「找字首」 在 `test_cpy.c` 中相對應的程式碼 ``` C case 's': res = tst_search_prefix(root, word, sgl, &sidx, LMAX); if (res) { printf(" %s - searched prefix in %.6f sec\n\n", word, t2 - t1); for (int i = 0; i < sidx; i++) printf("suggest[%d] : %s\n", i, sgl[i]); } else printf(" %s - not found\n", word); break; ``` ### TST - Insertion * 觀察在 `test_cpy.c` 中相對應的部份。 ``` C case 'a': res = tst_ins_del(&root, &p, INS, CPY); if (res) { idx++; printf(" %s - inserted in %.6f sec. (%d words in tree)\n", (char *) res, t2 - t1, idx); } else printf(" %s - already exists in list.\n", (char *) res); break; ``` ### TST - Deletion * 兩種刪除功能 * 刪除一個字串 * 刪除整個 TST * 「刪除一個字串」 在 `test_cpy.c` 中相對應的程式碼 ``` C case 'd': res = tst_ins_del(&root, &p, DEL, CPY); if (res) printf(" delete failed.\n"); else { printf(" deleted %s in %.6f sec\n", word, t2 - t1); idx--; } break; ``` * 「刪除整個 TST 」 在 `test_cpy.c` 中相對應的程式碼 ``` C case 'q': tst_free_all(root); return 0; break; ``` ## 觀察程式碼 * 從 `test_cpy.c` 可以發現 `TST` 的操作在 `tst.c` 中對應的函式為: * 搜尋字串 \ 字首:`tst_search()`、`tst_search_prefix()` * 插入、刪除字串:`tst_ins_del()` * 刪除整個 `TST`:`tst_free_all()` > TODO: 觀察上述的函式程式碼 ### 搜尋字串 `tst_search()` * `tst.c` 中的程式碼 ``` c void *tst_search(const tst_node *p, const char *s) { const tst_node *curr = p; while (curr) { /* loop over each char in 's' */ int diff = *s - curr->key; /* calculate the difference */ if (diff == 0) { /* handle the equal case */ if (*s == 0) /* if *s = curr->key = nul-char, 's' found */ return (void *) curr->eqkid; /* return pointer to 's' */ s++; curr = curr->eqkid; } else if (diff < 0) /* handle the less than case */ curr = curr->lokid; else curr = curr->hikid; /* handle the greater than case */ } return NULL; } ``` * `while` 迴圈的內容解讀 * `tst_node` 類型的指標 `curr` 指向的 `tst_node` 中的 `key` 會和 待搜索字串中的字元 `*s` 作比較 * 若是 `*s` > `key` 則將 `curr` 指向 `curr->hikid` 所指向的記憶體位置。 * 若是 `*s` < `key` 則將 `curr` 指向 `curr->lokid` 所指向的記憶體位置。 * 若是 `*s` = `key` * 且 `*s` 為字串中 `EOS (EndOfString)`,則表示找到待搜索的字串了,因此回傳 `curr->eqkid` 的位置。(因為 `curr->eqkid` 中紀錄著待搜索的字串,如上面提及的 `cat` 之例子。) * 若 `*s` 不是 `EOS`,則繼續在 `curr->eqkid` 中搜尋字串中的下一個字元(`s++`)。 * 重複以上步驟直到 `curr` 為 `NULL` 為止。 ### 搜尋字首 `tst_search_prefix()` * `tst.c` 中的程式碼 ``` C void *tst_search_prefix(const tst_node *root, const char *s, char **a, int *n, const int max) { const tst_node *curr = root; const char *start = s; if (!*s) return NULL; /* get length of s */ for (; *start; start++) ; /* wait */ const size_t nchr = start - s; start = s; /* reset start to s */ *n = 0; /* initialize n - 0 */ /* Loop while we haven't hit a NULL node or returned */ while (curr) { int diff = *s - curr->key; /* calculate the difference */ if (diff == 0) { /* handle the equal case */ /* check if prefix number of chars reached */ if ((size_t)(s - start) == nchr - 1) { /* call tst_suggest to fill a with pointer to matching words */ tst_suggest(curr, curr->key, nchr, a, n, max); return (void *) curr; } if (*s == 0) /* no matching prefix found in tree */ return (void *) curr->eqkid; s++; curr = curr->eqkid; } else if (diff < 0) /* handle the less than case */ curr = curr->lokid; else curr = curr->hikid; /* handle the greater than case */ } return NULL; } ``` * 可以被分成兩個部份來解讀 1. 紀錄字串長度的部份 2. `while` 迴圈的部份 * 先來看`1. 紀錄字串長度的部份` ```C /* get length of s */ for (; *start; start++) ; /* wait */ const size_t nchr = start - s; start = s; /* reset start to s */ *n = 0; /* initialize n - 0 */ ``` * 解讀: * 先透過 `for` 迴圈將 `start` 指向字串的最後一個字元 (`EOS`) * 用 `size_t` 型態的變數 `nchr` 來紀錄最後一個字元到第一個字元(也就是字串的全長) * 再來是 `2. while 迴圈的部份` ``` C while (curr) { int diff = *s - curr->key; /* calculate the difference */ if (diff == 0) { /* handle the equal case */ /* check if prefix number of chars reached */ if ((size_t)(s - start) == nchr - 1) { /* call tst_suggest to fill a with pointer to matching words */ tst_suggest(curr, curr->key, nchr, a, n, max); return (void *) curr; } if (*s == 0) /* no matching prefix found in tree */ return (void *) curr->eqkid; s++; curr = curr->eqkid; } else if (diff < 0) /* handle the less than case */ curr = curr->lokid; else curr = curr->hikid; /* handle the greater than case */ } ``` * 解讀 * 這個 `while` 迴圈和 `tst_search()` 中的 `while` 迴圈最大的差別在於:當搜尋到 待搜尋字串的`EOS` 的前一個字元時,就表示找到相符合的字首了,接著會呼叫 `tst_suggest()` 來將符合字首要求的字串全部儲存在一個 `char` 型態的雙指標(`double pointer`) `a` 中。 ### 插入、刪除字串:`tst_ins_del()` * 在 `tst.c` 中的程式碼 ``` C void *tst_ins_del(tst_node **root, char *const *s, const int del, const int cpy) { int diff; const char *p = *s; tst_stack stk = {.data = {NULL}, .idx = 0}; tst_node *curr, **pcurr; if (!root || !*s) return NULL; /* validate parameters */ if (strlen(*s) + 1 > STKMAX / 2) /* limit length to 1/2 STKMAX */ return NULL; /* 128 char word lenght is plenty */ pcurr = root; /* start at root */ 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 */ (curr->refcnt)--; /* decrement reference count */ /* chk refcnt, del 's', return NULL on successful del */ return tst_del_word(root, curr, &stk, 1); } 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 */ } if (del) tst_stack_push(&stk, curr); /* push node on stack for del */ } /* if not duplicate, insert remaining chars into tree rooted at curr */ 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; /* Place nodes until end of the string, at end of stign allocate * space for data, copy data as final eqkid, and return. */ 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; } } pcurr = &(curr->eqkid); } } ``` * 可以被分成三個部份來解讀 1. 檢查字串是否符合規範 2. `while` 迴圈 3. `for` 迴圈 * 首先是 `1. 檢查字串是否符合規範` ```C if (!root || !*s) return NULL; /* validate parameters */ if (strlen(*s) + 1 > STKMAX / 2) /* limit length to 1/2 STKMAX */ return NULL; /* 128 char word lenght is plenty */ ``` * 解讀 * 由於 `STKMAX` 在 `tst.c` 中的定義為 `WRDMAX` 的兩倍,因此若字串的長度加上 `EOS` 後比 `WRDMAX` 大的話,該字串就無法被插入到 `TST` 中。(註:在 `tst.c` 中,`WRDMAX` 被定義為 `128`) * 若待被插入或刪除的字串為 `0` 的話,則不插入或刪除。 * 接著是 `2. while 迴圈` ```C pcurr = root; /* start at root */ 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 */ (curr->refcnt)--; /* decrement reference count */ /* chk refcnt, del 's', return NULL on successful del */ return tst_del_word(root, curr, &stk, 1); } 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 */ } if (del) tst_stack_push(&stk, curr); /* push node on stack for del */ } ``` * 解讀時可以分成兩個情況: 1. `del == 0` 2. `del == 1` * 第一種情況: * 比較 `curr` 指向的 `tst_node` 的 `key` 和 字串中的字元 `*p` 的大小 * 若是 `*p` > `key`,則讓 `pcurr` 指向 指向`curr->lokid`的指標的記憶體位置。 * 若是 `*p` < `key`,則讓 `pcurr` 指向 指向`curr->hikid`的指標的記憶體位置。 * 若是 `*p` = `key` * 且字串的下一個字元(`*s++`)為 `EOS` 時,將`curr` 指向的 `tst_node` 的 `refcnt` 值加一。 * 若字串的下一個字元不為 `EOS`,則將 `pcurr` 指向 指向`curr->eqkid` 的指標的記憶體位置。 * 第二種情況: * 將含有 待刪除的字串的 `EOS` 之前 的字元的 `Node` 的記憶體位置儲存在 `stk` 中。 * 當字串的下一個字元(`s++`)為 `EOS` 時,將 `curr` 指向的 `tst_node` 的 `refcnt` 的值減一,並將 `stk` 中所儲存的記憶體的內容,透過呼叫 `tst_del_word()` 來釋放。 > 為什麼在新增或刪除字串時,不是將 `curr->eqkid` 的 `refcnt` 的值加一或減一?[name=David Kuo] * 最後是 `3. for 迴圈` ```C /* if not duplicate, insert remaining chars into tree rooted at curr */ 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; /* Place nodes until end of the string, at end of stign allocate * space for data, copy data as final eqkid, and return. */ 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; } } pcurr = &(curr->eqkid); } ``` ## 觀察字典檔 > TODO: 觀察字典檔 * `Cities.tst` * 內容為世界上各個地區的城市名稱 * 共有 `93827` 筆資料。 * 第一筆資料是 `Shanghai, China` * 排序沒有遵守 `A-Z` 的大小。 ## 測試程式 > TODO: 根據觀察的結果,預期可能會有的問題後,透過測試程式 `test_cpy` 來確認自己的想法,最後修正程式中的 bug。 ``` $ ./test_cpy ternary_tree, loaded 259112 words in 0.149612 sec Commands: a add word to the tree f find word in tree s search words matching prefix d delete word from the tree q quit, freeing all data ``` ## 問題探討 ### 觀察 `tst.h` 和 `tst.c` 為何要 Forward Declaration * 若別的程式員想要了解 `tst.c` 裡的函式的功能時,就可以在 `tst.h` 中很方便的閱讀當初的開發者對於 `tst.c` 裡各個函式的註解。