# 2023q1 Homework4 (quiz4) contributed by < `DokiDokiPB` > > 巨集展開後的[程式碼 Github gist](https://gist.github.com/a1091150/b7040acd206d0188d7fbd439c05ad51c) > 其中用於觀察紅黑樹行為的函式為 `good()` > 並附上 `printTreeInorder()` 用於印出紅黑樹結構 - 透過以上 [USFCA 大學的 B tree 視覺化工具](https://www.cs.usfca.edu/~galles/visualization/BTree.html) 顯示 2-3 tree - 使用 [線上工具 ysangkok.github.io](http://ysangkok.github.io/js-clrs-btree/btree.html) 繪製 B-tree 的 Graphviz code ## 測驗 1 撰寫這題時候發現自己缺少閱讀 - 從 GitHub 搜尋簡單的程式碼與介紹,初步了解最基本的紅黑樹與 B tree 介紹。 - [anandarao/Red-Black-Tree 實作](https://github.com/anandarao/Red-Black-Tree/blob/master/RBTree.cpp#L93) - [資料結構與演算法:Red Black Tree 紅黑樹 part 1 by Joseph](https://josephjsf2.github.io/data/structure/and/algorithm/2020/04/28/red-black-tree-part-1.html) - 閱讀教材的簡報 [Left-Leaning Red-Black Trees](https://hackmd.io/@sysprog/linux-rbtree),照著簡報中了解 LLRBT 對應的 2-3 樹與 2-3-4 樹。 - [Nicholas Tsai B Tree Deletion](https://yu-shin.github.io/posts/fc4bcf8f/) :::info 其中以下撰寫的內容的核心概念: $\rightarrow$ 原本 2-3 樹或 2-3-4 樹對應多種紅黑樹,但是限制為左傾紅黑樹後,便成為一對一的關係,只對應一種紅黑樹。 因此在了解原理與程式碼運作上,可以先透過 2-3 樹行為去了解紅黑樹插入與移除行為。 ::: ### 了解其原理 根據註釋,這裡使用的是 `C macro implementation of left-leaning 2-3 red-black trees.` #### `tree_insert()` 函式 透過 [USFCA 大學的 B tree 視覺化工具](https://www.cs.usfca.edu/~galles/visualization/BTree.html) 加入節點,可以很好的了解依序輸入後獲得的 2-3 樹,就能對照 `tree_insert()` 函式行為。 將巨集 `x_attr void x_prefix##insert(x_rbt_type *rbtree, x_type *node)` 展開為 `static void tree_insert (tree_t *rbtree, node_t *node)`,並分成三段程式碼。這裡主要展示第二段與第三段 - 第一段程式碼:紅黑樹也是一種二元搜尋樹,紀錄走訪過的節點 - 第二段程式碼:透過修改顏色的方式,消去 4 樹成為 2 樹與 3 樹 - 第三段程式碼:將 4 樹分裂、將 2 樹右傾改為左傾 在以下圖例中,節點元素有 `*` 表示當前 `*cnode` 指向的節點 <!-- ```c // 第一段程式碼 tree_path_entry_t path[(sizeof(void *) << 4)]; tree_path_entry_t *pathp; path->node = rbtree->root; for (pathp = path; pathp->node; pathp++) { int cmp = pathp->cmp = node_cmp(node, pathp->node); if (cmp < 0) pathp[1].node = rbtn_left_get(node_t, link, pathp->node); else // pathp[1].node = ((node_t *)(((intptr_t)(pathp->node)->link.right_red) & ~3)); pathp[1].node = rbtn_right_get(node_t, link, pathp->node); } pathp->node = node; ``` 第一段程式碼: - `cmp` 標示節點的傾向 - `pathp` 紀錄走訪過的節點、`pathp[1].node` 為下一個走訪的節點 - 紅黑樹的顏色標示是放在 `link.right_red` 最低 1 個位元 - 0 表示黑色、 1 表示紅色 --> ##### 第二段程式碼 ```c for (pathp--; (uintptr_t)pathp >= (uintptr_t)path; pathp--) { \ x_type *cnode = pathp->node; \ if (pathp->cmp < 0) { \ x_type *left = pathp[1].node; \ rbtn_left_set(x_type, x_field, cnode, left); \ if (!rbtn_red_get(x_type, x_field, left)) \ return; \ x_type *leftleft = rbtn_left_get(x_type, x_field, left); \ if (leftleft && rbtn_red_get(x_type, x_field, leftleft)) { \ /* Fix up 4-node. */ \ x_type *tnode; \ rbtn_black_set(node_t, link, leftleft); \ rbtn_rotate_right(x_type, x_field, cnode, tnode); \ cnode = tnode; \ } \ } else { \ /* ... */ \ } \ pathp->node = cnode; \ } ``` :::danger 使用 `clang-format` 對上述程式碼進行一致排版,確保以 4 個空白縮排。 :notes: jserv ::: 插入 1 元素之後,對應的示例圖 ```graphviz digraph BST { graph [ratio=.44 ordering="out"]; node [style=filled, shape=circle, width=.6 fontname=Helvetica, fontweight=bold, fontcolor=black, fontsize=36, fixedsize=true, fillcolor=white]; {rank = same; 2}; 1, 2 [color=red]; 3 [color=black, label="3*"]; n1, n2 [fontsize=0, fillcolor=white, color=transparent]; 3 -> 2; 2 -> 1; 3 -> n1 [color="transparent"]; 2 -> n2 [color="transparent"]; } ``` 轉換成 ```graphviz digraph BST { graph [ratio=.44 ordering="out"]; node [style=filled, color=red, shape=circle, width=.6 fontname=Helvetica, fontweight=bold, fontcolor=black, fontsize=36, fixedsize=true, fillcolor=white]; {rank = same; 1;3}; 3, 1 [color=black]; 2 [label="2*"] 2 -> 1; 2 -> 3; } ``` 若 2 為根節點,則最終會被標注為黑色 ##### 第三段程式碼 ```c for (pathp--; (uintptr_t)pathp >= (uintptr_t)path; pathp--) \ { \ x_type *cnode = pathp->node; if (pathp->cmp < 0) \ { ... }else{ x_type *right = pathp[1].node; \ rbtn_right_set(x_type, x_field, cnode, right); \ if (!rbtn_red_get(x_type, x_field, right)) \ return; \ x_type *left = rbtn_left_get(x_type, x_field, cnode); \ if (left && rbtn_red_get(x_type, x_field, left)) \ { \ /* Split 4-node. */ \ rbtn_black_set(x_type, x_field, left); \ rbtn_black_set(x_type, x_field, right); \ rbtn_red_set(x_type, x_field, cnode); \ } \ else \ { \ /* Lean left. */ \ x_type *tnode; \ bool tred = rbtn_red_get(x_type, x_field, cnode); \ rbtn_rotate_left(x_type, x_field, cnode, tnode); \ rbtn_color_set(x_type, x_field, tnode, tred); \ rbtn_red_set(x_type, x_field, cnode); \ cnode = tnode; \ } } pathp->node = cnode; } ``` 對應 `else case /* Lean left. */` 的程式碼,以示例圖表示其運作方式: 插入元素 7 之後,依據左傾的規定,必須左旋 ```graphviz digraph BST { graph [ratio=.44 ordering="out"]; node [style=filled, color=red, shape=circle, width=.6 fontname=Helvetica, fontweight=bold, fontcolor=black, fontsize=36, fixedsize=true]; 4,5,7 [fillcolor=white]; 4,5 [fillcolor=white, color="black"]; 6 [fillcolor=white, color="black", label="6*"]; n1, n2 [fontsize=0, fillcolor=white, color=transparent]; 5 -> 4; 5 -> 6; 6 -> n1 [color="transparent"]; 6 -> 7; } ``` 轉換成 ```graphviz digraph BST { graph [ratio=.44 ordering="out"]; node [style=filled, color=red, shape=circle, width=.6 fontname=Helvetica, fontweight=bold, fontcolor=black, fontsize=36, fixedsize=true, fillcolor=white]; 4, 5 [fillcolor=white, color="black"]; 7 [fillcolor=white, color="black", label="7*"]; n1, n2 [fontsize=0, fillcolor=white, color=transparent]; 5 -> 4; 5 -> 7; 7 -> 6; 7 -> n1 [color="transparent"]; } ``` 這裡以再加入一個元素 8 作為範例,對應 `if case` 的程式碼,分裂 4 樹 ```graphviz digraph BST { graph [ratio=.44 ordering="out"]; node [style=filled, color=red, shape=circle, width=.6 fontname=Helvetica, fontweight=bold, fontcolor=black, fontsize=36, fixedsize=true, fillcolor=white]; 4, 5 [fillcolor=white, color="black"]; 7 [fillcolor=white, color="black", label="7*"]; 8; 5 -> 4; 5 -> 7; 7 -> 6; 7 -> 8; } ``` 轉換成 ```graphviz digraph BST { graph [ratio=.44 ordering="out"]; node [style=filled, color=red, shape=circle, width=.6 fontname=Helvetica, fontweight=bold, fontcolor=black, fontsize=36, fixedsize=true, fillcolor=white]; 4, 5,6 ,8 [fillcolor=white, color="black"]; 7 [fillcolor=white, color="red", label="7*"]; 8; 5 -> 4; 5 -> 7; 7 -> 6; 7 -> 8; } ``` 再因為 LLRB 規則, `5*` 要左旋,從 ```graphviz digraph BST { graph [ratio=.44 ordering="out"]; node [style=filled, color=red, shape=circle, width=.6 fontname=Helvetica, fontweight=bold, fontcolor=black, fontsize=36, fixedsize=true, fillcolor=white]; 4, 5,6 ,8 [fillcolor=white, color="black"]; 5 [fillcolor=white, color="black", label="5*"]; 8, 7; 5 -> 4; 5 -> 7; 7 -> 6; 7 -> 8; } ``` 轉換成 ```graphviz digraph BST { graph [ratio=.44 ordering="out"]; node [style=filled, color=red, shape=circle, width=.6 fontname=Helvetica, fontweight=bold, fontcolor=black, fontsize=36, fixedsize=true, fillcolor=white]; 4,6 ,8 [fillcolor=white, color="black"]; 8, 7; 5; 7 [fillcolor=white, color="black", label="7*"]; 7 -> 5; 7-> 8; 5 -> 6; 5 -> 4; } ``` #### `tree_remove()` 函式 我決定直接跑一遍來看程式碼的作用。 在 ChatGPT 出來後,工程師寫程式碼的效率可以更高,其中<s>對於絕大多數的軟體工程師</s>,閱讀如紅黑樹移除節點的複雜程式碼,第一時間都無法知道或驗證程式碼作用。 :::warning 自己不懂的話,不要拉其他人下水。 :notes: jserv ::: 紅黑樹本身是 2-3 樹或 2-3-4 樹的變形,移除某個節點產生的平衡行為皆對應 2-3 tree 的操作。 ##### 推敲其原理 巨集展開後的程式碼請參考 [`LLRBT_m.cpp`](https://gist.github.com/a1091150/b7040acd206d0188d7fbd439c05ad51c#file-llrbt_m-c-L1),這裡使用測試資料來了解其中的運作模式。 依序輸入測試資料:`1, 2, 3, 4, 40, 41`,獲得 ```graphviz digraph g { node [shape = record,height=.1]; node0[label = "<f0> |2|<f1> |4|<f2>"]; node1[label = "<f0> |1|<f1>"]; "node0":f0 -> "node1" node2[label = "<f0> |3|<f1>"]; "node0":f1 -> "node2" node3[label = "<f0> |40|<f1> |41|<f2>"]; "node0":f2 -> "node3" } ``` 對應 LLRBT ```graphviz digraph 這圖片很難調整的二元樹的拉 { graph [ratio=.44 ordering="out"]; node [style=filled, color=red, shape=circle, width=.6 fontname=Helvetica, fontweight=bold, fontcolor=black, fontsize=36, fixedsize=true, fillcolor=white]; 2, 40; 4, 3, 41, 1 [fillcolor=white, color="black"]; n1, n2, n3 [fontsize=0, fillcolor=white, color=transparent]; 4 -> 2; 4 -> 41; 41 -> 40; 41 -> n1 [color="transparent"]; 41 -> n2 [color="transparent"]; 2 -> 1; 2 -> n3[color="transparent"]; 2 -> 3; } ``` ###### 移除元素 2 的節點 依據步驟(這裡以「2」表示元素 2 的節點): - 程式碼找到 2 ,並紀錄沿途走過的路徑 `pathp`,紀錄到 2 的中序下一個的節點 3 - 2 與 3 對調,3 變為紅色,2 變為黑色並且被移除,並從 `pathp` 開始往上修正 ```graphviz digraph 這圖片很難調整的二元樹的拉 { graph [ratio=.44 ordering="out"]; node [style=filled, color=red, shape=circle, width=.6 fontname=Helvetica, fontweight=bold, fontcolor=black, fontsize=36, fixedsize=true, fillcolor=white]; 40, 3; 4, 41, 1, 2 [fillcolor=white, color="black"]; n1, n2, n3 [fontsize=0, fillcolor=white, color=transparent]; 4 -> 3; 4 -> 41; 41 -> 40; 41 -> n1 [color="transparent"]; 41 -> n2 [color="transparent"]; 3 -> 1; 3 -> n3[color="transparent"]; 3 -> 2; } ``` - 往上走訪過程對應到以下程式碼,`pathp->node` 為 3,此時修改程指定的顏色,3 修改為黑色,1 修改為紅色。樹平衡並離開函式。 ```c if (leftleft && rbtn_red_get(x_type, x_field, leftleft)) { /* ... */ }else{ /* || */ /* pathp(r) */ /* / \\ */ /* (b) (b) */ /* / */ /* (b) */ rbtn_red_set(x_type, x_field, left); rbtn_black_set(x_type, x_field, pathp->node); return; } ``` 在使用 GDB 觀察程式碼運作後,會發現這些步驟可以對應 2-3 樹中的行為: - 原本的 2-3 樹,替換並移除 2 後的圖例: ```graphviz digraph g { node [shape = record,height=.1]; node0[label = "<f0> |3|<f1> |4|<f2>"]; node1[label = "<f0> |1|<f1>"]; "node0":f0 -> "node1" node2[label = "<f0> |X|<f1>"]; "node0":f1 -> "node2" node3[label = "<f0> |40|<f1> |41|<f2>"]; "node0":f2 -> "node3" } ``` - 該節點有 underflow,需要進行節點合併,這裡跟左邊節點合併 - 並且將 3 傳入至該合併的節點 ```graphviz digraph g { node [shape = record,height=.1]; node0[label = "<f0> |4|<f1>"]; node1[label = "<f0> |1|<f1> |3|<f2>"]; "node0":f0 -> "node1" node2[label = "<f0> |40|<f1> |41|<f2>"]; "node0":f1 -> "node2" } ``` ### 延伸問題開發出的效能評比程式,分析不同 rbtree 實作 xx