# 2024q1 Homework2 (quiz1+2) contributed by < `v0103` > ## 第一周測驗題 ### 測驗`1` ```c node_t *list_tail(node_t **left) { while ((*left) && (*left)->next) left = &(AAAA); return *left; } ``` 因為這裡使用的是單向鏈結串列,因此需要確保傳入的指標在該函式執行完依舊指向串列的頭,參考[你所不知道的 C 語言: linked list 和非連續記憶體](/YA7RMqd6RE2UL8OmmjU9PQ)提到的 indirect pointer,該函式用 `node_t **left`,而非 `node_t *left`。由於 `left` 是指向 `list` 的指標,`*left` 是 `list` ,`*left->next` 則是 `node1`,`left = &(*left->next)` 就會將 `left` 更新為 `node1`,經過while迴圈不斷往後找就可以找到串列的尾。 ```mermaid graph LR subgraph linked list list==>node1==>node2 end subgraph pointer to pointer left==>list end ``` ```c int list_length(node_t **left) { int n = 0; while (*left) { ++n; left = &(BBBB); } return n; } ``` `list_length` 函式要計算串列總長,跟上面的 `list_tail` 一樣,使用 while 迴圈不斷的找下一個 `node` ,解題思路跟上面一樣,所以 `BBBB` 會是 `*left->next`。 ### `quick_sort` 原方法 [ Optimized QuickSort — C Implementation (Non-Recursive)](https://alienryderflex.com/quicksort/) 首先選定最左邊的數字為 `pivot`,分別使用 `L` 由左往右移動,`R` 由右往左移動,在 LR 移動過程會把小於 `pivot` 的數字放到左側,大於的則放到右側,最後 LR 相遇後再把 `pivot` 放到該點,以此完成排序。再來就是對 `pivot` 右側做排序,右側排序完了再對 `pivot` 左側做排序。 有了原方法的排序概念接下來看看題目 `quick_sort` 和原方法有何異同。 ```c node_t *L = begin[i], *R = end[i]; ``` 這裡一樣使用 `begin` 和 `end` 作為堆疊,去儲存尚未排列好的序列。 ```graphviz digraph { node [shape=box]; rankdir = LR; 4 -> 1 -> 3 -> 5 -> 2 -> 7; node [shape=plaintext, fontsize=15]; 7 -> NULL; node [fontcolor=green]; L ->4; rank=same {L 4}; R ->7; rank=same {R 7}; } ``` ```c node_t *pivot = L; value = pivot->value; node_t *p = pivot->next; pivot->next = NULL; ``` 一樣選擇 `L` 作為 `pivot`,`value` 則是 `pivot` 的值。這邊多了一個 `node_t *p` 指向 `pivot->next` 目前還不知道其作用。 ```graphviz digraph { node [shape=box]; rankdir = LR; 4 -> 1 -> 3 -> 5 -> 2 -> 7; node [shape=plaintext, fontsize=15]; 7 -> NULL; node [fontcolor=red]; pivot -> 4; rank=same {pivot 4 }; node [fontcolor=red]; p -> 1; rank=same {p 1 }; node [fontcolor=green]; L ->4; rank=same {L 4}; R ->7; rank=same {R 7}; } ``` ```c while (p) { node_t *n = p; p = CCCC; list_add(n->value > value ? &right : &left, n); } ``` 可以看到這是一個 while 迴圈,運行到 `p==NULL` 才會結束,每次會用新指標 `n` 來存取 `p` 所指向的節點,`n->value > value ? &right : &left` 這邊判斷該次迴圈所指到的節點是否大於 `pivot` ,大於則會執行 `list_add(&right, n)` 將該節點放到 `right` 這個串列,反之,則放到`left` 串列。看起來這個迴圈的作用就是走訪串列,並將小於 `pivot` 的節點放左側,大於 `pivot` 的節點放右側。因此可以判斷出 `CCCC` 為 `p->next`。 `n` 指向 `p` 的位置後 `p` 指向 `p->next`。 ```graphviz digraph { node [shape=box]; rankdir = LR; 4 -> 1 -> 3 -> 5 -> 2 -> 7; node [shape=plaintext, fontsize=15]; node [fontcolor=red]; pivot -> 4; // rank=same {pivot 4 }; node [fontcolor=red]; p -> 3; rank=same {p 3 }; node [fontcolor=red]; n -> 1; rank=same {n 1 }; node [fontcolor=green]; L ->4; rank=same {L 4}; R ->7; rank=same {R 7}; } ``` `n->value` < `pivot->value`,所以 `n` 要放到 `left` 串列。 ```graphviz digraph { node [shape=box]; rankdir = LR; 1 ; node [shape=plaintext, fontsize=15]; node [fontcolor=black]; left -> 1; } ``` 下一輪迴圈 `n` 指到 3,因此將 3 也放到 `left`。 ```graphviz digraph { node [shape=box]; rankdir = LR; 3 -> 5 -> 2 -> 7; 4 ; node [shape=plaintext, fontsize=15]; node [fontcolor=red]; pivot -> 4; node [fontcolor=red]; p -> 5; rank=same {p 5 }; node [fontcolor=red]; n -> 3; rank=same {n 3 }; node [fontcolor=green]; L ->4; rank=same {L 4 }; R ->7; rank=same {R 7}; } ``` 最後會將原串列拆成三組串列。 ```graphviz digraph { node [shape=box]; rankdir = LR; 1 -> 3 -> 2; node [shape=plaintext, fontsize=15]; node [fontcolor=black]; left -> 1; node [shape=box]; rankdir = LR; 5 -> 7; node [shape=plaintext, fontsize=15]; node [fontcolor=black]; right -> 5; node [shape=box]; rankdir = LR; 4; node [shape=plaintext, fontsize=15]; node [fontcolor=red]; pivot -> 4; } ``` ```c 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; ``` 有了 `left` `right`兩個串列,再來要將他們存起來。`begin` `end` 會做為下一輪迴圈的 `L` `R`,因此如果 `begin[i]` 是 `left` 可以得知 `DDDD` 是 `list_tail(&left)`,同理 `EEEE` 是 `list_tail(&right)`。最後 `i += 2` 所以跟原方法一樣是先排序右側。 :::warning TODO : 用list API改寫,提出可避免最差狀況的快速排序實作 ::: ### 測驗`2` 首先在閱讀題目時講到 minrun 的選擇策略時,有個令我疑惑的點。 :::info 顯然,選擇 minrun 的策略是 Timsort 的關鍵,其中一個實務上的方法是,取資料長度 N 的前 6 位,若剩餘位中任何一位被設置,則加 1。這主要是為了在處理隨機數列時,使得 N 能夠被 minrun 切分成 2 的冪的 run,以便在合併時達到最佳平衡。 ::: 其中,**取資料長度 N 的前 6 位**我一直看不懂是甚麼意思,google 後才知道是**將資料長度 N 轉換為二進位表示,並取其前 6 位**的意思。 以題目的例子 N = 2112為例,2112 換成二進位是 100001000000 ,取其前六位也就是 100001 = 33,剩餘的六位都是 0,因此選擇 33 作為 minrun。 --- #### 運行原理 我解這題的方法是先大略理解了 Timsort 的原理,然後按照前後的結構來處理問題,先填補空缺的部分,最後再完全理解每個函式的作用。 首先,我看到了 `timsort` 函式,首先將原本的雙向鏈結串列轉換為單向的,然後使用 `find_run` 函式將原始串列劃分為多個 run。在這個拆分的過程中,同時使用 `merge_collapse` 函式來確保堆疊上的 run 長度保持平衡。接著,使用 `merge_force_collapse` 函式確保 run 的數量少於 3。如果 run 的數量剩餘 2,則執行 `merge_final` 函式進行最後的合併。同時,裡面包含一個 `build_prev_link` 函式,將原本的單向鏈結串列轉換為雙向的。如果 run 的數量剩餘 1,則直接執行 `build_prev_link` 函式。 我覺得一個有趣的地方是,在 `find_run` 函式中,將 run 的長度存儲在 `head->next->prev` 中。這樣做的好處是,在進行合併時可以直接讀取兩個 run 的長度,而不需要額外記錄或者重新掃描一次。 #### 解題思路 :::info `AAAA` `BBBB` `CCCC` 都在 `merge` 函式。 ::: 在 `merge` 函式中 `tail` 負責指向串列的尾部,讓下一次迴圈比較完的較小值可以知道要接在哪,所以 `tail` 初始值應該指向 `head`,`AAAA` 為 `&head`。 ```c struct list_head *head; struct list_head **tail = AAAA; ``` 在這段程式碼中,當 `a` 小於 `b` 時,在第一輪迴圈中,會將 `*tail`,也就是 `head`,設定為 `a`。接著,為了確保下一輪迴圈中較小的值能夠接在 `a` 後面,我們需要更改 `tail` 的指向位置。因此,`BBBB` 為 `&a->next`。 ```c if (cmp(priv, a, b) <= 0) { *tail = a; tail = BBBB; a = a->next; if (!a) { *tail = b; break; } } ``` 同理,`CCCC` 為 `&b->next`。 ```c else { *tail = b; tail = CCCC; b = b->next; if (!b) { *tail = a; break; } } ``` 前面有提到`build_prev_link` 函式的作用是將原本的單向鏈結串列轉換為雙向的。根據註解可以得知這兩行的作用是執行最後一步,讓串列的頭尾相接,因此 `DDDD` 為 `tail->next`, `EEEE` 為 `head->prev`。 ```c /* The final links to make a circular doubly-linked list */ DDDD = head; EEEE = tail; ``` :::warning TODO : 提出改進方案,整合進lab0-c ::: --- ## 第二周測驗題 ### 測驗`1` #### 運行原理 #### 解題思路 ### 測驗`2` ### 測驗`3` #### 運行原理 要找出第 N 個設定的位元,要先能找出第 1 個設定的位元,所以看到 `__ffs` 函式,使用多個 mask 逐步找出 `word` 中第 1 個設定的位元。我挑其中一個來講解。 下面條件若成立,表示 `word` 第 1 個設定的位元不在後 16 位,所以 `num` 計數要加 16 並且把 `word` 右移 16 位,檢查更高位元。 ```c if ((word & 0xffff) == 0) { num += 16; word >>= 16; } ``` 知道怎麼找第 1 個設定的位元,要找到第 N 個也就沒問題了。 `fns` 函式使用 while 迴圈,找到 `word` 第 1 個設定的位元後,再使用 `__clear_bit` 清除該位元,下一輪迴圈找到的就會是第 2 個設定的位元,以此循環便能找到第 N 個。 ```c static inline unsigned long fns(unsigned long word, unsigned int n) { while (word) { unsigned int bit = __ffs(word); if (n-- == 0) return bit; __clear_bit(bit, &word); } return BITS_PER_LONG; } ``` #### 解題思路 前面運行原理舉的例子是檢查後 16 位,mask 是 `0xffff`,所以這個檢查後 32 位的 mask `AAAA` 是 `0xffffffff`。 ```c #if BITS_PER_LONG == 64 if ((word & AAAA) == 0) { num += 32; word >>= 32; } #endif ``` 從 `fns` 函式可以知道,`nr` 是第 1 個設定的位元的位置,`BIT_MASK` 的作用是把 `nr` 的位元設為 1 其他設為 0,所以要將 `p` 的第 `nr` 個位元位元清除,`BBBB` 會是 `~mask`。 ```c static inline void __clear_bit(unsigned long nr, volatile unsigned long *addr) { unsigned long mask = BIT_MASK(nr); unsigned long *p = ((unsigned long *) addr) + BIT_WORD(nr); *p &= BBBB; } ``` 如果 `size` > `BITS_PER_LONG` 時,會執行此巨集。 ```c #define FIND_NTH_BIT(FETCH, size, num) \ ({ \ unsigned long sz = (size), nr = (num), idx, w, tmp; \ \ for (idx = 0; (idx + 1) * BITS_PER_LONG <= sz; idx++) { \ if (idx * BITS_PER_LONG + nr >= sz) \ goto out; \ \ tmp = (FETCH); \ w = hweight_long(tmp); \ if (w > nr) \ goto found; \ \ nr -= w; \ } \ \ if (sz CCCC BITS_PER_LONG) \ tmp = (FETCH) & BITMAP_LAST_WORD_MASK(sz); \ found: \ sz = min(idx * BITS_PER_LONG + fns(tmp, nr), sz); \ out: \ sz; \ }) ```