# 2025q1 Homework2 (quiz1+2) contributed by <`Dennis40816`> ## [第一週](https://hackmd.io/@sysprog/linux2025-quiz1) ### 測驗 1 #### Solution - `AAAA` : `&l->head` - `BBBB` : `before` - `CCCC` : `&(*p)->next` - `DDDD` : `item->next` #### Description 本題中使用++單向鏈結串列++。 先從 `list_insert_before` 的解釋著手: ```bash /** * 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 steps needed to * reach @before from the head of the list. * * Parameters: * @l : Pointer to the list. * @before : Pointer to the item before which the new item should be inserted. * - 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); ``` 得知 `before` 指向的,是++當插入新節點 `item` 後, `item` 的下一個元素++。 當 `before` 是頭部節點,`item` 將被插入在最前方,如果是 `NULL` 則被插入在最尾端。 ```c= list_item_t **p; for (p = AAAA; *p != BBBB; p = CCCC) ; *p = item; DDDD = before; ``` `list_insert_before` 做的事情是: 1. 找到 `before` 目前所在的位置 (第 2 - 3 行) 2. 將 `before` 用 `item` 替換 (第 4 行) 3. 將 `item` 的下個元素指向 `before` (第 5 行) 因此,`AAAA` - `DDDD` 的內容推論如下: - 尋找 `before` 應當從頭部節點開始,`struct list_t` 中僅有成員 `head`,故 `AAAA` 為 `&l->head`。 - 停止尋找條件為 `*p` 指向的節點與 `before` 相同時,故 `BBBB` 為 `before`。應注意,`list_insert_before` 的註釋提到當 `before` 不存在於 `l` 中時,可能產生未定義行為,推測將因為對尾部節點的下一個,即 `NULL` 解引用導致 core dump。 - 對於每次循環,`p` 應該往下移動,故 `CCCC` 為 `&(*p)->next`。應注意運算子的優先級別,在此為 `->` > `*` > `&`,詳見 [C Operator Precedence](https://en.cppreference.com/w/c/language/operator_precedence),故括弧應僅包含 `*p`。 - 將 `item` 的下一個節點換作 `before`,故 `DDDD` 為 `item->next`。 以 Graphviz 呈現: - 當循環開始時,`*p` 指向 `head` ```graphviz digraph g { rankdir=LR; node [shape=record]; head [label = "<name>head | {<data> 4 | <ref>}"]; node1 [label = "<name>node1 | {<data> 7 | <ref>}"]; node2 [label = "node2 | {<data> 10 | <ref>}"]; target [label = "TARGET | {<data> 3 | <ref>}"]; node3 [label = "node3 | {<data> 1 | <ref>}"]; null [label = "NULL", shape = none]; before [label = "before", shape = none]; dref_p [label = "*p", shape = none]; edge[weight=2]; head:ref:c -> node1:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; node1:ref:c -> node2:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; node2:ref:c -> target:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; target:ref:c -> node3:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; node3:ref:c -> null [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; edge[weight=1]; before -> target:name:n dref_p -> head:name:n } ``` - 當 `*p` 指向的節點為 `before` 時,插入 `item` ```graphviz digraph g { rankdir=LR; node [shape=record]; head [label = "<name>head | {<data> 4 | <ref>}"]; node1 [label = "<name>node1 | {<data> 7 | <ref>}"]; node2 [label = "node2 | {<data> 10 | <ref>}"]; target [label = "TARGET | {<data> 3 | <ref>}"]; node3 [label = "node3 | {<data> 1 | <ref>}"]; item [label = "item | {<data> 5 | <ref>}"]; null [label = "NULL", shape = none]; before [label = "before", shape = none]; p [label = "p", shape = none]; edge[weight=3]; head:ref:c -> node1:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; node1:ref:c -> node2:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; node2:ref:c -> target:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; target:ref:c -> node3:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; node3:ref:c -> null [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; edge[weight=1]; before:se -> target:name p -> node2:ref; } ``` ```graphviz digraph g { rankdir=LR; node [shape=record]; head [label = "<name>head | {<data> 4 | <ref>}"]; node1 [label = "<name>node1 | {<data> 7 | <ref>}"]; node2 [label = "node2 | {<data> 10 | <ref>}"]; target [label = "TARGET | {<data> 3 | <ref>}"]; node3 [label = "node3 | {<data> 1 | <ref>}"]; item [label = "item | {<data> 5 | <ref>}"]; null [label = "NULL", shape = none]; before [label = "before", shape = none]; p [label = "p", shape = none]; edge[weight=3]; head:ref:c -> node1:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; node1:ref:c -> node2:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; node2:ref:c -> item:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; target:ref:c -> node3:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; node3:ref:c -> null [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; edge[weight=1]; before:n -> target:name p -> node2:ref; } ``` - 將 `item->next` 指向 `before` ```graphviz digraph g { rankdir=LR; node [shape=record]; head [label = "<name>head | {<data> 4 | <ref>}"]; node1 [label = "<name>node1 | {<data> 7 | <ref>}"]; node2 [label = "node2 | {<data> 10 | <ref>}"]; target [label = "TARGET | {<data> 3 | <ref>}"]; node3 [label = "node3 | {<data> 1 | <ref>}"]; item [label = "item | {<data> 5 | <ref>}"]; null [label = "NULL", shape = none]; before [label = "before", shape = none]; p [label = "p", shape = none]; edge[weight=3]; head:ref:c -> node1:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; node1:ref:c -> node2:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; node2:ref:c -> item:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; item:ref:c -> target:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; target:ref:c -> node3:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; node3:ref:c -> null [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; edge[weight=1]; before -> target:name p -> node2:ref; } ``` 注意 p 指向的是節點 `next` 的地址,從圖中看便是指向 `next`,而 `next` 再指向下一個節點,可以描述出 `p` 是 indirect pointer。之所以說優雅是因為頭部節點的地址實際存儲在 `&head` 記憶體中,「像是」一個 `next` 的存在,這使的操作得以一致化。 #### 延伸 : 運作原理 測試程式 `test_list` 的流程如下: - 透過函式 `list_reset` 初始化全域串列與節點陣列,將每個節點的值設定為索引值並清除所有鏈結,對於所有測試前都會先執行。 - 測試第一種情況,傳入 `before` 為 `l.head` 以++逆序++方式將陣列中的內容插入到鏈結串列中,並確保測試後的鏈結串列數值是反向排列的。 - 測試第二種情況,傳入 `before` 為 `NULL`,以++順序++方式將陣列中的內容插入到鏈結串列中,並確保數值是順序的。 - 測試第三種情況,同上一條。 #### 延伸 : 合併排序 可以透過以下方式進行遞迴式單向鏈結串列排序: - 透過快慢指標將鏈結串列分成兩段,第一段的頭部由 `head` 指標指向,第二段頭部則是 `slow->next` 指向的節點,注意要透過另一個指標(e.g., `mid` 儲存)。 - 切斷第一部分和第二部分的鏈結( `slow->next == NULL` ) - 將 `head` 和 `mid` 傳入 `merge_sort_list` (遞迴呼叫) - 當 `head` 和 `mid` 已經排序完並返回後,呼叫 `merge_lists` 合併兩者為單個鏈結串列。 合併中,當兩個鏈結串列非空時,取出值較小者,放入要返回的鏈結串列尾部,並將被取出值的鏈結串列往下一個移動 實作如下: ```c /** * @brief Merges two sorted linked lists into one sorted list. * @param a pointer to the first sorted list. * @param b pointer to the second sorted list. * @return pointer to the head of the merged sorted list. */ static list_item_t *merge_lists(list_item_t *a, list_item_t *b) { list_item_t dummy; // dummy node to simplify merge operation list_item_t *tail = &dummy; // tail pointer for the merged list dummy.next = NULL; while (a && b) { if (a->value <= b->value) { tail->next = a; a = a->next; } else { tail->next = b; b = b->next; } tail = tail->next; } tail->next = (a) ? a : b; return dummy.next; } /** * @brief Recursively sorts the linked list using merge sort. * @param head pointer to the head of the list. * @return pointer to the head of the sorted list. */ static list_item_t *merge_sort_list(list_item_t *head) { if (head == NULL || head->next == NULL) return head; // Find the middle of the list using slow and fast pointers list_item_t *slow = head; list_item_t *fast = head->next; while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } // Split the list into two halves list_item_t *mid = slow->next; slow->next = NULL; // Recursively sort each half list_item_t *left = merge_sort_list(head); list_item_t *right = merge_sort_list(mid); // Merge the two sorted halves return merge_lists(left, right); } /** * @brief Sorts the entire linked list in ascending order. * @param l pointer to the list structure. */ void list_sort(list_t *l) { if (l == NULL) return; l->head = merge_sort_list(l->head); } ``` 藉由以下修改使 `merge_lists` 消除 if - else: ```c static list_item_t *merge_lists(list_item_t *a, list_item_t *b) { list_item_t dummy; list_item_t **last = &dummy.next; dummy.next = NULL; while (a && b) { list_item_t **min = (a->value <= b->value) ? &a : &b; *last = *min; last = &(*last)->next; *min = (*min)->next; } *last = a ? a : b; return dummy.next; } ``` ### 測驗 2 #### Solution - `EEEE` : `(*pred_ptr)->r` - `FFFF` : `&(*pred_ptr)->r` #### Description 當要快速解題時,著重在以下內容: ```c /* Find the in-order predecessor: * This is the rightmost node in the left subtree. */ block_t **pred_ptr = &(*node_ptr)->l; while (EEEE) pred_ptr = FFFF; ``` 註釋提到要找左子樹中最右下角的節點,起始位置已經設定成左子樹,因此我們只需要確認每個節點是否具有右子節點。如果有則更新成該節點並繼續,反之結束。 - `EEEE` 要確認是否具有右子節點(非 `NULL` ),故用 `(*pred_ptr)->r` - `FFFF` 是更新節點,`pred_ptr` 是 indirect pointer 故為 `&(*pred_ptr)->r`。 - ++注意 `*` 和 `->` 的優先級++。 #### 延伸 : 分析 & 解讀 先探討試題中的內容。 首先看到 `block_t` 定義: ```c typedef struct block { size_t size; struct block *l, *r; } block_t; ``` 其中 `size` 用於紀錄該 block 的大小 (bytes)。 接著有兩個未提供的實作的函式,我們稍後探討這部分的實作: ```c block_t **find_free_tree(block_t **root, block_t *target); block_t *find_predecessor_free_tree(block_t **root, block_t *node); ``` 關鍵函式 `remove_free_tree`: ```c void remove_free_tree(block_t **root, block_t *target) ``` 裡面用到幾個關鍵變數,如下表: | Variable | Type | Description | |:----------:|:-----------:| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `target` | `block_t*` | A pointer to the target node, i.e., the node that is intended to be removed from the tree. | | `pred_ptr` | `block_t**` | An indirect pointer to the field in the left subtree of the target node, used to locate its in-order predecessor (i.e. the rightmost node in the left subtree). | | `node_ptr` | `block_t**` | An indirect pointer pointing to the field (in the parent or root) that holds the target node's address, enabling direct modification of the tree structure during removal. | `remove_free_tree` 可以分成三個階段: - 在樹中找到 `target` ,並用其親代的左或右成員地址表達,並紀錄在 `node_ptr` 中。 - 根據 `target` 的左節點和右節點,在移除前更新親代的子節點,避免直接移除破壞樹結構。 - 移除 `target` 的子節點,避免被刪除的節點還能存取樹中的其他節點。 針對第二點,可以分為三種情況: 1. `target` 同時有左和右子節點。 2. `target` 僅具有單邊節點。 3. `target` 不具有任何子節點。 第二種情況只要將 `node_ptr` 指向唯一的子節點即可: ```c *node_ptr = child; ``` 第三種情況則更新成 `NULL` : ```c *node_ptr = NULL; ``` > [!Note] > 我認為第二種情況可以和第三種合併,因為第三種情況不論取左右子節點,都是 `NULL`。 針對第一種情況,要先尋找到++目標節點的中序前驅節點++。中序前驅中,中序是走訪方式(左 ➞ 中 ➞ 右);前驅則指最靠近目標節點,但比目標節點小的節點。因此中序前驅可以理解為++前一個++。注意靠近並非路徑靠近,而是++值++靠近,例如此處應為 `size` 接近。 對於二元搜尋樹 (Binary Search Tree, BST) 而言,中序前驅節點位於左子樹最右下角的節點,反之中序後繼則在右子樹最左下角。 當找到中序前驅節點後,分兩種情況: - 左子樹中沒有任何右節點 ( `*pred_ptr == (*node_ptr)->l` ) - 其他 對於++前者++: 1. 紀錄 `target` 右節點 ( `(*node_ptr)->r` )。 - 不用紀錄 `target` 左節點的左子樹原因是,即將用 `target` 左節點替換掉 `target`,其本身的 `l` 成員已有該資訊,缺乏的僅是右側節點的資訊。 1. 將原本指向 `target` 的 `*node_ptr` 改指向中序前驅節點 `*pred_ptr`,注意不是 `node_ptr = pred_ptr` 而是 `*node_ptr = *pred_ptr`,後者才會改變樹結構。 1. 確保沒有顯而易見的環 (指向自身)。 對於++後者++: 1. 紀錄 `target` 左右節點資訊。 1. **用另一個指標保存中繼前驅節點**,用於解決下一步從樹中移除中繼前驅後,其 `l` 成員會被置 `NULL` 導致遺失左節點的問題,用 `pred_node` 紀錄。 1. 將中繼前驅從原位置移除(再次呼叫 `remove_free_tree` ) 1. 變更 `*node_ptr` 指向 `pred_node` ++指向的節點++。注意不可用 `node_ptr = &pred_node` 因為 `pred_node` 是區域變數。 1. 檢查是否有顯而易見的環。 #### 延伸 : 實作 free tree 實作前可以思考以下問題: > 為什麼 `find_free_tree` 需要返回 `block_t**` ? 如果只是要修改 `block_t` 內容,`block_t*` 就足夠了。但若要++更新其親代節點的子節點++,則應返回 `block_t**`,並透過以下方式進行更新: ```c block_t **ptr = find_free_tree(root, target); if (ptr) { // With an indirect pointer, we can directly modify the parent's pointer. // Get size size_t size = (*ptr)->size; // For example, remove target by updating the parent's pointer to point to target->l. *ptr = target->r; } ``` 我們不用再特別尋找親代節點的指標再透過該指標去修改,而是直接返回親代的左子樹或右子樹 field 的地址 ( `&parent->l` or `&parent->r` ),如此便能直接藉由一次解引用更新裡面指向的節點。 `remove_free_tree` 中的使用方法便是如此 ( `*node_ptr = *pred_ptr` ) 。 下圖描述返回值 `block_t**` ( `ptr` ) 所指向的位置。 ```graphviz digraph g { rankdir=TD; node [shape=record]; ranksep=0.3; nodesep=0.4; parent [label="{Parent | Size: 2 | {<l_ref>| <r_ref>}}"]; l_node [label="{LeftChild | Size: 1| {<l_ref> | <r_ref>}}"]; target [label="{Target | Size: 3 | {<l_ref>| <r_ref>}}"]; ptr [label="ptr", shape=none]; // not show node [shape=none]; none1 [label="", width=0.5, height=0, fixedsize=true, margin=0]; none2 [label="", width=0.5, height=0, fixedsize=true, margin=0]; none3 [label="", width=0.5, height=0, fixedsize=true, margin=0]; none4 [label="", width=0.5, height=0, fixedsize=true, margin=0]; edge[weight=2] // Parent's right field points to target. parent:r_ref:c -> target [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; parent:l_ref:c -> l_node [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; l_node:r_ref:c -> none1 [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; l_node:l_ref:c -> none2 [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; target:r_ref:c -> none3 [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; target:l_ref:c -> none4 [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; edge[weight=1] ptr -> parent:r_ref [style=solid, headclip=false, arrowhead=vee]; parent -> ptr [style=invis]; } ``` 我們需要實作以下的函式: - `create_block` - `insert_free_tree` - `remove_free_tree` (同上方實作,移除檢查部分) - `find_free_tree` - `free_free_tree` - `find_predecessor_free_tree` (optional) - `print_free_tree` (optional) 以下是實現方法,運行範例請見 [Compiler Explorer](https://godbolt.org/z/TfsEYrWrE): - `create_block` ```c block_t *create_block(size_t size) { block_t *node = malloc(sizeof(block_t)); if (!node) { perror("malloc"); exit(EXIT_FAILURE); } node->size = size; node->l = node->r = NULL; return node; } ``` - `insert_free_tree` 當找到 `NULL` 處即可插入,以下是迭代寫法: ```c void insert_free_tree(block_t **root, block_t *node) { if (root == NULL || node == NULL) return; while (*root != NULL) { if (node->size < (*root)->size) root = &(*root)->l; else root = &(*root)->r; } *root = node; } ``` 遞迴寫法: ```c void insert_free_tree(block_t **root, block_t *node) { if (root == NULL) return; if (*root == NULL) { *root = node; return; } if (node->size < (*root)->size) { insert_free_tree(&((*root)->l), node); } else { insert_free_tree(&((*root)->r), node); } } ``` - `find_free_tree` 採用迭代走訪子樹,可以善用 BST 的特性,以 size 為判斷條件決定繼續走訪左子節點(小於)還是右子節點(大等於)。當 `*root` 為 `target` 時就返回。 ```c block_t **find_free_tree(block_t **root, block_t *target) { while (root && *root) { if (*root == target) { return root; } if (target->size < (*root)->size) { root = &(*root)->l; } else { root = &(*root)->r; } } return NULL; } ``` 當然也能透過遞迴作法實現: ```c block_t **find_free_tree(block_t **root, block_t *target) { if (root == NULL || *root == NULL) return NULL; if (*root == target) return root; block_t **found = find_free_tree(&(*root)->l, target); if (found != NULL) return found; return find_free_tree(&(*root)->r, target); } ``` - `free_free_tree` ```c void free_free_tree(block_t *root) { if (root == NULL) return; free_free_tree(root->l); free_free_tree(root->r); free(root); } ``` #### 延伸 : 參考 [memory-allocators](https://github.com/mtrebi/memory-allocators) 的效能比較與討論 要使當前程式為真正的記憶體配置器,要針對 `block_t` 結構體進行修改,例如需要紀錄真正可以讀寫的 buffer 地址。 ##### 可改進 - BST 非常依賴 `root`,其決定該樹的平衡狀況,如果 `root` 為極端值,則操作速度會退化,是否能使用其他平衡樹? #### Reference - [charliechiu : linux2025-homework2](https://hackmd.io/@charliechiu/linux2025-homework2) ### 測驗 3 #### Solution - `GGGG` : `head->prev=prev` - `HHHH` : `list_entry(pivot,node_t,list)->value` - `IIII` : `list_entry(n,node_t,list)->value` - `JJJJ` : `pivot` - `KKKK` : `right` #### Description 先看到 `GGGG` 所在的輔助函式: ```c static void rebuild_list_link(struct list_head *head) ``` 只做以下流程: 1. 用 `prev` 紀錄目前 `head`。 2. 找到 `head->next` 用 `node` 紀錄。 3. 進入循環,由於目前是單向鏈結串列,故停止條件為 `node` 不是 `NULL` (走訪到尾)。 4. 循環內透過 `prev`, `node` 等操作,建立沿途每個元素的 `prev` 關係 ( `next` 已經被建立)。 5. 離開循環後,使鏈結串列構成環,即將最後一個元素 `prev` 的 `next` 指向 `head` ,而 `head->prev` 則指向最後一個元素 `prev`。故得 `GGGG` 為 `head->prev=prev`。 接著看到 `HHHH` 的關鍵程式: ```c if (L != R) { struct list_head *pivot = L; value = /* HHHH */ // ... if (n_value > value) { n->next = right; ``` 可知 `value` 是比較的基準值,在快速排序中,我們以 `pivot` 的值作為基準,因此 `value` 是類似 `to_node_t_func(pivot)->value`,這可以透過 `list_entry` 巨集達成。由於 `list_head` 儲存在成員 `list` 中。故 `HHHH` 為 `list_entry(pivot,node_t,list)->value`。 接著移動到 `IIII` : ```c struct list_head *n = p; p = p->next; int n_value = /* IIII */; ``` 我們能夠推斷 `n_value` 實際上就是 `to_node_t(n)->value`,也就是 `list_entry(n,node_t,list)->value` 即 `IIII`。 最後的 `JJJJ` 和 `KKKK` 可以放在一起看。快速排序中,合併後的順序是 `left` ➜ `pivot` ➜ `right`。題目中的作法是從取出第 `begin[i]`,而 `i` 是遞減的 ( `i--` ),也就是實際合併時,會先將 `begin[i+2]` 處理後的放到 `result` 前面,之後是 `begin[i+1]` 處理後的,...。所以 `right` 應該要放置在第一個被拿取的地方,即: ```c begin[i] = left; begin[i + 1] = pivot; /* JJJJ */ begin[i + 2] = right; /* KKKK */ ``` #### 延伸 : 解讀程式 在[〈Optimized QuickSort — C Implementation (Non-Recursive)〉](https://alienryderflex.com/quicksort/)提到的陣列快速排序流程分成作者自己提出的和本來就存在於 Wiki 百科上的,可以用以下文字解釋: **Darel Rex Finley's Quick Sort** - 將 `head` 指向的元素複製到 `pivot` 中,`L` 從++最左側++開始,`R` 從最++右側++開始 ```graphviz digraph G { node [shape=none]; arr [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR> <TD PORT="f0" WIDTH="25">4</TD> <TD PORT="f1" WIDTH="25">5</TD> <TD PORT="f2" WIDTH="25">3</TD> <TD PORT="f3" WIDTH="25">6</TD> <TD PORT="f4" WIDTH="25">2</TD> <TD PORT="f5" WIDTH="25">8</TD> <TD PORT="f6" WIDTH="25">1</TD> <TD PORT="f7" WIDTH="25">7</TD> </TR> </TABLE> >]; // pivot p [label="4", shape=box]; p_text [label="pivot", shape=plaintext]; // labels head_text [label="head", shape=plaintext]; L_text [label="L", shape=plaintext]; R_text [label="R", shape=plaintext]; { rank=same; L_text; R_text } // connections { // force L under arr arr:f0 -> L_text [style=invis] p_text->p; head_text->arr:f0 L_text -> arr:f0 R_text -> arr:f7 } } ``` - `R` 往左側移動直到遇到比 `pivot` 小的值,此處為第七個元素 1,因此將該值++複製到++ `L` 指向的空間 ```graphviz digraph G { node [shape=none]; arr [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR> <TD PORT="f0" WIDTH="25">1</TD> <TD PORT="f1" WIDTH="25">5</TD> <TD PORT="f2" WIDTH="25">3</TD> <TD PORT="f3" WIDTH="25">6</TD> <TD PORT="f4" WIDTH="25">2</TD> <TD PORT="f5" WIDTH="25">8</TD> <TD PORT="f6" WIDTH="25">1</TD> <TD PORT="f7" WIDTH="25">7</TD> </TR> </TABLE> >]; // pivot p [label="4", shape=box]; p_text [label="pivot", shape=plaintext]; // labels head_text [label="head", shape=plaintext]; L_text [label="L", shape=plaintext]; R_text [label="R", shape=plaintext]; four_text [label="4", shape=plaintext] arr:f0:w -> four_text [headlabel="remove "] { rank=same; L_text; R_text } // connections { // force L under arr arr:f0 -> L_text [style=invis] p_text->p; head_text->arr:f0 L_text -> arr:f0 R_text -> arr:f6 } } ``` - `R` 已經置換過,換成 `L` 往右邊移動直到遇到比 `pivot` 大的值,此處為第二個元素 `5`,將該值複製到 `R` 指向的地方 ```graphviz digraph G { node [shape=none]; arr [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR> <TD PORT="f0" WIDTH="25">1</TD> <TD PORT="f1" WIDTH="25">5</TD> <TD PORT="f2" WIDTH="25">3</TD> <TD PORT="f3" WIDTH="25">6</TD> <TD PORT="f4" WIDTH="25">2</TD> <TD PORT="f5" WIDTH="25">8</TD> <TD PORT="f6" WIDTH="25">5</TD> <TD PORT="f7" WIDTH="25">7</TD> </TR> </TABLE> >]; // pivot p [label="4", shape=box]; p_text [label="pivot", shape=plaintext]; // labels head_text [label="head", shape=plaintext]; L_text [label="L", shape=plaintext]; R_text [label="R", shape=plaintext]; one_text [label="1", shape=plaintext] one_text -> arr:f6[dir=back taillabel="remove "] { rank=same; L_text; R_text } // connections { // force L under arr arr:f1 -> L_text [style=invis] p_text->p; head_text->arr:f0 L_text -> arr:f1 R_text -> arr:f6 } } ``` - 繼續往左移動 `R`,最終指到第五個元素 `2` ```graphviz digraph G { node [shape=none]; arr [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR> <TD PORT="f0" WIDTH="25">1</TD> <TD PORT="f1" WIDTH="25">2</TD> <TD PORT="f2" WIDTH="25">3</TD> <TD PORT="f3" WIDTH="25">6</TD> <TD PORT="f4" WIDTH="25">2</TD> <TD PORT="f5" WIDTH="25">8</TD> <TD PORT="f6" WIDTH="25">5</TD> <TD PORT="f7" WIDTH="25">7</TD> </TR> </TABLE> >]; // pivot p [label="4", shape=box]; p_text [label="pivot", shape=plaintext]; // labels head_text [label="head", shape=plaintext]; L_text [label="L", shape=plaintext]; R_text [label="R", shape=plaintext]; five_text [label="5", shape=plaintext] five_text -> arr:f1[dir=back label="remove"] { rank=same; L_text; R_text } // connections { // force L under arr arr:f3 -> L_text [style=invis] arr:f4 -> R_text [style=invis] p_text->p; head_text->arr:f0 L_text -> arr:f1 R_text -> arr:f4 } } ``` - 接下來又換到 `L`,停在第四個元素 `6` ```graphviz digraph G { node [shape=none]; arr [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR> <TD PORT="f0" WIDTH="25">1</TD> <TD PORT="f1" WIDTH="25">2</TD> <TD PORT="f2" WIDTH="25">3</TD> <TD PORT="f3" WIDTH="25">6</TD> <TD PORT="f4" WIDTH="25">6</TD> <TD PORT="f5" WIDTH="25">8</TD> <TD PORT="f6" WIDTH="25">5</TD> <TD PORT="f7" WIDTH="25">7</TD> </TR> </TABLE> >]; // pivot p [label="4", shape=box]; p_text [label="pivot", shape=plaintext]; // labels head_text [label="head", shape=plaintext]; L_text [label="L", shape=plaintext]; R_text [label="R", shape=plaintext]; two_text [label="2", shape=plaintext] two_text -> arr:f4 [dir=back label=" remove"] { rank=same; L_text; R_text } // connections { // force L under arr arr:f3 -> L_text [style=invis] arr:f4 -> R_text[style=invis] p_text->p; head_text->arr:f0 L_text -> arr:f3 R_text -> arr:f4 } } ``` - 此時,換 `R`,但此時 `R` 碰到 `L` 故結束,將 `pivot` 的值複製進 `L` 指向的地方,並更新 `begin` 和 `end` - 第一輪開始前,`begin[i]` 是 `0`、`end[i]` 是 `8` - 第一輪結束後,`begin[i+1]` 是 `L+1 (4)`、`end[i+1]` 是 `end[i] (8)`、`end[i]` 被更新為 `L (3)`,且 `i` 往上遞增 1 為 `1` ```graphviz digraph G { node [shape=none]; arr [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR> <TD PORT="f0" WIDTH="25">1</TD> <TD PORT="f1" WIDTH="25">2</TD> <TD PORT="f2" WIDTH="25">3</TD> <TD PORT="f3" WIDTH="25">4</TD> <TD PORT="f4" WIDTH="25">6</TD> <TD PORT="f5" WIDTH="25">8</TD> <TD PORT="f6" WIDTH="25">5</TD> <TD PORT="f7" WIDTH="25">7</TD> </TR> </TABLE> >]; // pivot p [label="4", shape=box]; p_text [label="pivot", shape=plaintext]; // labels head_text [label="head", shape=plaintext]; L_text [label="L", shape=plaintext]; R_text [label="R", shape=plaintext]; // begin beg [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR> <TD PORT="f0" WIDTH="25">0</TD> <TD PORT="f1" WIDTH="25">4</TD> </TR> </TABLE> >]; beg_text [label="begin", shape=plaintext]; // end end [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR> <TD PORT="f0" WIDTH="25">3</TD> <TD PORT="f1" WIDTH="25">8</TD> </TR> </TABLE> >]; end_text [label="end", shape=plaintext]; beg_text -> beg [style=invis] beg -> end_text [style=invis] end_text -> end [style=invis] // 為了對齊,讓 beg_text 在左邊與 arr 同一行 { rank=same; beg_text; arr } // L、R 排在 arr 下方 { rank=same; L_text; R_text } // connections { arr:f3 -> L_text [style=invis] arr:f3 -> R_text [style=invis] p_text -> p; head_text -> arr:f0 L_text -> arr:f3 R_text -> arr:f3 } } ``` - 繼續從 `i = 1` 開始,L 的新值為指向 index 為 4 的 `6`,`R` 指向 index 為 7 的 `7`,pivot 值改為 `6` ```graphviz digraph G { node [shape=none]; arr [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR> <TD PORT="f0" WIDTH="25">1</TD> <TD PORT="f1" WIDTH="25">2</TD> <TD PORT="f2" WIDTH="25">3</TD> <TD PORT="f3" WIDTH="25">4</TD> <TD PORT="f4" WIDTH="25">6</TD> <TD PORT="f5" WIDTH="25">8</TD> <TD PORT="f6" WIDTH="25">5</TD> <TD PORT="f7" WIDTH="25">7</TD> </TR> </TABLE> >]; // pivot p [label="6", shape=box]; p_text [label="pivot", shape=plaintext]; // labels L_text [label="L", shape=plaintext]; R_text [label="R", shape=plaintext]; { rank=same; L_text; R_text } // connections { // force L under arr arr:f4 -> L_text [style=invis] arr:f7 -> R_text[style=invis] p_text->p; L_text -> arr:f4 R_text -> arr:f7 } } ``` - 接著 `R` 往左移動到第七個元素 `5`,並將該值複製到 `L` 處 ```graphviz digraph G { node [shape=none]; arr [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR> <TD PORT="f0" WIDTH="25">1</TD> <TD PORT="f1" WIDTH="25">2</TD> <TD PORT="f2" WIDTH="25">3</TD> <TD PORT="f3" WIDTH="25">4</TD> <TD PORT="f4" WIDTH="25">5</TD> <TD PORT="f5" WIDTH="25">8</TD> <TD PORT="f6" WIDTH="25">5</TD> <TD PORT="f7" WIDTH="25">7</TD> </TR> </TABLE> >]; // pivot p [label="6", shape=box]; p_text [label="pivot", shape=plaintext]; // labels L_text [label="L", shape=plaintext]; R_text [label="R", shape=plaintext]; six_text [label="6", shape=plaintext] six_text -> arr:f4[dir=back label=" remove"] { rank=same; L_text; R_text } // connections { // force L under arr arr:f4 -> L_text [style=invis] arr:f6 -> R_text[style=invis] p_text->p; L_text -> arr:f4 R_text -> arr:f6 } } ``` - `L` 往右移動到第六個元素 `8` 時,複製到 `R` ```graphviz digraph G { node [shape=none]; arr [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR> <TD PORT="f0" WIDTH="25">1</TD> <TD PORT="f1" WIDTH="25">2</TD> <TD PORT="f2" WIDTH="25">3</TD> <TD PORT="f3" WIDTH="25">4</TD> <TD PORT="f4" WIDTH="25">5</TD> <TD PORT="f5" WIDTH="25">8</TD> <TD PORT="f6" WIDTH="25">8</TD> <TD PORT="f7" WIDTH="25">7</TD> </TR> </TABLE> >]; // pivot p [label="6", shape=box]; p_text [label="pivot", shape=plaintext]; // labels L_text [label="L", shape=plaintext]; R_text [label="R", shape=plaintext]; five_text [label="5", shape=plaintext] five_text -> arr:f5[dir=back label=" remove"] { rank=same; L_text; R_text } // connections { // force L under arr arr:f5 -> L_text [style=invis] arr:f7 -> R_text[style=invis] p_text->p; L_text -> arr:f5 R_text -> arr:f6 } } ``` - 換 `R` 但碰到 `L`,將 `pivot` 複製到 `L` 後結束第二輪 - 第二輪開始前,`begin[i]` 是 `4`、`end[i]` 是 `8` - 第二輪結束後,`begin[i+1]` 是 `L+1 (6)`、`end[i+1]` 是 `end[i] (8)`、`end[i]` 被更新為 `L (5)`,且 `i` 往上遞增 1 為 `2` ```graphviz digraph G { node [shape=none]; arr [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR> <TD PORT="f0" WIDTH="25">1</TD> <TD PORT="f1" WIDTH="25">2</TD> <TD PORT="f2" WIDTH="25">3</TD> <TD PORT="f3" WIDTH="25">4</TD> <TD PORT="f4" WIDTH="25">5</TD> <TD PORT="f5" WIDTH="25">6</TD> <TD PORT="f6" WIDTH="25">8</TD> <TD PORT="f7" WIDTH="25">7</TD> </TR> </TABLE> >]; // pivot p [label="4", shape=box]; p_text [label="pivot", shape=plaintext]; // labels L_text [label="L", shape=plaintext]; R_text [label="R", shape=plaintext]; // begin beg [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR> <TD PORT="f0" WIDTH="25">0</TD> <TD PORT="f1" WIDTH="25">4</TD> <TD PORT="f2" WIDTH="25">6</TD> </TR> </TABLE> >]; beg_text [label="begin", shape=plaintext]; // end end [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR> <TD PORT="f0" WIDTH="25">3</TD> <TD PORT="f1" WIDTH="25">5</TD> <TD PORT="f2" WIDTH="25">8</TD> </TR> </TABLE> >]; end_text [label="end", shape=plaintext]; beg_text -> beg [style=invis] beg -> end_text [style=invis] end_text -> end [style=invis] // 為了對齊,讓 beg_text 在左邊與 arr 同一行 { rank=same; beg_text; arr } // L、R 排在 arr 下方 { rank=same; L_text; R_text } // connections { arr:f5 -> L_text [style=invis] arr:f5 -> R_text [style=invis] p_text -> p; L_text -> arr:f5 R_text -> arr:f5 } } ``` - 第三輪,`L` 指向第七個元素 `8`,`R` 指向第八個元素 `7`。由於 `R` 當前指向的已經小於新的 `pivot` 值 `8`,因此將該值複製到 `L`。 ```graphviz digraph G { node [shape=none]; arr [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR> <TD PORT="f0" WIDTH="25">1</TD> <TD PORT="f1" WIDTH="25">2</TD> <TD PORT="f2" WIDTH="25">3</TD> <TD PORT="f3" WIDTH="25">4</TD> <TD PORT="f4" WIDTH="25">5</TD> <TD PORT="f5" WIDTH="25">6</TD> <TD PORT="f6" WIDTH="25">7</TD> <TD PORT="f7" WIDTH="25">7</TD> </TR> </TABLE> >]; // pivot p [label="8", shape=box]; p_text [label="pivot", shape=plaintext]; // labels L_text [label="L", shape=plaintext]; R_text [label="R", shape=plaintext]; eight_text [label="8", shape=plaintext] eight_text -> arr:f6[dir=back label=" remove"] { rank=same; L_text; R_text } // connections { // force L under arr arr:f6 -> L_text [style=invis] arr:f7 -> R_text[style=invis] p_text->p; L_text -> arr:f6 R_text -> arr:f7 } } ``` - 而此時顯而易見的是,`L` 將會碰到 `R`,所以 `pivot` 的值將被放入最後一個元素中 ```graphviz digraph G { node [shape=none]; arr [label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR> <TD PORT="f0" WIDTH="25">1</TD> <TD PORT="f1" WIDTH="25">2</TD> <TD PORT="f2" WIDTH="25">3</TD> <TD PORT="f3" WIDTH="25">4</TD> <TD PORT="f4" WIDTH="25">5</TD> <TD PORT="f5" WIDTH="25">6</TD> <TD PORT="f6" WIDTH="25">7</TD> <TD PORT="f7" WIDTH="25">8</TD> </TR> </TABLE> >]; // pivot p [label="8", shape=box]; p_text [label="pivot", shape=plaintext]; // labels L_text [label="L", shape=plaintext]; R_text [label="R", shape=plaintext]; eight_text [label="7", shape=plaintext] eight_text -> arr:f7[dir=back label=" remove"] { rank=same; L_text; R_text } // connections { // force L under arr arr:f6 -> L_text [style=invis] arr:f7 -> R_text[style=invis] p_text->p; L_text -> arr:f7 R_text -> arr:f7 } } ``` - 達到該步驟後,剩下的就是更新 `begin`、`end` 並重走訪先前排序好的子陣列。剩下的過程中不會有任何除了 `L` 的值被替換(被 `pivot` 替換),直到 `i < 0` 分析完陣列的作法後,結合原本題目的說明來看看鏈結串列的快速排序吧! 題目中一開始舉的例子省略中間步驟,導致有點難快速理解,其流程如下: - `L` 一開始在 `array[0]`,`R` 在 `array[5]`,當 `R` 往左邊走一格就碰到了小於 `pivot` 的值,因此發生 `array[0] = array[4]`。 - 換 `L` 移動,到 `array[3]` 發現大於 `pivot` 的值,所以 `array[3] = array[4]` - `L` 和 `R` 碰到,把 `pivot` 值給 `array[3]` 接著看到非 Linux Kernel list 的 `quick_sort` 實現,有幾點可以注意: - `max_level` 設定成 `2n` 是確保在極端情況下,不發生數組越界。 - 透過 `list_tail` 獲得最後一個元素 (相當於 `R = end[i] - 1` ) - `L` 從 `pivot` 的下一個開始(和陣列不一樣) - 鏈結串列不使用 `L` 和 `R` 的複製,因為交換成本高,而是拆分成兩個子鏈結串列,小於等於的放 `left`,大於的放 `right` - 只有 `begin` 沒有 `end`,放置到 `begin` 的順序按照索引順序為 `left`、 `pivot`、`right` 這些部分也會在用 list 實作的 `quick_sort` 中。 實作中有++三處遮罩++: - `CCCC` : `p->next` - `DDDD` : `list_tail(&left)` (先放左邊) - `EEEE` : `list_tail(&right)` (再放右邊) 最後看回使用 list 實作的 `quick_sort`,其測試的流程是透過 `shuffle` 先打亂有 100000 的鏈結串列,之後使用 `quick_sort` 排序並透過 `list_is_ordered` 驗證。 - `shuffle` 使用 `rand` 實作,這能透過先前實作 PRNG 進行改進。 - `list_tail` 在這邊沒有使用 `prev` 的作法是因為 `quick_sort` 過程中,會只維護單邊 ( `netx` 方向) 的鏈結,到最後才從新建立另一邊鏈結。 #### 延伸 : 雙向鏈結的排序演算法考量 ##### 研讀 [〈A comparative study of linked list sorting algorithms〉](https://dl.acm.org/doi/pdf/10.1145/225890.225893) ---------------------------------------------------------- ## [第二週](https://hackmd.io/@sysprog/linux2025-quiz2) ### 測驗 1 #### Solution - `AAAA` : `list_first_entry` - `BBBB` : `list_del` - `CCCC` : `list_move_tail` - `DDDD` : `list_add` - `EEEE` : `list_splice` - `FFFF` : `list_splice_tail` #### Description 該測驗延續上週的快速排序,差別是更改了 API 讓使用者能夠傳入自定義的比較函式指標。 觀察核心程式碼,在開始時檢查邊緣情況確認是否能提早返回。接著進行 `list_head` 的初始化,再往下碰到了 `AAAA`。快速排序通常取頭部節點作為 `pivot`,因此 `AAAA` 對應的操作是取出成員 `list` 為 `ad` 的節點地址操作,如此後續才能執行如 `pivot->list` 等操作。故 `AAAA` 為 `container_of`。而由於 `pivot` 要移出鏈結串列外,因此 `BBBB` 為 `list_del`。 接著來到比較大小的部分,比 `pivot` 小的放到 `list_less` 中,其它放到 `list_greater`,應注意題目要求符合 stable sorting,意謂要符合原鏈結串列順序。因此插入時應插入在++尾端++。故 `CCCC` 和另一分支都使用 `list_move_tail`。 分區完成後,透過遞迴方式呼叫 `list_quicksort`,並於遞迴結束後,按序將 `list_less`、`pivot`、`list_greater`,題目中首先操作的是 `pivot`,因此可以使用 `list_add` 將其先加入 `head` 中,同時也是 `head` 的唯一節點。接著透過 `EEEE` 和 `FFFF` 分別操作 `list_less`、`list_greater`,如前述 `EEEE` 要讓 `list_less` 在 `head` 唯一的節點前,因此是從頭部插入,相反 `FFFF` 則要從尾部插入。故 `EEEE` 為 `list_splice` 而 `FFFF` 為 `list_splice_tail`。 在此,我們記錄 `list_splice` 和 `list_cut_position` 的功能。前者用於拼接鏈結串列,注意拼接後的哨兵節點 (例如 `list_less`) 是++不能直接承接其他節點的++!需要進行初始化後才能繼續使用。或者使用 `list_splice_init` 來簡化該流程。 `list_cut_position` 是輔助使用者從 `head` 的第一個節點到指定節點(含)移動到另一個鏈結串列中,剩下的留在原鏈結串列中。 #### 延伸 : 改進 random_shuffle_array 在 `random_shuffle_array` 註釋提到: ``` /* WARNING biased shuffling */ ``` 所謂 biased shuffling 是指取樣時,由於使用取餘操作而結果非 0 時,會導致小於取餘結果的值有更高的機會被取樣。儘管機率很小,依然會降低隨機性。 可以藉由移除多出的尾端,使每種取餘結果出現的次數是相同的。 ```c static uint16_t uniform_rand(uint16_t n) { uint32_t range = (uint32_t)UINT16_MAX + 1; uint32_t bound = range - (range % n); /* upper bound */ uint16_t x; do { x = get_unsigned16(); } while ((uint32_t)x >= bound); /* reject if in the “extra” tail */ return x % n; } ``` ```c static inline void random_shuffle_array(uint16_t *ops, uint16_t len) { for (uint16_t i = 0; i < len; i++) { /* unbiased shuffling */ uint16_t j = uniform_rand(i + 1); operations[i] = operations[j]; operations[j] = i; } } ``` 但要注意,目前的作法是拒絕取樣(rejection sampling),極端情況下可能會經歷多論迴圈一直重新取樣,這種非常數的運行時間有被惡意偵測去猜測(只要猜測尾端)的可能性! #### 延伸 : list_move_tail 換為 list_move 造成的影響 若將 `list_move_tail` 置換成 `list_move` 會使得原先在後的節點移動到新鏈結串列時被插入到頭部,最終得到的鏈結串列就會和原本的順序顛倒,也會使排序過程變成非穩定的。 穩定排序對於多重排序非常重要,例如 PCI 匯流排上的裝置被掃描時,會依照 bus number、device number、function number 等進行排序,若在排序第三種鍵時,發現兩個目標值相同而改變了兩者順序,可能導致依照第一、二種鍵的排序被破壞! 因此,應 `list_move_tail` 不能被 `list_move` 替換。 ### 測驗 2 #### Solution - `GGGG`:`14` - `HHHH`:`2` - `IIII`:`0` - `JJJJ`:`3` - `KKKK`:`2` - `LLLL`:`1` - `MMMM`:`~1` - `NNNN`: - `PPPP`: `2` #### Description 首先觀察 `clz2`: ```c= static const int mask[] = {0, 8, 12, GGGG}; static const int magic[] = {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 == JJJJ) return upper ? magic[upper] : KKKK + magic[lower]; return upper ? clz2(upper, c + 1) : (16 >> (c)) + clz2(lower, c + LLLL); } ``` 對照題目給的 step 1 - 3,其中: - step 1 : line 9 - 10 - step 2 : line 13 - step 3 : line 11 - 12 step 1 中,根據 `#define clz32(x) clz2(x, 0)` 可知道 `c` 的初始值是 0。只要上半部非 0,`c` 每次遞增 1。因此 upper 的右移位數將會是 16、8、4、2 (根據 step 3,會在 2 bits 的情況時結束遞迴)。 對於 lower,遮罩 `0xFFFF >> mask[c]` 中,低位為 1 的數量與當前的 upper 位數 (也就是 upper 右移位數) 相同。也就是低為是 1 的數量也是 16、8、4、2。`mask` 陣列和上述數量的總和固定會是 16,所以 `mask` 如下: ```c static const int mask[] = {0, 8, 12, 14}; // GGGG == 14 ``` 接著讓我們跳到 step 3,對於停止條件是 2 bits,也就是 `c` 等於 3 時。故 `JJJJ` 為 3。line 12 告訴我們,當 upper 非 0,返回 `magic[upper]`,否則返回 `KKKK + magic[lower]`。變相告訴我們: - `magic[1:3]` 跟==高半段==有關 ( `upper` 為 0 會走另一個分支)。 - `magic[0]` 只跟==低半段==有關。 `magic` 陣列是針對 `0b00`、`0b01`、`0b10`、`0b11` 而言,各提供了幾個 leading zero,因此 `magic` 陣列為 ```c static const int magic[] = {2, 1, 0, 0}; // HHHH == 2, IIII == 0 ``` 對於 `KKKK`,當 2 bits 下,`upper` 是 0,代表 upper 固定提供兩個 leading zero,故 `KKKK` 為 2。 最終回到 step 2,重點在處理完 `upper` 後處理低半段的地方: ```c (16 >> (c)) + clz2(lower, c + LLLL) ``` 其中: - `16 >> c` : 代表當前 upper 全是 0 會提供的 leading zero 數量。例如 `c` 是 0 也就是第一輪 `upper` 就都是 0,就會提供 `16 >> 0` 個 leading zero。 - `clz2(lower, c + LLLL)` : 代表繼續處理下半部,每次遞迴 `c` 都遞增,故 `LLLL` 為 1。 探討完 `clz2` 和 `clz32`,可以繼續擴展至 `clz64`,後者依照上半段是否全 0 分為兩種情況 - 上半段全 0,使用 `clz32` 處理下半段 + 32 (上半段全 0)。 - 上半段不是全 0,使用 `clz32` 處理上半段。 至此,輔助函式探討完畢,進入 `sqrti`,參數是 `x`。根據註解,我們要計算 `m`,該值由 `1 << shift` 表達,且 `m` 要是 4 的冪。而 `shift` 為 `fls(x) & MMMM`。首先,`fls` 代表 find last set,即最高為是 1 的位元索引。該部分可以由 `clz64` 改寫,即 line 2: ```c= int total_bits = 64; int fls = total_bits - 1 - clz64(x); /* here */ int shift = fls & MMMM; int m = 1 << shift; ``` `m` 要求是 4 的冪代表了 `shift` 必須偶數,如此 `m` 就會是 $2^{shift} = 4^{shift/2}$,確保其為 4 的冪。進而推斷 bitwise and `MMMM` 會使 `fls` 向下取偶,例如 29 會變成 28。該過程可以透過將第 0 位放 0 其他放 1 達成。故 `MMMM` 為 `~1`,注意 `~` 的優先級比 bitwise and 高,因此不需要加上括號。 `NNNN` 和 `PPPP` 放在下節說明。 #### 延伸 : sqrti 原理 (手算平方根) 延伸我們手算平方根的原理,可以推導出以下方式: **流程** - **X**:待開平方的整數,例:十進制的 12312 或二進制的 `0b100101` - **n**:進制基底,十進制時 ($n=10$),二進制時 ($n=2$) - **分組(group)**:將 ($X$) 的數位從最高位起,每次「兩位」作為一組;十進制即「兩個十進位位元」,二進制即「兩個 bit」 - **P**:已經確定的高位部分的商(partial root) - **rem**:剩餘前綴(remainder),即還沒開平方的那部分 - **d**:本輪要嘗試的下一位商,範圍 ($0 \le d<n$) 逐位試商方法可在任意進制 $n$ 下高效求整數平方根。首先將整數 $X$ 從最高位起,每次成組(兩位)帶下作為前綴。假設已得高位商為 $P$,剩餘前綴為 $\mathrm{rem}$,我們在 $0 \le d < n$ 中找 ==最大== $d$,使得新商 $P\,n + d$ 的平方不超過前綴。 透過 **「帶下 → 試商 → 扣除 → 更新商」** 的迭代,直至所有組處理完畢,即可得到 $\lfloor\sqrt{X}\rfloor$。 根據平方和公式: $$ (Pn + d)^2 = P^2n^2 + 2Pnd + d^2 = P^2n^2 + (2Pn + d)d $$ 由於每次計算都已經會從 $\mathrm{rem}$ 扣除 $P^2n^2$,因此我們在試商時使用的判斷式便是: $$ (2Pn + d)d \le \mathrm{rem} $$ $P$ 的更新則是 $$ P = Pn + d $$ **範例** 讓我們以 10 進制開始示範,以 12312 為例子,此處 $2Pn$ 可以寫成 $20P$。 1. **分組** $$12312 \;\longrightarrow\; (1)\,(23)\,(12)$$ 2. **處理第一組 $(1)$** - 帶下:$\mathrm{rem} = 0 \times 100 + 1 = 1$ - 除數底數:$P=0$, $20P = 0$ - 試商:最大 $d = 1$,因 $(0+1)\times1 = 1 \le 1$ - 更新:$\mathrm{rem} = 1 - 1 = 0,\;\; P = 0 \times 10 + 1 = 1$ 3. **處理第二組 $(23)$** - 帶下:$\mathrm{rem} = 0 \times 100 + 23 = 23$ - 除數底數:$P=1$, $20P = 20$ - 試商: $$d=1:\;(20+1)\times1 = 21 \le 23,\qquad d=2:\;(20+2)\times2 = 44 > 23$$ 取 $d = 1$ - 更新:$\mathrm{rem} = 23 - 21 = 2,\;\; P = 1 \times 10 + 1 = 11$ 4. **處理第三組 $(12)$** - 帶下:$\mathrm{rem} = 2 \times 100 + 12 = 212$ - 除數底數:$P=11$, $20P = 220$ - 試商: $$d=0:\;(220+0)\times0 = 0 \le 212,\qquad d=1:\;(220+1)\times1 = 221 > 212$$ 取 $d = 0$ - 更新:$\mathrm{rem} = 212 - 0 = 212,\;\; P = 11 \times 10 + 0 = 110$ 5. **結果驗證** $$110^2 = 12100 \le 12312 < 111^2 = 12321$$ 因此答案是 110。 以 2 進制寫成 C code,$d \in \{0, 1\}$,$(4P + d)d$ 作為試除數,$P = 2P + d$ : ```c uint64_t isqrt_direct(uint64_t X) { uint64_t P = 0; // partial root (accumulated result) uint64_t rem = 0; // remainder after bringing down groups // Compute highest group index k (each group = two bits) // (63 - clz64(X)) gives the index of the top bit; >>1 to get group count int k = (63 - clz64(X)) >> 1; // Process each group from most significant (k) down to 0 for (int i = k; i >= 0; --i) { // 1. Bring down the next group of two bits into rem: // rem = rem * 4 + G_i, where G_i = (X >> (2*i)) & 0x3 rem = (rem << 2) | ((X >> (2 * i)) & 0x3); // 2. Form the trial divisor t = 4*P + 1 uint64_t t = (P << 2) | 1; if (rem >= t) { // d = 1 case: subtract t and set next root bit to 1 rem -= t; // rem -= (4*P + 1) P = (P << 1) | 1; // P = 2*P + 1 } else { // d = 0 case: set next root bit to 0 P <<= 1; // P = 2*P } } return P; } ``` 其中迴圈對 $P$ 的操作能夠更簡化些: ```c for (int i = k; i >= 0; --i) { rem = (rem << 2) | ((X >> (2 * i)) & 0x3); uint64_t t = (P << 2) | 1; P <<= 1; if (rem >= t) { rem -= t; P |= 1; } } ``` #### 延伸 : sqrti 原理 (多元平方和) 題目中的作法可以參考 [從 √2 的存在談開平方根的快速運算 - Digit-by-digit Method](https://hackmd.io/HUzAPdVVSACpsnaj5Omvgw?view#Digit-by-digit-Method)、 Hacker's Delight Edition 2 中 11-2 Hardware Algorithm 和 [2025-02-25 問答簡記](https://hackmd.io/l4--gpsZQBiEI5LKS1lDvg?view)。 - **$N^2$**:要開平方的整數。 - **$n$**:最高位的索引。將 $N$ 在所選進制下展開為 $$ N = a_n + a_{n-1} + \cdots + a_0 $$ 後,最大的次方指標即為 $n$。 - **$m$**:當前迴圈的位元指標,從 $n$ 依序遞減到 $0$,用以決定第 $m$ 位的貢獻。 - **$a_m$**:第 $m$ 位的貢獻值。 - 二進制:$a_m \in \{0,\,2^m\}$ - 十進制:$a_m \in \{0,\,1\cdot10^m,\dots,9\cdot10^m\}$ - **$P_{m+1}$**:已經「放入」的高位和, $$ P_{m+1} = \sum_{i=m+1}^n a_i. $$ 注意,我們計算時會從高位往低位計算,因此 $P_{m+1}$ 會比 $P_m$ 更早被計算。 - **$P_m$**:加入第 $m$ 位後的部分和 $$ P_m = P_{m+1} + a_m. $$ - **$X_{m+1}$**:第 $m+1$ 輪結束後剩餘的餘量,初始為 $$ X_{n+1} = N^2. $$ - **$Y_m$**:若放入第 $m$ 位所產生的 ==增量==, $$ Y_m = (2P_{m+1} + a_m)\,a_m. $$ - **$X_m$**:第 $m$ 輪結束後的新餘量, $$ X_m = X_{m+1} - Y_m. $$ 老師推導中的核心被開根號數 ($N^2$) 可以被拆解成多項和 $(a_n + a_{n-1} + ... + a_0)^2$,且多元平方和中,如果多增加一項 ($a_m$),則增量 ($Y_m$) 可以由下式表達: $$ Y_m = (2P_{m+1} + a_m)\,a_m. $$ 此處 $P_{m+1}$ 是指從最前面的項往下加到 $a_m$ 前一項,例如對於 $N=a_n + ... + a_{m+1} + a_m$,注意此處 $m+1$ 代表的是 $m$ 的前一項而不是後一項: $$ P_{m+1} = a_n + a_{n - 1} + ... + a_{m+2} + a_{m+1} $$ 這是很好的特性,因為能知道增加下一項 $a_m$ 會不會超過 $N^2$。將此概念套用到套用到 2 進制整數系統後,數字 $x$ 可表示成 : $$ x \;=\; \Bigl(\underbrace{00000}_{\text{leading zeros}} \;\underbrace{b_n\,b_{n-1}\dots b_0}_{\text{value}}\Bigr)_2^2 = \lfloor N^2 \rfloor $$ 此時,$\lfloor N^2 \rfloor$ 可表示成: $$ \begin{aligned} \lfloor N^2 \rfloor &= (b_n \times 2^n + b_{n-1} \times 2^{n-1} + \dots + b_1 \times 2^1 + b_0 \times 2^0)^2 \\ &= (a_n + a_{n-1} + \dots + a_0)^2 \end{aligned} $$ 此時,誤差 $X_m$ 可表達為 : $$ X_m = N^2 - P_m^2 = X_{m+1} - Y_m $$ 而 $Y_m$ 可進一步表達成 $$ \begin{aligned} Y_m &= (2P_{m+1} + a_m)\,a_m = \bigl(2P_{m+1} + b_m\,2^m\bigr)\,\bigl(b_m\,2^m\bigr) \\[6pt] &= 2b_mP_{m+1}2^m + b_m^2 2^{2m} = \begin{cases} 0, & b_m = 0,\\ P_{m+1}2^{m+1} + 2^{2m}, & b_m = 1. \end{cases} \end{aligned} $$ 我們能將其拆解成 $C_m = P_{m+1}2^{m+1}$ 和 $Dm = 2^{2m}$ 其中 $C_m$ 可寫成: $$ \begin{align} C_m &= P_{m+1}2^{m+1} \\ &= (P_m-a_m)2^{m}2 \\ &= 2P_m2^{m} - 2a_m2^{m} \\ &= 2c_{m-1} - 2a_m2^m. \end{align} $$ 移向後得: $$ C_{m-1} = \frac{C_m}{2} + 2a_m2^m = \frac{C_m}{2} + D_m $$ $D_m$ 的關係則如下: $$ D_m = a_m 2^{2m} = a_m 2^{2(m-1) + 2} = a_m D_{m-1} \times 4 $$ 移向後得 $$ D_{m-1} = \begin{cases} \frac{D_m}{4}, & a_m = 1 \\ 0, & a_m = 0 \end{cases} $$ 對應到題目中的實作可知: - `b` : $Y_m = C_m + D_m$ - `y` : $C_m = \frac{C_{m+1}}{2} + D_m$ - `m` : $D_m$,不論如何持續右移 2 bits 故 `NNNN` 對應 1 ($C_{m-1} = C_m / 2$,右移 1),`PPPP` 對應 2 ($D_{m-1} = D_m / 4$,右移 2)。 另外,若不希望分支存在,可以使用: ```c t = (int64_t)(x | ~(x - b)) >> 63; // -1 (all set) if x >= b, else 0 (all unset). x = x - (b & t); y = y + (m & t); ``` #### 延伸 : sqrti 向上取整 向上取整的實作可以基於前述向下取整的實作,只需要判斷 `x` 殘留值是否大於 0。因此返回值更改為以下即可: ```c return y + (x > 0); ``` 根據 C11 §6.5.8 - 6 >Each of the operators < (less than), > (greater than), <= (less than or equal to), and >= (greater than or equal to) **shall yield 1 if the specified relation is true and 0 if it is false**.$^{107)}$ The result has type **int**. 判斷大小的運算子只可能返回 0 或 1,因此可用來進行向上取整。 #### 延伸 : sqrtf ### 測驗 3 #### 延伸 : 證明 Theorem S :::success When we successively insert the points $\{\theta\}, \{2\theta\}, \dots$ into the interval $[0,1]$, **Theorem S** asserts that each new point always splits one of the largest remaining subintervals. If the interval $[a,c]$ is thereby broken into two parts $[a,b]$ and $[b,c]$, we call it a *bad break* if one of these parts is more than twice as long as the other, namely if - $b - a > 2\,(c - b)$, or - $c - b > 2\,(b - a)$. **Exercise 8 (Section 6.4):** Prove that bad breaks will occur for some $\{n\theta\}$ unless $$ \theta \bmod 1 = \varphi^{-1} \quad\text{or}\quad \theta \bmod 1 = \varphi^{-2}, $$ and that for these latter values of $\theta$, no bad breaks ever occur. :::