# 2024q1 Homework2 (quiz1+2) contributed by < [ollieni](https://github.com/ollieni) > ## 第一周測驗題 ### 測驗一 這題是想透過非遞迴的方式實作 quick sort。 根據參考資料: [ Optimized QuickSort — C Implementation (Non-Recursive)](https://alienryderflex.com/quicksort/) ,作者提到想改進維基百科的做法,其中原因如下 : 1. 避免函式呼叫,因為進出函式是耗時的。 2. 不使用堆疊儲存資料,避免遞迴函數的性能慢且可能引起堆疊溢位。 3. 交換變數只在每輪中傳遞一個項目,降低效能成本。 4. 每輪中元素只移動到列表中的新位置一次,減少不必要的移動。 5. 當元素在 pivots 目的地正確一側,避免不必要的移動。 6. 避免在每輪中有50%的機會將列表中的元素與自身交換,降低性能成本。 這裡發現了一個問題,作者提到不使用堆疊儲存資料,但老師的測驗中提到"這份程式碼嘗試用堆疊模擬原本的遞迴呼叫"。 目前還沒想出這之間的關係。 本題的鏈結串列結構體如下: ```clike typedef struct __node { struct __node *left, *right; struct __node *next; long value; } node_t; ``` ```graphviz digraph structs { node[shape=record]; struct0 [label="{<node>*left|<node>*right}|<node>*next|<data>value",color=black] rankdir=LR; node[shape=box]; } ``` ## 重新回答題目 要我們填空的程式碼如下: 首先是`list_tail()`,依據函式名稱可以推論出此函式的功能是找到鍊結串列的尾端。 ```c node_t *list_tail(node_t **left) { while ((*left) && (*left)->next) left = &(AAAA); return *left; } ``` 可以看到 `while` 迴圈的停止條件是 `*left` 和 `(*left)->next`,其中之一為 `NULL` ,因此當 `*left` 指向尾端時,此迴圈就會停止,接著函式就會回傳 `*left` ,也就是尾端。 因此若是要找到尾端, `left = &(AAAA);` ,應為不斷往下一格移動。 所以 `AAAA` 的答案是 `(*left)->next`。 --- 根據 `list_length()` 的函式名稱,我們可以得知此程式的目的是回傳鍊結串列的長度。 ```c int list_length(node_t **left) { int n = 0; while (*left) { ++n; left = &(BBBB); } return n; } ``` 依據現有的程式碼可以得知,此函式會在 `*left` 不為 `NULL` 時,持續將 `n` 加一,當 `*left` 為 `NULL` 時停止(也就達到尾端)。 因此可以推論出 `BBBB` 會讓 `*left` 持續往尾端移動。 答案為 `(*left)->next`。 --- 以下程式碼為 quick sort 實作的一部份。 ```c . . . while (p) { node_t *n = p; p = CCCC; list_add(n->value > value ? &right : &left, n); } begin[i] = left; end[i] = DDDD; begin[i + 1] = pivot; end[i + 1] = pivot; begin[i + 2] = right; end[i + 2] = EEEE; left = right = NULL; i += 2; . . . ``` CCCC 在一個 `while` 迴圈中,這個迴圈會根據 n->value 是否大於 value,將節點 n 添加到名為 right 或 left 的鍊結串列中,此迴圈目的是在將鍊結串列依據大小分成 pivot 左右兩邊的串列。 因此需要走過每一個節點, CCCC 為 `p->next`。 begin 和 end 分別是在記錄左右兩邊串列的頭尾,因此 DDDD 應為 begin[i] 的尾端,我們可以用 `list_tail()` 來找尾端, DDDD 的答案就會是 `list_tail(left)`。 EEEE 會是 begin[i+2] 的尾端,答案會是 `list_tail(right)`。 ### 測驗二 請補完 Timsort 程式碼,使其運作符合預期。 Timsort 為合併排序和插入排序的組合算法。 將數列分成數個 run (比較小的數列)以 insertion sort 將 run 裡的數字排序,再將所有 run 用 merge sort 的方式合併在一起。 分 run 時,有幾點需注意 1. run 的長度不宜超過 64,因為 insertion sort 的效果會變差 2. run 最好是二的次方, merge 合併效率才會比較好 ## 第二周測驗題 ### 測驗一 LeetCode [105 Construct Binary Tree from Preorder and Inorder Traversal](https://leetcode.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/description/) 用前序 (preorder) 和中序 (inorder) 重構二元樹。 **Preorder Traversal 前序遍歷**: 用Depth-first Search走訪整棵樹,順序是:根、左子樹、右子樹。根排在最前面。 **Inorder Traversal 中序遍歷**: 也是採用Depth-first Search,走訪順序是:左子樹、根、右子樹。根排在中間。 #### **Key point** : preorder 和 inorder ,可以用根的位置,用 Divide-and-Conquer 的概念,建立唯一二元樹。 在 preorder 之中,最左端一定是根節點。 inorder 中,根的兩側是左子樹和右子樹。因為子樹也是樹,可以用相同方式遞迴下去,求出整棵樹的架構。 **hlist_node** ```graphviz digraph structs { node[shape=record]; struct0 [label="{<node1>*next|<node2>*pprev}",color=black] rankdir=LR; node[shape=box]; } ``` **Tree_Node 結構**: ```graphviz digraph structs { node[shape=record]; struct0 [label="{<node>*left|<node>*right}|<data>val",color=black] rankdir=LR; node[shape=box]; } ``` **order_node 結構**: ```graphviz digraph structs { node[shape=record]; struct0 [label="<node>*node|<data>val|<data>idx",color=black] rankdir=LR; node[shape=box]; } ``` 這是一個經典的問題,接下來我會以我的理解,解釋這題的程式。 函式解釋: **find** 找出 value = num 的節點。 這個函數的目的是在 hash 串列中查找特定數值 num 的索引。 使用 hash 值計算,確保索引落在 hash 表範圍內。 透過 `hlist_for_each` 循環遍歷 hash 鏈表,並使用 `container_of` 將節點轉換為 order_node 結構。 若找到相應的數值,則返回對應的索引;否則返回 -1。 **dfs** 使用遞迴的方式做深度優先搜尋。 用於構建二元樹。 接收前序和中序走訪的區間,以及相應的 hash 串列等參數。 透過遞迴方式,根據前序遍歷的順序,建立二元樹的每個節點。 利用 `find` 函數找到中序走訪中該節點的索引,並分別遞迴建立左右子樹。 **buildTree** 使用 inorder 和 preorder 來建立一個唯一的樹。 這個函數是用於建立二元樹。 一開始配置 hash 串列的內存,並初始化每個 hash 串列。 使用前序遍歷的數組建立 hash 串列,同時調用 `dfs` 遞迴函式構建整個二元樹。 返回指向根節點的指標。 ### 測驗二 程式目的 : 實作 Least Recently Used (LRU) Cache 在清除快取中的資料時,將上一次使用時間離現在最遠的資料清除。 **LRUNode 結構**: ```graphviz digraph G { rankdir = LR; node [shape=record] subgraph cluster_1 { node [shape=record]; label="LRUNode"; hn [label="{ **pprev | *next }"]; lh [label="{ *prev | *next }"]; node1[label="{ key | value }"]; } } ``` **LRUCache 結構** ```graphviz digraph LRUCache { rankdir = LR; node [shape=record] subgraph cluster_1 { node [shape=record]; label="LRUCache"; node1[label="{ capacity | count }"]; lh [label="{ *prev | *next }"]; hn [label="{ hhead }"]; } hln1 [label="{ **pprev | *next }"]; hln2 [label="{ **pprev | *next }"]; hn->hln1->hln2 } ``` 函式解釋 : **lRUCacheCreate** 創建一個空的 lRU cache ,並根據 `capacity` 配置記憶體。 **lRUCacheFree** 將 lRUCache 清空,並釋放記憶體。 使用 `list_for_each_safe` 走訪全部節點,並將結點記憶體釋放。 **lRUCacheGet** 根據 key ,從快取中取出相對應的值。 將 key 除餘 capacity ,得到 key 的 hash 值,再用 hlist_for_each走訪,若在LRUCache->hhead[hash] 的每個節點中,有找到 key 的值,將該節點放到 `obj->dhead`,也就是最前面。 **lRUCachePut** 將 key-value 放入快取中。 先查詢 key 是否存在,若存在就將其放到最前面的位置。若不存在,先檢查 cache 是否滿了,若沒滿則直接將 key-value 插入。若 cache 已滿,將最後的 key-value 刪除,並將要插入的 key-value 放到最前面。 ### 測驗三 程式目的 : 找到第 n 個為1的 bit 。 **ffs** : 此函式功能為找出第一個是 1 的 bit。 其運作原理和 find_leading_zero 很相似。 ```c ... ///如果最低的16位都为0,将 num 加上16,然后将 word 右移16位 if ((word & 0xffff) == 0) { num += 16; word >>= 16; } ///如果最低的8位都为0,将 num 加上8,然后将 word 右移8位。 if ((word & 0xff) == 0) { num += 8; word >>= 8; } ///如果最低的4位都为0,将 num 加上4,然后将 word 右移4位。 if ((word & 0xf) == 0) { num += 4; word >>= 4; } ///如果最低的2位都为0,将 num 加上2,然后将 word 右移2位。 if ((word & 0x3) == 0) { num += 2; word >>= 2; } ///如果最低的1位为0,将 num 加上1 if ((word & 0x1) == 0) num += 1; return num; ... ``` **clear_bit** : 將特定區塊的 bits 刪除。 用 `BIT_MASK(nr)` 產生一個在 nr 位置是1,其他位置皆為0的 mask ,可用於清除特定 bit。 找到包含要刪除 bit 的 word 記憶體位置的 pointer。 對pointer 用 `~mask` 做 bit-wise and 將 bit 刪除。 **fns** : 用 __ffs 去找第一個是1的 bit,用 __clear_bit 刪除,重複 n 次,即可找到第 n 個是 1 的 bit。