## Linux 核心專題: 平衡二元搜尋樹的研究 > 執行人: ICARUSHERALD96500 [解說影片連結](https://youtu.be/3valbZZZ8gs) ## 任務簡介 研究 Linux 核心的 red-black tree 並探討相關的資料結構。 ## TODO: 探討第 4 周測驗 3 的 XTree > 第 4 周測驗 3 提到 XTree ,將其與紅黑樹 (Linux 核心風格)、AVL 樹進行比較: 量化分析 (含演算法分析: tree height, 要有對應的數學分析) > ### 第四週測驗題 測驗 3 :::danger 不要急著列出程式碼並逐行解釋,你可能會淪為「舉燭」的下場。 你應該揣摩程式設計者的思維,羅列 XTree 的關鍵決策、特性,還有相關的取捨等等。 你是個研究生,就該用有效的研究方法來解釋工程領域的多項事務。 >Xtree 似乎在其他地方並沒有說明該樹的機制,因此原想透過解釋程式碼來說明其實作機制,但我應該用圖說明其左右旋後無法平衡樹高的問題,確實不該逐行解釋。 > $\to$ 提出數學分析,回去翻閱 The Art of Computer Programming 一類的經典著作 > $\to$ 若你因為無法 Google 搜尋到某個東西,然後就無從判斷,這樣還算具備理工素養嗎?我們之所以用高標準要求自己,就是在這樣未知的場合中,得以扎實地拆解、分析、判斷,從而提出改進方案。 > $\to$ 從平衡二元樹的理論開始回顧 ::: **Xtree** 是一種同時兼具 AVL 數與紅黑樹特性的自平衡二元樹樹狀結構,其中一項特點為刪除節點的速度較優,利用 hint 值評估樹平衡與否,且僅在 `xt_node` 被呼叫時更新。下列程式碼說明其移除解點機制 ```c static void __xt_remove(struct xt_node **root, struct xt_node *del) { if (xt_right(del)) { struct xt_node *least = xt_first(xt_right(del)); if (del == *root) *root = least; xt_replace_right(del, AAAA); xt_update(root, BBBB); return; } if (xt_left(del)) { struct xt_node *most = xt_last(xt_left(del)); if (del == *root) *root = most; xt_replace_left(del, CCCC); xt_update(root, DDDD); return; } if (del == *root) { *root = 0; return; } /* empty node */ struct xt_node *parent = xt_parent(del); if (xt_left(parent) == del) xt_left(parent) = 0; else xt_right(parent) = 0; xt_update(EEEE, FFFF); } ``` 首先提及其中所使用的函式說明: * `xt_root(r)` 找出節點 r 的父節點 * `xt_left(n)` 功能為找出節點 n 底下最小的節點,也就是最左邊的極點。巨集定義:`#define xt_left(n) (n->left) ` * `xt_right(n)` 功能為找出節點 n 底下最大的節點,也就是最又邊的節點。巨集定義:`#define xt_right(n) (n->right) ` * `xt_replace_right(n, r)` 使用右子樹的節點 r 來調換原本節點 n * `xt_replace_right(n, l)` 使用左子樹的節點 l 來調換原本節點 n * `xt_update(n, m)` 更新被節點操作後的樹,並根據 `hint` 判定該樹樹否平衡,而決定後續是否進行旋轉。雖然該機制與 AVL tree 雷同。但差異在於 xt_node 之計算方式為其左右節點 `hint` 最大值再 +1,並且僅有在 `xt_update(n,m)` 被呼叫時才更新。而每個節點的 hint 值雖用來判斷樹是否平衡,但是該值卻不用與實際上最長子節點的練長相同。如此一來,便可以減少在每次樹的節點操作後進行更镡時 `xt_update(n, m)` 所需重新計算高度的工作量。 * `xt_rotate_right`: ```c static inline void xt_rotate_left(struct xt_node *n) { struct xt_node *l = xt_left(n), *p = xt_parent(n); xt_parent(l) = xt_parent(n); xt_left(n) = xt_right(l); xt_parent(n) = l; xt_right(l) = n; if (p && xt_left(p) == n) xt_left(p) = l; else if (p) xt_right(p) = l; if (xt_left(n)) xt_lparent(n) = n; } ``` ```graphviz digraph Tree { graph[ordering=out] p -> n n -> l n -> r l -> A l -> B label=origin } ``` ```graphviz digraph Tree { graph[ordering=out] p -> l l -> A l -> n n -> B n -> r } ``` - `xt_rotate_left`: ```graphviz digraph Tree { graph[ordering=out] p -> n n -> l n -> r r -> A r -> B label=origin } ``` ```graphviz digraph Tree { graph[ordering=out] p -> r r -> n r -> B n -> l n -> A } ``` 由上述的兩個操作發現,xtree 不論進行左旋或右旋都無法在情況下平衡樹高。 :::danger 提供相關的數學分析。 ::: XTree 的最壞情況樹高與普通的二元搜尋樹相似,取決於節點的插入順序。在沒有特殊的平衡措施下,最糟糕情況下可能形成一個高度接近於線性的右斜樹。例如,如果按照特定的插入順序導致每次插入的節點都比前一個節點大(或小),則最終形成的樹可能高度接近於 $𝑛$ ,等同於節點的數量。 ### 與 AVL tree 、 rbtree 比較 **AVL tree** 為確保平衡,利用平衡因子判斷其子樹高差距,再決定是否旋轉。當為平衡時,必滿足以下條件:$左子樹高 - 右子樹高 \in \left(-1,0,1\right)$ 。xtree 與 AVL tree 類似,利用 `hint` 紀錄子樹高,在 `xt_update` 時計算高度差,當左右差異大於一時,進行旋轉。但從程式碼中可以發現,其僅對平衡因子 `b` 進行大於一或小於一的判斷,進而右旋或左旋。AVL tree 相較之下,有 LL、RR、LR、RL 四種旋轉方式。利用前面提及 xtree 單純使用左或右旋無法解決平衡樹高的情境下。僅需要使用右左或左右旋便可解決。 初始結構: ```graphviz digraph Tree { graph[ordering=out] p->n p->r n->A n->l l->B label=origin } ``` 先對 n 左旋: ```graphviz digraph Tree { graph[ordering=out] p->l p->r l->n l->B n->A } ``` 再對 p 右旋: ```graphviz digraph Tree { graph[ordering=out] l->n l->p n->a p->b p->r } ``` 如此一來,變完成樹高平衡。由於樹高對搜尋的效能有所影響,因此簡單討論 AVL tree 的樹高。在平衡的情況下,假設樹高為 h,n(h)為節點最少的情況下樹高與節點的數量關係:$n(h) = n(h-1)+n(h-2)+1$ 在 AVL tree 樹高。可以發現該關係式與費氏數列相似,因此使用該特性推算第 k 項的數值:$\phi^k/\sqrt{5}$。基於該相似特性,推算 $n(h)$ 也是如此,只不過是把 $\sqrt5$ 代換為其他常數,$n(h)\approx k\phi^h$。即便再糟糕的情況下都滿足 $\phi ^h\leq n$,取對數後樹高即為 $h\leq log(n)/log(\phi)\approx1.44\times log(n)$。 **rbtree** 規則: 1. 節點只能是紅或黑色 2. 根節點必為黑色 3. 葉節點必為黑色 4. 紅色節點的子節點必為黑色 5. 從任一節點到其葉節點,途中路徑上的黑色節點數目相同 滿足上述規則的二元樹,相比一般的二元樹更能保持平衡性,往後套用二元樹的算法來查找資料時能更快速、方便地到達目的地,套用運算的時間複雜度為 O(log n),這是因為紅黑樹保證最長路徑不會超過最短路徑的兩倍,最短路徑為葉節點到根節點路徑上全部的節點均為黑色,而最長路徑為黑紅相間。 為了 rbtree 在節點操作後,仍滿足 rbtree 的條件。因此,有'變色'與'旋轉'兩種操作。在樹高上,rbtree 與 AVL tree 西相比,因為後者使用平衡因子所以對樹高差異限制嚴格,樹高為 $1.44\times log(n)$。反觀 rbtree 平衡機制是利用上述的五條規則,因此樹高較高。因為最長的路徑,可以視為在最短路徑(即路徑上節點均為黑色)間隔插入紅色節點。在 rbtree 樹高上,假設黑高為 bh,那該數至少包含 $2^{bh}-1$ 個節點(即不存在紅色節點)。利用紅色節點不會連續的條件,可以視為紅色節點被間隔插入,故最高樹高為兩倍的黑高($h=2 \times bh$)。假設一顆 rbtree 有 n 個節點,則 $2^{bh}-1\leq n$,重新整理後可得。 $bh \leq log(n+1)$,故在樹高在最糟的情況下 $h \leq 2 \times log(n+1)$。在 $n$ 極大的情況下便近似為 $2 \times log(n)$。 對比 rbtree 與 AVL tree 在樹高上的差異後,rbtree 因為平衡規則較寬鬆,樹高較高。所衍伸的缺點就是雖然在平均狀況下複雜度仍為 $log(n)$,但最糟的情況下畢竟樹高較高,搜尋的速度稍較 AVL tree 慢。 xtree 因為前面與 AVL tree 作比較時發現,其無法有效的利用程式碼原始的單純左右旋達到平衡樹高,容易出現歪斜的情況,從而增加樹高,減慢在最糟的情況下的搜尋速度。 ## TODO: 提出 XTree 的改進方案 > 從資料結構的設計,探討其改進方案,融合 AVL 和 rbtree 的特性,提供對應的實驗 xtree 原始程式碼中,不如 AVL tree 有四種旋轉,僅存在兩種旋轉。在遭遇上述所提及的部份樹狀結構時,無法平衡樹狀結構。對以下程式碼修改,使的 xtree 同樣擁有 4 種旋轉: ```diff if (b < -1) { /* leaning to the right */ + if (xt_balance(xt_right(n)) > 0) + xt_rotate_left(xt_right(n)); if (n == *root) *root = xt_right(n); xt_rotate_right(n); } else if (b > 1) { /* leaning to the left */ + if (xt_balance(xt_left(n)) < 0) + xt_rotate_right(xt_left(n)); if (n == *root) *root = xt_left(n); xt_rotate_left(n); } ``` 當從 `xt_balance` 回傳左右子樹高的差值 b,除了原始程式碼中判斷其是否在 -1 到 +1 之間外,還必須進一部判斷該節點的子樹是否需要先旋轉。例如當 `b<-1` 也就是左子樹高小於右子樹高的情況下,還需要進一步判定其右子樹是否需要先被旋轉。也就是 `xt_balance(xt_right(n))>0` 條件為'真'時,代表 `xt_right(n)` 的右子樹較高,需要先對其左旋。最後再對 n 右旋。反之亦然。 對於為什麼認為樹高不平衡對於 xtree 來說是缺點? 是以搜索的角度切入,若是一顆 complete binary tree 其搜索複雜度為 $O(log(n))$。對於原本的 xtree,其樹高會如同上面拿 xtree 與 AVL tree、rbtree 闡述的,隨節點插入,該樹會越發歪斜,而越深則搜索越慢。但若是為了如 AVL tree 為了維持子樹高平衡,就需要加入更多機制在插入節點時進行檢查與旋轉,操作的成本就會增加。而因為 xtree 這些機制較少,因此插入節點的成本較低。所以我推測 xtree 的設計初衷是為了因應需要進行大量節點插入/刪除操作,而捨棄搜索速度所做出的權衡。 :::danger 補上數學分析 ::: 在上述的程式碼修改後,xtree 的樹高平衡機制幾乎無異於 AVL tree,差別僅在於 xtree 是透過在每一節點利用 `hint` 紀錄樹高,而不是在一開始就將左右子樹高的相差先計算好。並且僅在 `xt_node` 被呼叫時進行平衡狀態更新。有了以上對數高平衡的修正,xtree 的結後會趨近 complete binary tree,其中,對於該樹的第 $i$ 個節點會在第 $floor(\log_2(i))$ 層。而對於邊條數量計算上,深度為 $d$ 的節點從根結點到該節點會共有 $d$ 個邊條,也就是說可以透過計算第 $i$ 個節點的深度,推溯出跟節點到第 $i$ 節點的邊數為 $floor(\log_2(i))$。所以總共 $n$ 個節點就會有 $\sum^{n}_{i=1} floor(\log_2(i)) = O(n \times log(n))$ 個邊條。 下面圖解其機制,首先考慮初始狀態的 xtree ```graphviz digraph Tree { graph[ordering=out] n->xt_left n->xt_right xt_left->A xt_left->B B->C } ``` 使用 `xt_balance(n)` 可以獲得左右子樹高的相差,即平衡因子,在這裡為 $3-1=2$,沒有落在正負 1 的範圍。引此 `b>1` 判斷式為真。進入 `xt_balance(xt_left(n)<0)` 的判斷當中。而 `xt_left` 節點的子樹高所得的平衡一因子為:$1-2=-1$,該判斷式同為真。故觸發 `xt_rotate_right(xt_left)` 對 `xt_left` 進行左玄。完成該步驟的 xtree 如下: ```graphviz digraph Tree { graph[ordering=out] n->B n->xt_right xt_left->A B->xt_left B->C } ``` 最後再進行 `xt_rotate_right(n)` n 右旋: ```graphviz digraph Tree { graph[ordering=out] n->C n->xt_right xt_left->A B->xt_left B->n } ``` 這樣變可以完成與 AVL tree 相同的旋轉機制,以確保結構平衡。