# 2023q1 Homework4 (quiz4) contributed by < `Joshualee0321` > ## 開發環境 ```bash $ gcc --version gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 Copyright (C) 2021 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 43 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 12 On-line CPU(s) list: 0-11 Vendor ID: AuthenticAMD Model name: AMD Ryzen 5 2600X Six-Core Processor CPU family: 23 Model: 8 Thread(s) per core: 2 Core(s) per socket: 6 Socket(s): 1 Stepping: 2 Frequency boost: enabled CPU max MHz: 3600.0000 CPU min MHz: 2200.0000 BogoMIPS: 7199.10 Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht ``` ## 測驗一 [`rb.h`](https://gist.github.com/jserv/6610eb56bf2486979c8bf6ee8061f71c) * 打開這個標頭檔後,可發現許多巨集以下為自己列出的筆記,<s>歡迎各位幫我指正我哪裡解釋錯誤。</s> :::danger 筆記和程式碼本來就要讓各方指教,不用特別提及。 :notes: jserv ::: ### 理解 rb.h 的實作 展開該巨集,利用 `rb_gen(static, ex_, ex_t, ex_node_t, ex_link, ex_cmp)` 把以下的巨集展開之後可以得到 ```c static void ex_new(ex_t *tree); static ex_node_t *ex_search(ex_t *tree, ex_node_t *key); static void ex_insert(ex_t *tree, ex_node_t *node); static void ex_remove(ex_t *tree, ex_node_t *node); static void ex_destroy(ex_t *tree, void (*cb)(ex_node_t *, void*), void *arg); ``` :::danger 避免非必要的項目標示 (亦即 `* ` 開頭的描述),並改進你的漢語表達。 :notes: jserv ::: 接下來我們可以觀察 `ex_insert` 的實作方法,其中大致可以分成幾個部分 ```c static void ex_insert(ex_t *rbtree, ex_node_t *node) { ex_path_entry_t path[RB_MAX_DEPTH]; ex_path_entry_t *pathp; rbt_node_new(ex_node_t, ex_link, rbtree, node); /* Wind. */ path->node = rbtree->root; for (pathp = path; pathp->node; pathp++) { int cmp = pathp->cmp = x_cmp(node, pathp->node); assert(cmp != 0); if (cmp < 0) { pathp[1].node = rbtn_left_get(ex_node_t, ex_link, pathp->node); } else { pathp[1].node = rbtn_right_get(ex_node_t, ex_link, pathp->node); } } pathp->node = node; ``` * 首先宣告 `ex_path_entry_t path[RB_MAX]` 以及 `ex_path_entry_t *pathp` 作為 iterator * 其作用為從樹根開始走訪節點(尋找插入的地方),並且把往左往右記錄在 `ex_path_entry_t` 的陣列當中 * 假設一紅黑樹如下 ```graphviz digraph BST { graph[ordering="out"] node [shape=circle, fixedsize=true, style=filled,width=.8] "B:35"[fillcolor=red] "C:60"[fillcolor=red] "A:50" "A:50" -> "B:35" "A:50" -> "C:60" } ``` * 假設現在要插入 `15`,則會先以 `BST` 的特性找到該插入的地方並且插入 :::info 注意這邊先行設定愈插入的 node 為紅色 ::: * 則接下來會得到以下樹 ```graphviz digraph BST{ graph[ordering="out"] node [shape=circle, fixedsize=true, style=filled, width=.8] "B:35"[fillcolor=red] "C:60"[fillcolor=red] NULL[color=transparent] "D:15"[color=red] "A:50" -> "B:35" "A:50" -> "C:60" "B:35" -> "D:15" "B:35" -> NULL } ``` 此時的 `path` 會長這樣 ```graphviz digraph { node [shape=plaintext, fontcolor=black, fontsize=16]; "Iter:" -> "Values:" [color=white]; node [shape=record, fontcolor=black, fontsize=14, width=4.75, fixedsize=true]; pointers [label="<f0> pathp | <f2> pathp[1]", color=white]; values [label="<f0> 50, -1| <f1> 35, -1| <f2> NULL| <f3> | <f4> | <f5> "]; { rank=same; "Iter:"; pointers } { rank=same; "Values:"; values } edge [color=blue]; pointers:f0 -> values:f1; pointers:f2 -> values:f2; } ``` 依照 `rb_insert` 中的 `pathp->node = node` ,把 `D:15` 插入在 `NULL` 中,接下來就進入到程式碼的第二個片段 ```c for (pathp--; (uintptr_t) pathp >= (uintptr_t) path; pathp--) { x_type *cnode = pathp->node; if(pathp->cmp < 0) { ... } else { ... } } ``` 由於在上一個迴圈中 `pathp` 的位置如下 ```graphviz digraph { node [shape=plaintext, fontcolor=black, fontsize=16]; "Iter:" -> "Values:" [color=white]; node [shape=record, fontcolor=black, fontsize=14, width=4.75, fixedsize=true]; pointers [label="<f0> pathp | <f2> ", color=white]; values [label="<f0> 50, -1| <f1> 35, -1| <f2> NULL| <f3> | <f4> | <f5> "]; { rank=same; "Iter:"; pointers } { rank=same; "Values:"; values } edge [color=blue]; pointers:f0 -> values:f2; } ``` 故要把原本的指針往前調整如下 ```graphviz digraph { node [shape=plaintext, fontcolor=black, fontsize=16]; "Iter:" -> "Values:" [color=white]; node [shape=record, fontcolor=black, fontsize=14, width=4.75, fixedsize=true]; pointers [label="<f0> pathp | <f2> ", color=white]; values [label="<f0> 50, -1| <f1> 35, -1| <f2> NULL| <f3> | <f4> | <f5> "]; { rank=same; "Iter:"; pointers } { rank=same; "Values:"; values } edge [color=blue]; pointers:f0 -> values:f1; } ``` 所以才會在這個迴圈先做一次 `pathp--` 往前走一格。 再來,把以上 `if` 的部分展開之後可以看到 `else` 在下一個部分再看 ```c // cnode = 當前位置 if(pathp->cmp < 0) { ex_node_t *left = pathp[1].node; rbtn_left_set(ex_node_t, ex_link, cnode, left); if (!rbtn_red_get(ex_node_t, ex_link, left)) return; ex_node_t *leftleft = rbtn_left_get(ex_node_t, ex_link, left); if(leftleft && rbtn_red_get(ex_node_t, ex_link, leftleft)) { ex_node_t *tnode; // 暫存 rbtn_black_set(ex_node_t, ex_link, leftleft); rbtn_rotate_right(ex_node_t, ex_link, cnode, tnode); cnode = tnode; } else{...} } ``` * 由於這樣設計只需要考慮往左邊或往右邊,所以這邊用格子中的值去判斷自己的親節點是左邊還是右邊 `if(pathp->cmp < 0){...} else{...}`,並且由於是左傾斜的樹,他只需要對右邊的特殊狀況處理即可 * 如果有兩個顏色都為紅色的節點,那就代表這棵 `RB_t` 並不平均,那他就會旋轉,圖示如下 ```graphviz digraph BST { graph[ordering="out"] node [shape=circle, fixedsize=true, style=filled,width=.8] left,leftleft[fillcolor=red] null,null1[fontsize=0,color=transparent] cnode -> left cnode -> null[color=transparent] left -> leftleft left -> null1[color=transparent] fix_left[fillcolor=red] fix_left -> fix_leftleft fix_left -> fix_cnode } ``` 當然,這邊只考慮了往左的狀況,那如果往右邊呢? ```c else { ex_node_t *right = path[1].node; rbtn_right_set(ex_node_t, ex_link, cnode, right); // 把 current node 的右邊設定為當前的下一個點 if (!rbtn_red_get(ex_node_t, ex_link, right)) // 如果沒有連續兩個紅色 return; ex_node_t *left = rbtn_left_get(ex_node_t, ex_link, cnode); // 這邊對右邊的狀況作特殊處理 if (left && rbtn_red_get(ex_node_t, ex_link, left)) { rbtn_black_set(ex_node_t, ex_link, left); rbtn_black_set(ex_node_t, ex_link, right); rbtn_red_set(ex_node_t, ex_link, cnode); } else { /* 因為並不是兩個連續的紅色 node,又是右邊,所以必須回復左傾斜的特性*/ ex_node_t *tnode; bool tred = rbtn_red_get(ex_node_t, ex_link, cnode); // 看當前的點是否為紅 rbtn_rotate_left(ex_node_t, ex_link, cnode, tnode); rbtn_color_set(ex_node_t, ex_link, tnode, tred); rbtn_red_set(ex_node_t, ex_link, cnode); // 旋轉後根據顏色設定 cnode = tnode; } } pathp->node = cnode; ``` 最後再根據紅黑樹的特性,把樹根設定為黑色即可 `rbtree-root = path->node; rbtn_black_set(ex_node_t, ex_link, rbtree->root);` :::info 以上為 linux 左傾斜紅黑樹插入的實作 > 確認 Linux 核心的 rbtree 是否為 LLRBT,若不是,考慮因素在哪? :notes: jserv <s> `AAAA` = `rbtn_black_set` `BBBB` = `tred` `CCCC` = `rbtn_red_set` </s> ::: ### 以下為 `rb_tree` 的刪除部分,由於太長,這邊只列出想法以及答案 此程式碼也可以分成幾段來觀看,並且於先前 `insert` 一樣,都會創建一個 `iteration_list` 來記錄當前走過的路。第一段 ```c for(pathp = path; pathp->node; pathp++) { int cmp = pathp->cmp = x_cmp(node, pathp->node); if(cmp < 0) {...} // 代表往左走 else { // 代表往右走 或是找到了 if(cmp == 0) { // 找到要刪除的點 pathp->cmp = 0; // 這邊可以視為用 0 紀錄要刪除的點 for(pathp++; pathp->node; pathp++) { ... } // 往左找替換點 } } } ``` * 接下來為下一個段落,下一個段落就是從先確認是否在 0 和 1 層做事,如果是的話就不需要和後繼者 (`successor`) 互相交換,並且把要刪除的點放在 `path[-1]`,反之,則調整顏色 ```c pathp--; // 因為會跑到 NULL 節點 要往回走一格 if (pathp->node != node) { ... } // left successor else { ex_node_t *left = rbtn_left_get(ex_node_t, ex_link, node); rbtn_black_set(ex_node_t, ex_link, left); if (pathp->node != node) { ... } else { ex_node_t *left = rbtn_left_get(ex_node_t, ex_link, node); if(left) { ... } else if(!rbtn_right_get(ex_node_t, ex_link, node)) {} } } ``` * 經剛剛所述都調整過後 (經過交換 `successor` 以及設定顏色後) 開始從當前位置往前調整紅黑樹 * <s>根據此[網誌](http://alrightchiu.github.io/SecondRound/red-black-tree-deleteshan-chu-zi-liao-yu-fixupxiu-zheng.html)得知紅黑樹的刪除過程</s> :::warning 參照更權威的書籍和文獻,例如大學教科書,避免從語焉不詳的網誌學習。 :notes: jserv ::: * 最後就是根據當前的點往前調整紅黑樹使其恢復平衡 首先先來看到程式碼片段的 FFFF ```c if (leftrightleft && \ rbtn_red_get(x_type, x_field, leftrightleft)) { \ /* || */ \ /* pathp(b) */ \ /* / \\ */ \ /* (r) (b) */ \ /* \ */ \ /* (b) */ \ /* / */ \ /* (r) */ \ x_type *unode; \ rbtn_black_set(x_type, x_field, leftrightleft); \ rbtn_rotate_right(x_type, x_field, pathp->node, \ unode); \ rbtn_rotate_right(x_type, x_field, pathp->node, \ tnode); \ rbtn_right_set(x_type, x_field, unode, tnode); \ FFFF(x_type, x_field, unode, tnode); ``` 欲刪除的節點為目前節點的右節點,為了要使刪除時維持左傾斜的特性,作者選擇右旋轉兩次,以下為圖示 ```graphviz digraph BST { graph[ordering="out"] node [shape=circle, fixedsize=true, style=filled,width=.8] n1, n2, n3[color=transparent, fontsize=0] "A" "B:unode"[fillcolor=red] "C" "D"[fillcolor=red] "A" -> "B:unode" "A" -> "E" "B:unode" -> n1[color=transparent] "B:unode" -> "C" "C" -> "D" "C" -> n2[color=transparent] } ``` 在最後一步之前會轉化為這樣 ```graphviz digraph BST { graph[ordering="out"] node [shape=circle, fixedsize=true, style=filled,width=.8] n1, n2, n3, n4[color=transparent, fontsize=0] "A" "B:unode"[fillcolor=red] "C" "D"[fillcolor=red] tnode -> n4[color=transparent] tnode -> "B:unode" "B:unode" -> n1[color=transparent] "B:unode" -> "A" "A" -> "C" "A" -> "E" "C" -> "D" "C" -> n2[color=transparent] } ``` 在經過最後一次左旋轉即可得到左傾斜的紅黑樹 ```graphviz digraph BST { graph[ordering="out"] node [shape=circle, fixedsize=true, style=filled,width=.8] n1, n2, n3, n4[color=transparent, fontsize=0] "A" "B:unode"[fillcolor=red] "C" "D"[fillcolor=red] "B:unode" -> tnode "B:unode" -> "A" "A" -> "C" "A" -> "E" "C" -> "D" "C" -> n2[color=transparent] } ``` 如此一來即恢復平衡 在 `GGGG` 則是可以直接以 `rbtn_rotate_right` 填入來恢復平衡 :::info 以上為 linux 左傾斜紅黑樹刪除的想法 `DDDD` = `0` : 為了要記錄刪除的點 `EEEE` = `!rbtn_right_get(x_type, x_field, node)` : 為了看是否右邊沒有東西 `FFFF` = `rbtn_rotate_left` `GGGG` = `rbtn_rotate_right` ::: --- ## 測驗二 ### `TimSort` * 是一種排序演算法,幾乎所有的情況都可以有 `O(nlogn)` (comparison based sort 的最佳時間複雜度),其中利用了 `insertion_sort` 跟 `merge_sort` 來分別處理比較少的資料以及比較多的資料。 ### `__inplace_merge` 為了要合併兩個 `run` 並且保持 `inplace`, 這邊採用的方法為反轉裡面的資料來達到排序的效果 * 由於我們可以確定每一個 run 都經由 `insertion_sort` 排序,所以為升冪排序,以下給予圖片解說來解釋裡面程式碼的作用 * 假設兩個run 分別為 `[1, 2, 4, 5]` 跟 `[3, 6, 10]` 流程如下: 1. 首先將 `cur_1` 指到比 `first_2` 更小的位置 利用 `while(cur_1 < last_2 && !cmp(cur_2, cur_1)) cur_1 += size;` 2. 再來把 `cur_2` 指到比 `cur_1` 更小的位置 利用 `while(cur_2 < last_2 && cmp(cur_2, cur_1)) cur_2 += size;` 即可得到目前的狀況 ```graphviz digraph { node [shape=plaintext, fontcolor=black, fontsize=16]; "" -> "Values:" [color=white]; node [shape=record, fontcolor=black, fontsize=14, width=4.75, fixedsize=true]; pointers [label="<f0> first_1 |<cur> cur_1| <f1> last_1 | <f2> first_2| <f5> cur_2| <f3> last_2", color=white]; values [label="<f0> 1| <f1> 2| <f2> 4| <f3> 5 | <f4> 3 | <f5> 6 | <f6> 10"]; { rank=same; ""; pointers } { rank=same; "Values:"; values } edge [color=blue]; // pointers:f0 -> values:f2; pointers:f0 -> values:f0; pointers:f2 -> values:f4; pointers:f1 -> values:f3; pointers:f3 -> values:f6; pointers:cur -> values:f2; pointers:f5 -> values:f4; } ``` 接下來把 `cur_1` 到 `first_2` 之前相反過來 * `__reverse(cur_1, first_2, size)` 再來將 `first_2` 與 `cur_2` 之前相反過來 * `__reverse(first_2, cur_2, size)` 即可得到當前的陣列 ```graphviz digraph { node [shape=plaintext, fontcolor=black, fontsize=16]; "" -> "Values:" [color=white]; node [shape=record, fontcolor=black, fontsize=14, width=4.75, fixedsize=true]; pointers [label="<f0> first_1 |<cur> cur_1| <f1> last_1 | <f2> first_2| <f5> cur_2| <f3> last_2", color=white]; values [label="<f0> 1| <f1> 2| <f2> 5| <f3> 4 | <f4> 3 | <f5> 6 | <f6> 10"]; { rank=same; ""; pointers } { rank=same; "Values:"; values } edge [color=blue]; // pointers:f0 -> values:f2; pointers:f0 -> values:f0; pointers:f2 -> values:f4; pointers:f1 -> values:f3; pointers:f3 -> values:f6; pointers:cur -> values:f2; pointers:f5 -> values:f4; } ``` 可以注意到,根據剛剛的翻轉,中間已經呈現了一個降冪的關係,此時只需要重新反轉一次 即可得到最後排序好的兩個 `run` `__reverse(cur_1, cur_2, size)` ### 為避免單個 run 太長,導致 `merge` 速度變差 * 設計這個 `tim_sort` 的人有稍微考慮到,當兩個 `run` 的長度不一致,達到非常懸殊的等級的時候,假設兩個 `run` 分別為 `A_run, (len(A_run) == 10)` 以及 `B_run, (len(B_run) == 100)`,此時的 `merge` 會在兩個已經sort好的陣列中做出相當多不需要的操作,而當兩個 `run` 的長度差不多時,此時的 `merge` 才算比較有效率。 * 為了達到此效用,作者設計了一個 `merge_rule` 如下 ```c static inline bool merge_rule(run_t *stack, size_t top) { if (top < 1) return false; if (top < 2) return run_length(stack[top]) > run_length(stack[top - 1]); return run_length(stack[top]) > run_length(stack[top - 1]) || run_length(stack[top]) + run_length(stack[top - 1]) > run_length(stack[top - 2]); } ``` 意即若當前這個 `run` 加上前一個 `run` 沒有比前兩個 `run` 大時,不會進入 `merge` 的片段 :::info <s> 各答案如下: `AAAA` : `__reverse(cur_1, cur_2, size)` `BBBB` : `top - 1` `CCCC` : `top - 2` `DDDD` : `first + MIN_RUN * size` 我想這邊應該不需要太過解釋,就只是沒有超過時取最小 `EEEE` : `top--` 為了要合併 top 的內容 `FFFF` : `stack[top - 1].last = stack[top].last` 一個一個合併 </s> 不用抄寫答案,只要專注程式碼本身和你的理解。 :notes: jserv ::: --- ## 測驗三 ### `RB_LOG2_MAX_NODES` ```c #define RB_LOG2_MAX_MEM_BYTES (sizeof(void *) << 3) /* clang-format off */ #define RB_LOG2_MAX_NODES \ (RB_LOG2_MAX_MEM_BYTES - \ (sizeof(void *) >= 8 ? 4 \ : sizeof(void *) >= 4 ? 3 \ : 1) - 1) /* clang-format on */ ``` 可以根據 `rb_tree` 的定義,樹高為 `2 * log(n + 1)` 以及不超出記憶體限制的規則得到以下結論。 ### `sum_tree` :::danger 沒必要將程式碼註解改為中文,直接解讀程式碼的作用,避免逐行解說,而該揣摩設計者的思維。 :notes: jserv ::: ```c static int sum_subtree(node_t *node) { int result = 0; while (node) { node_t *pre; if (!rbtn_left_get(node_t, link, node)) goto do_print; for (pre = rbtn_left_get(node_t, link, node); rbtn_right_get(node_t, link, pre) && rbtn_right_get(node_t, link, pre) != node; pre = rbtn_right_get(node_t, link, pre)) { } /* 根據每一個點往右走*/ if (!rbtn_right_get(node_t, link, pre)) { rbtn_right_get(node_t, link, pre) = node; node = rbtn_left_get(node_t, link, node); /* 右邊沒有找左邊 */ } else { rbtn_right_get(node_t, link, pre) = NULL; do_print: result += node->value; printf("value: %d\n", node->value); node = rbtn_right_get(node_t, link, node); /* cache */ } } return result; } ```