contributed by < `jerry7961` > ## 第一周測驗題 ### 測驗一 這是一個非遞迴的快速排序演算法,用來對鏈結串列進行排序: 1. 首先計算出原始鏈結串列的長度 n,並初始化一些必要的資料結構,將原始鏈結串列 push 進 stack 中。 2. 進入 while 迴圈,每次從stack頂端 pop 出一對節點指標( begin[i] 和 end[i] ),代表一個子鏈結串列。 3. 如果 begin[i] 和 end[i] 不同,代表子鏈結串列至少有兩個節點: * 選擇 begin[i] 所指向的節點作為 pivot 。 * 走訪子鏈結串列,將小於 pivot 的節點插入 left 鏈結串列,大於 pivot 的節點插入 right 鏈結串列。 * 將 left 、 pivot 以及 right 三個部分的開始和結束節點指標依序 push 進 stack 中,等待進一步排序。 5. 如果 begin[i] 和 end[i] 相同,代表子鏈結串列只有一個節點或為空串列。使用 list_add 函數將 begin[i] 指向的節點(如果存在)插入result鏈結串列中。 6. 重複步驟 2 ,直到 stack 為空 7. 最終result鏈結串列就是排序後的結果,將其賦值給原始鏈結串列。 以包含五個節點 (4、2、5、10、1) 的鏈結串列為例: 未進入 while 迴圈前, stack 中只有原始鏈結串列(如下)。 ![linked_list](https://hackmd.io/_uploads/HJ70CNoa6.png) 在第一輪 while 結束後, stack 中會有三個子鏈結串列(如下)。 ![子2](https://hackmd.io/_uploads/Bk3uzroTa.png) ![linked_list](https://hackmd.io/_uploads/B1SSXBiaT.png) ![image](https://hackmd.io/_uploads/r1EkMHsTa.png) ```c void quick_sort(node_t **list) { int n = list_length(list); int value; int i = 0; int max_level = 2 * n; node_t *begin[max_level], *end[max_level]; node_t *result = NULL, *left = NULL, *right = NULL; begin[0] = *list; end[0] = list_tail(list); while (i >= 0) { node_t *L = begin[i], *R = end[i]; if (L != R) { node_t *pivot = L; value = pivot->value; node_t *p = pivot->next; pivot->next = NULL; while (p) { node_t *n = p; p = p->next; // CCCC list_add(n->value > value ? &right : &left, n); } begin[i] = left; end[i] = list_tail(&left); // DDDD begin[i + 1] = pivot; end[i + 1] = pivot; begin[i + 2] = right; end[i + 2] = list_tail(&right); // EEEE left = right = NULL; i += 2; } else { if (L) list_add(&result, L); i--; } } *list = result; } ``` ### 測驗二 Timsort 主要改進了 Merge Sort 的以下幾個方面: * 降低 Cache miss 次數: Timsort 在尋找、分割和合併 runs (已排序的子序列)時,儘量讓要排序的節點剛才存取過,還在記憶體階層上層時,就進行合併,避免不必要的資料遷移,降低 cache miss 。 * 降低記憶體開銷:傳統 Merge Sort 在合併時需額外配置與待合併的二個 runs 相同大小的記憶體空間,而 Timsort 只需配置較短的 run 的空間大小。 * 降低比較次數:利用資料的部分有序性,減少比較操作。 主要函式: * `merge()` :合併兩個已排序的 runs 為一個新 run 。 - `AAAA`: 應該填入 `&head` ,初始化 `tail` 指向 `head` ,以建構新的鏈結串列。 - `BBBB` : 應該填入 `&a->next` , 從 `a` 鏈結串列取出一個節點後,需要將 `tail` 指向該節點的下一個節點。 - `CCCC` : 應該填入 `&b->next`,從 `b` 鏈結串列取出一個節點後,需要將 `tail` 指向該節點的下一個節點。 * `build_prev_link()` :將一個單向鏈結串列轉換為雙向環狀鏈結串列。 - `DDDD` : 應該填入 `tail->next`,將最後一個節點的 `next` 指標設置為 `head`,這樣鏈結串列就形成了環,從尾部指向頭部。 - `EEEE` : 應該填入 `head->prev`,將頭部節點的 `prev` 指標設置為 `tail`,這樣鏈結串列就完成了雙向鏈結,從頭部指向尾部。 * `timsort()` : ```c stk_size = 0; struct list_head *list = head->next, *tp = NULL; if (head == head->prev) return; /* Convert to a null-terminated singly-linked list. */ head->prev->next = NULL; ``` - 首先初始化 `stk_size` 為 0 ,用來記錄堆疊中 `run` 的數量。 - 然後將 `list` 指向鏈結串列的第一個節點, `tp` 初始化為 `NULL`。 - 如果鏈結串列為空,直接返回。 - 將原來的雙向環狀鏈結串列轉換為單向鏈結串列,方便後續操作。<br><br> ```c do { /* Find next run */ struct pair result = find_run(priv, list, cmp); result.head->prev = tp; tp = result.head; list = result.next; stk_size++; tp = merge_collapse(priv, cmp, tp); } while (list); ``` - 這個 loop 用於找出鏈結串列中的所有 run,並將它們暫存在一個堆疊中。 在每次循環中: 1. 調用 `find_run` 找出當前位置開始的一個 run ,並返回 run 的頭部和尾部節點。 2. 將新找到的這個 run 的頭部節點的 `prev` 指針指向前一個 run 的頭部節點 `tp`。 3. 將 `tp` 更新為新 run 的頭部節點。 4. 將 `list` 指向新 run 的下一個位置,準備下一次循環。 5. `stk_size` 累加 1,表示堆疊中 run 的數量增加了 1。 6. 調用 `merge_collapse` 嘗試將堆疊中的相鄰 run 進行合併,確保堆疊上的 run 長度保持平衡。 直到 `list` 指向 `NULL`,表示鏈結串列中的所有 run 都已經找出並加入堆疊中。<br><br> ```c tp = merge_force_collapse(priv, cmp, tp); ``` - 合併所有 run 。<br><br> ```c struct list_head *stk0 = tp, *stk1 = stk0->prev; while (stk1 && stk1->prev) stk0 = stk0->prev, stk1 = stk1->prev; if (stk_size <= 1) { build_prev_link(head, head, stk0); return; } ``` - 如果堆疊中只剩一個 run ,直接調用 `build_prev_link` 將這個 run 重建成雙向環狀鏈結串列。否則調用 `merge_final` 做最終的合併。 **延伸問題** 定義一個 minrun (最小運行長度),用以表示每個 run 的最小長度,若長度太短,就用二分插入排序,將 run 後面的元素插入到前面的 run 裡面。 ## 第二周測驗題 ### 測驗ㄧ 這個程式根據 preorder 和 inorder 走訪的結果,利用 dfs 方法來重建二元樹。 * 利用 `find` 在 hash table 中找到值為 `preorder[pre_low]` 的節點在 `inorder` 陣列中的位置,並賦值給 `idx`。 ``` int idx = find(preorder[pre_low], size, in_heads);` ``` * 遞迴建構左子樹 ```c tn->left = dfs(preorder, pre_low + 1, pre_low + (idx - in_low), inorder, in_low, idx - 1, in_heads, size); ``` * 遞迴建構右子樹 ```c tn->right = dfs(preorder, pre_high - (in_high - idx - 1), pre_high, inorder, idx + 1, in_high, in_heads, size); ``` ### 測驗二 `hlist_del` 用來刪除節點。 ```c void hlist_del(struct hlist_node *n) { struct hlist_node *next = n->next, **pprev = n->pprev; *pprev = next; if (next) next->pprev = pprev; // EEEE } ``` `lRUCacheFree` 用來釋放整個 cache 佔用的空間。 它使用 `list_for_each_safe` 走訪鏈結串列中的所有節點,並利用 `list_del` 來刪除這些節點。 ```c void lRUCacheFree(LRUCache *obj) { struct list_head *pos, *n; list_for_each_safe (pos, n, &obj->dhead) { LRUNode *cache = list_entry(pos, LRUNode, link); // FFFF list_del(&cache->link); // GGGG free(cache); } free(obj); } ```