# 2025q1 Homework2 (quiz1+2) contributed by < devarajabc > ## q1-1 此題旨在利用指標的指標實作 `list_insert_before` >The macro `LIST_INSERT_BEFORE()` inserts the new element `elm` before the `element` listelm. ### 解釋程式碼運作原理 #### 為何要使用 pointet to pointer ? 最天真的想法是走訪整個鏈結串列,找到 `before` 的前一位節點 `cure` 並將 `cur->next` 的值改為 `item` 而`item->next` 賦值為 `before`. ```c list_item_t* cur = l->head; while(cur && cur->next != before) cur = cur->next; cur->next = item; item->next = before; ``` 但這樣有什麼問題呢? 因為此設計並未考量到 `l->head` 為 `before` 的情況 > If @before is the head of list, the new item is inserted at the front 上方的程式無法處理此情況,因此要需要新增一個分支 ```diff +if(l->head == before){ + l->head = item; + item->next = before; +} while(cur && cur->next != before) cur = cur->next; cur->next = item; item->next = before; ``` 若使用指標的指標可以減少分支: ```c list_insert_before(list_t* l, list_item_t *item, list_item_t* before){ list_item_t **p; for (p = &l->head; *p != before; p = &(*p)->next) ; *p = item; item = before; } ``` `p` 的意義為「指向某一節點的指標」的指標,而 `*p` 為指向某一節點的指標,利用 deference 的方式即可修改某一結構的指標裡的內容。 若將「指標」比喻為一個記載地址的信封,而「指標的指標」就是該信封的位置;藉由「指標的指標」找到「信封」後,修改裡面所記載的地址。 ![image](https://hackmd.io/_uploads/rJmkkhGhyl.png) 以上方程式碼來說,利用 `for loop` 找到「指向 `before` 的指標」,再將該指標的地址紀錄在 `p` 中,接著利用 `*p->item` 將「指向 `before` 的指標」改為 「指向 `item` 的指標」 #### 如何測試? ```c #define my_assert(test, message) \ do { \ if (!(test)) \ return message; \ } while (0) ``` ### 在現有的程式碼基礎上,撰寫合併排序操作 ## q1-2 [你所不知道的 C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory) >A dynamic memory allocator maintatins maintains an area of a process's virtual memory known as the **heap**. >An allocator maintains the heap as a collection of various-size blocks 為何要要這樣呢? 調整 `brk ptr`的大小再 `sw` 不就好了嗎?為何還要 various-size blocks 呢? malloc vs. calloc ### 解釋程式碼運作原理 此題要求完成二元搜尋樹的刪除程式,為了維護二元搜尋樹的結構,刪除節點 `target` 時需考慮以下三種情況: 1. `target` 為單一節點: ```c /* No children: remove the node. */ *node_ptr = NULL; ``` 2. `target` 只有一個子節點: ```c /* If the target node has one child (or none), simply splice it out. */ block_t *child = ((*node_ptr)->l) ? (*node_ptr)->l : (*node_ptr)->r; *node_ptr = child; ``` 3. `target` 有兩個子節點: 此時要先找到 `target` 的 predecessor ,即左子樹中的最大節點 >A "predecessor" of a node refers to the node that comes immediately before it in an inorder traversal. ```c block_t **pred_ptr = &(*node_ptr)->l; while ((*pred_ptr)->r) pred_ptr = &(*pred_ptr)->r; ``` 接著要再分成兩種情況: 1. predecessor 即為 `target` 的左子節點: ```graphviz digraph RBT { graph[ordering="out"] node [shape=circle, fixedsize=true, style=filled,width=.5] target -> {35 80} 35 [fillcolor=red] 80 -> {70 90} } ``` ```c /* If the predecessor is the immediate left child. */ if (*pred_ptr == (*node_ptr)->l) { block_t *old_right = (*node_ptr)->r; *node_ptr = *pred_ptr; /* Replace target with its left child. */ (*node_ptr)->r = old_right; /* Attach the original right subtree. */ } ``` 此時可以直接以 `pred_ptr` 取代 `taget` 原先的位置 ```graphviz digraph RBT { graph[ordering="out"] node [shape=circle, fixedsize=true, style=filled,width=.5] 35 -> {80} 35 [fillcolor=red] 80 -> {70 90} } ``` 2. predecessor 為左子樹中的的最大值 ```graphviz digraph RBT { graph[ordering="out"] node [shape=circle, fixedsize=true, style=filled,width=.5] target -> {33 50} 33 -> {20 48} 48 [fillcolor=red] } ``` ```c else { /* The predecessor is deeper in the left subtree. */ block_t *old_left = (*node_ptr)->l; block_t *old_right = (*node_ptr)->r; block_t *pred_node = *pred_ptr; /* Remove the predecessor from its original location. */ remove_free_tree(&old_left, *pred_ptr); /* Replace the target node with the predecessor. */ *node_ptr = pred_node; (*node_ptr)->l = old_left; (*node_ptr)->r = old_right; ``` ```graphviz digraph RBT { graph[ordering="out"] node [shape=circle, fixedsize=true, style=filled,width=.5] 48 -> {33 50} 33 -> {20} 48 [fillcolor=red] } ``` ## q1-3 題目說明介紹了非遞迴 (non-recursive; iterative) 的快速排序法,利用 `begin[i]`, `end[i]` 分別用來存放單一排序分割的起始點和終點(how),i 為分割編號,堆疊中的片段(鏈結串列)會被依序取出,取出的片段會依據其長度進行不同的操作: - 片段長度為 `1`: 將該節點加入 `result` - 片段長度不為 `1`: 將取出的片段切分成三段: `left`(小於 pivot), pivot, `right`(大於 pivot) 並依序放進堆疊 題目的要求是利用 Linux 核心風格 List API 的簡化標頭檔 list.h 來改寫快速排序程式碼。(有真的加快嗎?「加快」跟修改 API 有關係嗎?) 節點的結構做出以下調整 ```diff typedef struct __node { - struct __node *left, *right; - struct __node *next; + struct list_head list; long value; } node_t; ``` 因應結構的挑整,需要增加以下輔助函式: - `list_tail`: 回傳鏈結串列的最後一個節點的指標,即最後一個節點 - `list_length`: 回傳鏈結串列的節點數 - `rebuild_list_link`: 走訪鏈結串列並修正每一個節點的 `prev` 指標,將其指向該節點的前一個節點以符合雙相鏈結串列的定義。 ```c 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; } ``` ### 解釋程式碼運作原理 改寫後的程式碼和原先的邏輯一致,差別在於將節點加入不同鏈結串列的方式: 改寫後的版本反而沒有用 list.h 的 API ,在排序的過程中把雙向鏈結串列當作單向鏈結串列處理:直接修改節點中,指標 `next` 的內容,並在最後用 `rebuild_list_link` 修正每個節點的 `prev` 指標。 ```c while (p) { struct list_head *n = p; p = p->next; int n_value = list_entry(n, node_t, list)->value; if (n_value > value) { n->next = right; right = n; } else { n->next = left; left = n; } } ``` 為何要將 `list_add` 拆成兩個步驟呢? >是因為在排序結束之前,`prev` 的數值尚未確定,沒必要每次加入節點都要調整 `prev` 。 TODO: perf ### 研讀〈A comparative study of linked list sorting algorithms〉 ## q2-1 使用 Linux 核心風格 List API 的簡化標頭檔 list.h 來實作遞迴版本的快速排序程式 在每次遞迴呼中,取出鏈結陣列的第一個節點當作 `pivot` ```c pivot = list_first_entry(head, struct listitem, list); list_del(&pivot->list); ``` 接著走訪剩餘的節點並依據節點的值放將其存入 `list_less` 或 `list_greater` ```c 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); } ``` 此時鏈結陣列已被分成三個部分:`list_less`, `pivot`, `list_greater` 接著將分割好的鏈結串列 `list_less` 和 `list_greater` 放入遞迴呼叫中 ```c list_quicksort(&list_less); list_quicksort(&list_greater); ``` 最後將排序好的 `list_less`, `list_greater` 和 `pivot` 重新加入鏈結串列 `head` 中 ```c list_add(&pivot->list, head); list_splice(&list_less, head); list_splice_tail(&list_greater, head); ``` ### 若將 `list_move_tail` 換為 `list_move`,為何會無法滿足 stable sorting 根據 list.h 中對 `list_move` 的描述 >Move a list node to the beginning of the list 節點會往 linked-list 的 `prev` 方向加入,如此一來便會改變節點原先的順序 ## q2-2 題目要求開發整數的開平方根運算,此任務分成兩個部分 1. 藉由遞迴呼叫以計算 count leading zero (clz) 2. 實作整數開平方根 ### 解釋程式碼(1) 每次呼叫 `clz2()` 函式會將目前關注的位元(bits)「切成一半」,以檢查 leading zero 的數量。 ***Recursive case*** 讓我們來觀察「切一半」的過程,以 32 位元的 `x` 為例: 獲得 `x_upper` 的內容可以透過 shift right :`x >> 16` 而 `x_lower` 則需要使用 bit mask:`x & 0xFFFF` 接著 `x_upper` 和 `x_lower` 分別作為下一次遞迴呼叫的輸入: 如此壹來可以整理出規律: ```c static const int mask[] = {0, 8, 12, 14}; uint32_t upper = (x >> (16 >> c)); uint32_t lower = (x & (0xFFFF >> mask[c])); ``` 其中 `c` 為 「切」第幾刀,`mask` 的內容為 $16-2^n$, $n$ 為欲取得之位數。 接著依據 upper 的數值判斷下一次遞迴呼叫應該處理哪一部分,以累計 leading zero 的數量。 若 upper 等於 0,代表 leading zero 至少有 `16 >> c`個零 若 upper 不等於 0,則下一次遞迴呼叫應繼續檢查 upper 部分。 ```c return upper ? clz2(upper, c + 1) : (16 >> (c)) + clz2(lower, c + 1); ``` ***Base case*** 遞迴呼叫 `clz2()` 函式,直到僅剩 2 位元(bits)需要檢查 leading zero,然後回傳結果。 僅剩下 2 位元,代表 32 已經被「切」了 4 次,即 `c == 3` ```c if (c == 3) return upper ? magic[upper] : 2 + magic[lower]; ``` 陣列 `magic` 的內容為索引值的 count leading zero: `0` -> `00` -> 兩位 `1` -> `01` -> 一位 `2` -> `10` -> 零位 `3` -> `11` -> 零為 ```c static const int magic[] = {2, 1, 0, 0}; ``` 如果 `upper` 為 true 有三種可能: `10`, `11` 或 `01` ,透過 `mask` 可以得到 leadind zero 的數量。 ```c if (c == 3) return upper ? magic[upper] : 2 + magic[lower]; ``` 如果 `upper` 為零則 leading zero 至少有兩位(+2),再根據 `lower` 的值獲得 leading zero 的數量。 ### 解釋程式碼(2) ## q2-3 ### 解釋程式碼運作原理,應提供測試程式碼 題意是給定一個陣列 `nums` 和一個目標值 `target`,求找到 `nums` 的 2 個元素相加會等於 `target` 的索引值。題目確保必為單一解,且回傳索引的順序沒差異。 例如給定輸入 `nums` = [2, 7, 11, 15], `numsSize` = 4, `target` = 9,相加變成 9 的元素僅有 2 及 7,因此回傳這二個元素的索引值 `returnSize` = [0, 1] 把當前數字 ```c int *twoSum(int *nums, int numsSize, int target, int *returnSize) { map_t *map = map_init(10); *returnSize = 0; int *ret = malloc(sizeof(int) * 2); if (!ret) goto bail; for (int i = 0; i < numsSize; i++) { int *p = map_get(map, target - nums[i]); if (p) { /* found */ ret[0] = i, ret[1] = *p; *returnSize = 2; break; } p = malloc(sizeof(int)); *p = i; map_add(map, nums[i], p); } bail: map_deinit(map); return ret; } ``` 程式首先利用 `map_init` 初始化一個大小為?的 hashtable ```c typedef struct { int bits; struct hlist_head *ht; } map_t; ``` 為何不直接使用 `struct hlist_head` 陣列(e.g. `struct hlist_head Array[100]`)? `bits` 是什麼呢? ```c MAP_HASH_SIZE(map->bits) ``` `map_add` ```c void map_add(map_t *map, int key, void *data) { struct hash_key *kn = find_key(map, key); if (kn) return; kn = malloc(sizeof(struct hash_key)); kn->key = key, kn->data = data; struct hlist_head *h = &map->ht[hash(key, BBBB)]; struct hlist_node *n = &kn->node, *first = h->first; n->next = first; if (first) CCCC = &n->next; h->first = n; DDDD = &h->first; } ```