# 2024q1 Homework2 (quiz1+2) contributed by < [steven523](https://github.com/steven523) > ## 第一週測驗題 ### 測驗一 此題參考〈[Optimized QuickSort — C Implementation (Non-Recursive)](https://alienryderflex.com/quicksort/)〉,透過 quicksort() 實作非遞迴的快速排序法,並運用 stack 來模擬遞迴函式呼叫 以下是鏈結串列的結構體,有一個 long 儲存的資料及一個 `next` 指標,可判斷其為單向的鏈結串列,還有兩個指標為 `left` 和 `right` ```c typedef struct __node { struct __node *left, *right; struct __node *next; long value; } node_t ``` ```graphviz digraph "__node" { rankdir=LR; node[shape=record, style=bold]; subgraph cluster_0 { node [shape=record]; value [label="value"]; ptr [label="{<node> left |<node> right}|next"]; style=bold; label=node_t } } ``` 首先程式一開始為 `test_arr` 這個整數型態的陣列配置記憶體空間,接著指派 0~99999 給它,並呼叫 `shuffle` 函式將陣列中的元素打亂 `list_construct` 函式用於建立一個新節點,並將其插入到鏈結串列 `list` 的頭部,然後返回鏈結串列的首節點 所以下列程式整體上來說是透過 while 將 `test_arr[count]` 的值逐一插入 `list` 頭部,最終形成的鏈結串列會與 `test_arr[count]` 順序相反 ```c node_t *list_construct(node_t *list, int n) { node_t *node = malloc(sizeof(node_t)); node->next = list; node->value = n; return node; } int main(int argc, char **argv) { node_t *list = NULL; ... while (count--) list = list_construct(list, test_arr[count]); ... } ``` ```graphviz digraph list_construct { node[shape=record]; rankdir=LR; n1[label="{<data> node1|<next>}", color=red]; edge[tailclip=false,arrowtail=dot,dir=both]; n1:next:c -> NULL; } ``` ```graphviz digraph list_construct { node[shape=record]; rankdir=LR; n1[label="{<data> node1|<next>}"]; n2[label="{<data> node2|<next>}", color=red]; edge[tailclip=false,arrowtail=dot,dir=both]; n2:next:c -> n1; n1:next:c -> NULL; } ``` :::info `list_add` 與 `list_construct` 用途相似,都是將節點插入鏈結串列的頭部,只差在 `list_construct` 傳入值的型態是 `int`,所以要為其先用 `malloc` 配置一個記憶體位置 ::: `qiuck_sort` 函式在每一輪迴圈都會經過以下步驟 首先宣告`L` 及 `R` 分別指向鍵結串列的第一個節點與最後一個節點,並指派第一個節點為 pivot,`p` 為下一個節點,最後將 `pivot` 與原鏈結串列分開 利用以下程式碼走訪整個串列,並判斷目前的節點是否大於 `pivot->value`,是的話加入 `right`,否則加入 `left` ```c while (p) { node_t *n = p; p = p->next; list_add(n->value > value ? &right : &left, n); } ``` 此為原鏈結串列初始化過後的樣子,`left` 和 `right` 在每次回全結束前都會重設為空的鏈結串列 ```graphviz digraph G { node[shape=plaintext]; pivot; L R p [fontcolor="red"]; left; right; node[shape=box]; n0 [label= "3"]; n1 [label= "2"]; n2 [label= "1"]; n3 [label= "4"]; n4 [label= "5"]; { rank="same"; n0 -> NULL n1 -> n2 n2 -> n3 n3 -> n4 } pivot -> n0; L -> n0; R -> n4; p -> n1; } ``` 因為 `p` 指向的值 2 小於 `pivot` 的 3,所以將 `p` 指向的節點透過 `list_add` 函式加入 `left` 頭部, 接著 `p` 指向下一個節點 ```graphviz digraph G { node[shape=plaintext]; pivot; L R p [fontcolor="red"]; left; right; node[shape=box]; n0 [label= "3"]; n1 [label= "2"]; n2 [label= "1"]; n3 [label= "4"]; n4 [label= "5"]; { rank="same"; n0 -> NULL n2 -> n3 n3 -> n4 } pivot -> n0; L -> n0; R -> n4; p -> n2; left -> n1; } ``` 持續同樣操作直到整個鏈結串列走訪完的結果會像下圖 ```graphviz digraph G { node[shape=plaintext]; pivot; left; right; node[shape=box]; n0 [label= "3"]; n1 [label= "2"]; n2 [label= "1"]; n3 [label= "4"]; n4 [label= "5"]; { rank="same" } pivot -> n0; left -> n2 -> n1; right -> n4 -> n3; } ``` ```c begin[i] = left; end[i] = list_tail(&left); begin[i + 1] = pivot; end[i + 1] = pivot; begin[i + 2] = right; end[i + 2] = list_tail(&right) ``` 最後透過上述程式碼將 `left` 頭尾放入 beg[i], end[i] 中來取代原本的整個佇列的位置,right 頭尾則放入 beg[i+2], end[i+2],並將 i+2 後重新下輪迴圈進行排序 如果 `L != R` 成立,即 `beg[i]` 等於 `end[i]`,代表此佇列只有一個節點,此時就將該節點加入 `result` 。接著 i-- 後會將上圖 `pivot` 節點再加入 `result` ,最後對上圖 left 進行排序直到整個鏈結串列排序完成。 整體插入 `result` 順序為 `right` -> `pivot` -> `left`,因為 `list_add` 是從頭部插入所以能知道結果是由小到大排序好的鏈結串列 :::danger 不用列出參考題解,你專注在程式碼的原理即可。 ::: ### 測驗二 Tim sort 為 Tim Peter 提出,結合了合併排序和插入排序的特點 首先識別出資料中已排序的子序列 (這些子序列稱為 run),然後透過不斷合併這些 run 來達成全資料範圍的排序 Timsort 採用一組堆疊 (stack) 來暫存 run,此舉是為了避免在一開始就掃描整個資料範圍來產生 run,從而降低記憶體開銷。過程中,Timsort 運用 merge_collapse 函式來確保堆疊上的 run 長度保持平衡。該函式主要檢查堆疊頂端的 3 個 run 是否滿足以下原則: 1. A 的長度要大於 B 和 C 的長度總和。 2. B 的長度要大於 C 的長度。 :::danger 不用列出參考題解,你專注在程式碼的原理即可。 ::: ## 第二週測驗題 ### 測驗一 透過 preoreder 和 inorder 的二元樹排序,重建原本的二元樹。 參考 [Linux 核心的 hash table 實作](https://hackmd.io/rpIi8FeqQ9eBJ-UTFgb--A) hlist_node 間接指標 ![image](https://hackmd.io/_uploads/BJPyN2T66.png) * 僅有末端指標內容是 NULL。 * `next` 指向相鄰的**節點本身**,而 `pprev` 指向**指著自身節點的指標** ```c void hlist_delete_node(struct hlist_node *n) { struct hlist_node *next = n->next; struct hlist_node **pprev = n->pprev; // Since there is always a pointer which points to node n, // no special case is needed to deal with. *pprev = next; if (next) next->pprev = pprev; } ``` * `*pprev` 是指上個節點的 `next` 指標 * `**pprev` 就是上一個節點本身,所以通過執行 `*pprev = next;` 就代表將 n 的上一個節點的 `next` 指向 n 的下一個節點,實現將 n 從鏈結串列中移除的操作 `hlist_add_head` ```c static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) { if (h->first) h->first->pprev = &n->next; n->next = h->first; n->pprev = &h->first; h->first = n; } ``` 這個函式執行 hash table 中,將 `n` 插入雙向鏈結串列。將 `hlist_node n` 插入於 `hlist_head h` 的前端。 `find` ```c static int find(int num, int size, const struct hlist_head *heads) { struct hlist_node *p; int hash = (num < 0 ? -num : num) % size; hlist_for_each (p, &heads[hash]) { struct order_node *on = list_entry(p, struct order_node, node); if (num == on->val) return on->idx; } return -1; } ``` 將傳入的 `num` 尋找雜湊表中的位置索引。宣告 `hash` 得知該節點放於雜湊表中的哪個槽,並以 `hlist_for_each` 走訪每個節點,尋找節點並回傳索引。 `dfs` ```c static struct TreeNode *dfs(int *preorder, int pre_low, int pre_high, int *inorder, int in_low, int in_high, struct hlist_head *in_heads, int size) { if (in_low > in_high || pre_low > pre_high) return NULL; struct TreeNode *tn = malloc(sizeof(*tn)); tn->val = preorder[pre_low]; int idx = find(preorder[pre_low], size, in_heads); tn->left = dfs(preorder, pre_low + 1, pre_low + (idx - in_low), inorder, in_low, idx - 1, in_heads, size); tn->right = dfs(preorder, pre_high - (in_high - idx - 1), pre_high, inorder, idx + 1, in_high, in_heads, size); return tn; } ``` Preorder 中第一筆資料為根,而 Inorder 中左子樹及右子樹之資料分別在根的左右,所以利用 `find()` 得到根的 index 後,即可切割出左子樹及右子樹,且因為 Preorder 中左子樹之資料必定在右子樹之資料前面,所以先對左子樹遞迴做 `dfs()``,接著對右子樹遞迴做 `dfs()` 便可得到整棵樹 `node_add` ```c static inline void node_add(int val, int idx, int size, struct hlist_head *heads) { struct order_node *on = malloc(sizeof(*on)); on->val = val; on->idx = idx; int hash = (val < 0 ? -val : val) % size; hlist_add_head(&on->node, &head[hash]); } ``` 這裡執行的是建立一個 node 並將其存放至 hash table。建立一個新節點,宣告 `hash` 決定該節點要存放於 hash table 的哪個槽裡,最後使用 `hlist_add_head` 將該節點加入 hash table `buildTree` ```c static struct TreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) { struct hlist_head *in_heads = malloc(inorderSize * sizeof(*in_heads)); for (int i = 0; i < inorderSize; i++) INIT_HLIST_HEAD(&in_heads[i]); for (int i = 0; i < inorderSize; i++) node_add(inorder[i], i, inorderSize, in_heads); return dfs(preorder, 0, preorderSize - 1, inorder, 0, inorderSize - 1, in_heads, inorderSize); } ``` 透過前序和中序來完成