# 2025q1 Homework2 (quiz1+2) contributed by < [otischung](https://github.com/otischung) > ## [Quiz 1](https://hackmd.io/@sysprog/linux2025-quiz1) ### [測驗 1-1](https://hackmd.io/@sysprog/linux2025-quiz1#%E6%B8%AC%E9%A9%97-1) 補完之程式碼如下所示 ```cpp= typedef struct list_item { int value; struct list_item *next; } list_item_t; typedef struct { struct list_item *head; } list_t; /** * Inserts a new item into the list before a specified item. * * This function traverses the list to locate the position immediately before the item pointed to by @before and inserts @item in that position. * The time complexity is O(n), where n is the number of stepsneeded to reach @before fromthe head of the list. * * Parameters: * @l : Pointer to the list. * @ before : Pointer to the item before which the new item should be inseted. * - If @before is the head of the list, the new item is inserted at the front. * - If @before is NULL, the new item is appended to the end of the list. * - In all other cases, behavior is undefined if @before does not belong to @l. * @item : The new list item to be inserted. */ static inline void list_insert_before(list_t *l, list_item_t *before, list_item_t *item) { list_item_t **p; for (p = &l->head; *p != before; p = &(*p)->next) // AAAA, BBBB, CCCC ; *p = item; item->next = before; // DDDD } ``` 由於 `l->head` 與 `list_item_t *before` 都是 `struct list_item *`,因此我們只需要紀錄這些指標的位址,透過 dereference 即可操作該記憶體內容。 因此,我們的操作步驟如下: 1. 從頭開始,鏈結串列是由 `l->head` 所紀錄,其型態是 `list_item_t *`,故其位址為 `&l->head`,其型態是 `list_item_t **`。 2. 搜尋 `*p` 是否等於目標 `before`,若相同則下一步,若不同則將 `*p` 的下一個 `(*p)->next` 的位址紀錄起來 `&(*p)->next`。 3. 將 `*p` 所指之處放入 `item`,並將 `item->next` 記成 `before`。 我們分析以下三種情況 1. 插入在鏈結串列之頭 這種情況的話,`p = &l->head`,因此,執行插入的時候,`*p = l->head = item`,再把 `item->next` 紀錄成 `before`,也就是之前的頭。 2. 插入在鏈結串列之中間部份 3. 插入在鏈結串列之尾 這種情況的話,`before = NULL`,所以最後 `*p = NULL`,`**p` 指向最後一個元素的位址,因此將最後一個元素的 `next`,也就是 `*p`,指向 `item`,並將 `item` 的下一個指向 `NULL`,而此時 `before = NULL`,因此不違反原先的設定。 由此證明,以上程式碼具有一般性,不須做其他額外處理。 ### [測驗 1-2](https://hackmd.io/@sysprog/linux2025-quiz1#%E6%B8%AC%E9%A9%97-2) 補完之程式碼如下所示 ```cpp void remove_free_tree(block_t **root, block_t *target) { /* Locate the pointer to the target node in the tree. */ block_t **node_ptr = find_free_tree(root, target); /* If the target node has two children, we need to find a replacement. */ if ((*node_ptr)->l && (*node_ptr)->r) { /* Find the in-order predecessor: * This is the rightmost node in the left subtree. */ block_t **pred_ptr = &(*node_ptr)->l; while ((*pred_ptr)->r) // EEEE pred_ptr = &(*pred_ptr)->r; // FFFF ... } ... } ``` 要找到最右邊的 node,就從左邊一路掃到右邊就可以了。 ### [測驗 1-3](https://hackmd.io/@sysprog/linux2025-quiz1#%E6%B8%AC%E9%A9%97-3) 補完之程式碼如下所示 ```cpp static void rebuild_list_link(struct list_head *head) { if (!head) return; struct list_head *node, *prev; prev = head; node = head->next; while (node) { node->prev = prev; prev = node; node = node->next; } prev->next = head; head->prev = prev; // GGGG } void quick_sort(struct list_head *head) { int n = list_length(list); int value; int i = 0; int max_level = 2 * n; struct list_head *begin[max_level]; struct list_head *result = NULL, *left = NULL, *right = NULL; begin[0] = list->next; list->prev->next = NULL; while (i >= 0) { struct list_head *L = begin[i], *R = list_tail(begin[i]); if (L != R) { struct list_head *pivot = L; value = list_entry(pivot, node_t, list)->value // HHHH struct list_head *p = pivot->next; pivot->next = NULL; /* break the list */ while (p) { struct list_head *n = p; p = p->next; int n_value = list_entry(n, node_t, list)->value; // IIII if (n_value > value) { n->next = right; right = n; } else { n->next = left; left = n; } } begin[i] = left; begin[i + 1] = pivot; // JJJJ begin[i + 2] = right; // KKKK left = right = NULL; i += 2; } else { if (L) { L->next = result; result = L; } i--; } } list->next = result; rebuild_list_link(list); } ``` 首先,`rebuild_list_link()` 只在 `quick_sort()` 最後一行執行,而觀察 `quick_sort()` 只有處理 `next`,並未處理 `prev`,因此判定此 `rebuild_list_link()` 是為了補上 `prev`。 因此,我們定義 `prev` 為第一個元素,`node` 為第二個元素,透過雙向環狀 linked-list 依序處理,即可推斷 GGGG 為何。 HHHH 與 IIII 皆是利用 `container_of` macro 取得 `node_t` 的位址,再取得該 node 的值。 JJJJ 與 KKKK 皆為圖例所示,依序為 `left`, `pivot`, 與 `right`。 ## [Quiz 2](https://hackmd.io/@sysprog/linux2025-quiz2) ### [測驗 2-1](https://hackmd.io/@sysprog/linux2025-quiz2#%E6%B8%AC%E9%A9%97-1) 補完之程式碼如下所示 ```cpp static void list_quicksort(struct list_head *head) { struct list_head list_less, list_greater; struct listitem *pivot; struct listitem *item = NULL, *is = NULL; if (list_empty(head) || list_is_singular(head)) return; INIT_LIST_HEAD(&list_less); INIT_LIST_HEAD(&list_greater); pivot = list_first_entry(head, struct listitem, list); // AAAA list_del(&pivot->list); // BBBB list_for_each_entry_safe (item, is, head, list) { if (cmpint(&item->i, &pivot->i) < 0) list_move_tail(&item->list, &list_less); else list_move_tail(&item->list, &list_greater); // CCCC } list_quicksort(&list_less); list_quicksort(&list_greater); list_add(&pivot->list, head); // DDDD list_splice(&list_less, head); // EEEE list_splice_tail(&list_greater, head); // FFFF } ``` 其實對於 AAAA 來說,選擇 pivot 可以是任意的,不過在選項中,可以使用 `container_of` 得出「一個」 node 的 macro,只有 `list_first_entry`,那麼他就是唯一解。 與[測驗 1-3](#測驗-1-3) 不同的是,這裡使用遞迴的方式實作,額外準備兩個 list 做遞迴。 取出 pivot 後,將 pivot 移出 list,之後將每個 node 與 pivot 作比較,分別移入兩個 lists。 至此,原本的 head 已經沒有東西了。 遞迴解完之後,先把 pivot 放回 head,再把左邊 list 插入頭段,最後把右側 list 插入尾段。 ### [測驗 2-2](https://hackmd.io/@sysprog/linux2025-quiz2#%E6%B8%AC%E9%A9%97-2) 補完之程式碼如下所示 ```cpp static const int mask[] = {0, 8, 12, 14}; // {0, 8, 12, GGGG} static const int magic[] = {2, 1, 0, 0}; // {HHHH, 1, 0, IIII} unsigned clz2(uint32_t x, int c) { if (!x && !c) return 32; uint32_t upper = (x >> (16 >> c)); uint32_t lower = (x & (0xFFFF >> mask[c])); if (c == 3) // JJJJ return upper ? magic[upper] : 2 + magic[lower]; // KKKK return upper ? clz2(upper, c + 1) : (16 >> (c)) + clz2(lower, c + 1); // LLLL } #define clz32(x) clz2(x, 0) ``` 這個實作是使用遞迴呼叫求解 counting leading zero,直到剩下 2 bits 為止。 每次都將目前的位元平分成 upper 與 lower | c | upper/lower 位元數 | | - | ----------------- | | 0 | 16 | | 1 | 8 | | 2 | 4 | | 3 | 2 | - 當 upper 非 0,則繼續尋找 upper,忽略 lower。 - 當 upper 為 0,則繼續尋找 lower,將目前 upper 的位元數加進來。 當 `c == 3` 時,則 upper 與 lower 只剩下 `0b00`, `0b01`, `0b10`, `0b11` 三種可能,而這些數字的開頭分別有 `{2, 1, 0, 0}` 個 0,因此,當 `c == 3` 時, - 當 upper 非 0,則返回 upper 有幾個 0。 - 當 upper 為 0,則返回 lower 有幾個 0,加上 upper 的部分,有 2 個 0。 接下來,我們補完開平方根的程式碼 ```cpp uint64_t sqrti(uint64_t x) { uint64_t m, y = 0; if (x <= 1) return x; int total_bits = 64; /* clz64(x) returns the count of leading zeros in x. * (total_bits - 1 - clz64(x)) gives the index of the highest set bit. * Rounding that index down to an even number ensures our starting m is a * power of 4. */ int shift = (total_bits - 1 - clz64(x)) & ~1; // MMMM m = 1ULL << shift; while (m) { uint64_t b = y + m; y >>= 1; // NNNN if (x >= b) { x -= b; y += m; } m >>= 2; // PPPP } return y; } ``` 我們的目標是要找到一個最大的數字 $y$ 使得 $y^2 \le x$。 我們知道對於所有正整數與 0 可以以二進位表示,即 $$ y = a_n 2^n + a_{n-1} 2^{n-1} + ... + a_1 2^1 + a_0 2^0 $$ 對於所有 $n$ 為正整數或 0,$a_n$ 為 0 或 1。 因此,對於所有 $y$,找到最大的 $n$ 使得 $(2^n)^2 \le x$,由此開始進行二分搜尋法,繼續尋找 $n - 1$, $n - 2$, ..., $1$, $0$,即可得到 $$ y^2 = (a_n 2^n + a_{n-1} 2^{n-1} + ... + a_1 2^1 + a_0 2^0)^2 \le x $$ 由於我們總是從 $(2^n)^2 = 4^n$ 開始搜尋,因此 `shift` 必定是偶數,而又因為我們的起始項不超過 $x$,因此我們總是向下取偶數,這等同於將 LSB mask 成 0,即 `& ~1`。 接下來,我們看一下 $x = 170$ 的例子,170 在二進位表示成 `0xAA`,因此 `shift == 6`,此時我們有 $m = (2^3)^2$, $y = 0$, $b = y + m = (2^3)^2$, $$ 64 = (2^3)^2 \le 170 $$ 由於 $64 \le 170$,所以 $y = 2^3$。 接下來,我們搜尋 $(2^3 + 2^2)^2 \le 170$ 是否正確 $$ 144 = (2^3 + 2^2)^2 \le 170 $$ $$ 196 = (2^3 + 2^2 + 2^1)^2 > 170 $$ $$ 169 = (2^3 + 2^2 + 2^0)^2 \le 170 $$ 我們將等式做展開 $$ \begin{aligned} 144 = (2^3 + 2^2)^2 &\le 170 \\ 144 = (2^3)^2 + (2(2^3) + (2^2)) (2^2) &\le 170 \\ 144 - (2^3)^2 = 2(2^3)(2^2) + (2^2)^2 &\le 170 - (2^3)^2 \\ 80 = 2(2^3)(2^2) + (2^2)^2 &\le 106 \\ 196 = (2^3 + 2^2 + 2^1)^2 &> 170 \\ 196 = (2^3 + 2^2)^2 + 2(2^3 + 2^2)2^1 + (2^1)^2 &> 170 \\ 169 = (2^3 + 2^2 + 2^0)^2 &\le 170 \end{aligned} $$