# 背景
普通的二叉查找樹在極端情況下可退化成鏈表,此時的增刪查效率比較低。平衡的二叉樹(如AVL、紅黑樹等)能較好的解決這個問題。
本文首先介紹了紅黑樹的五個重要性質,然后詳細介紹了紅黑樹重要的兩個操作——插入和刪除的原理。最后將紅黑樹與Linux中虛擬內存的管理進行結合,用代碼展示了紅黑樹插入、刪除的實現過程。
# 紅黑樹的性質
每顆紅黑樹必須滿足的五條性質:
- 節點共有紅、黑兩種顏色
- 根節點是黑色
- 葉子節點是黑色(葉子是NIL節點)
- 若一個節點是紅色,那它的兩個孩子都是黑色(每個葉子到根的路徑上不能有兩個連續的紅色節點)。
- 從任一節點到其子孫的所有簡單路徑都包含相同數目的黑色節點。
# 紅黑樹的查找路徑長度
以上五條性質可以保證任一節點到其葉子的長度不會過長(即樹的高度不至於過高)。具體來說,利用上述性質4和性質5的約束,可保證:
任一節點到其葉子的長度 <= 最短路徑 × 2。
(注意,這里的最短路徑指的是某節點到其葉子的最短路)
證明方法如下:
當某條路徑最短時,這條路徑必然都是由黑色節點構成。當某條路徑長度最長時,這條路徑必然是由紅色和黑色節點相間構成(性質4限定了不能出現兩個連續的紅色節點)。而性質5又限定了從任一節點到其每個葉子節點的所有路徑必須包含相同數量的黑色節點。
此時,在路徑最長的情況下,路徑上紅色節點數量 = 黑色節點數量。該路徑長度為兩倍黑色節點數量,也就是最短路徑長度的2倍。舉例說明一下,請看下圖:
總結:通過證明紅黑樹任一節點其葉子節點的路徑上界,表明紅黑樹大致上是平衡的。
# 紅黑樹的操作
紅黑樹的查找方法與二叉搜索樹完全一樣。所以插入和刪除節點的方法前半部分與二叉搜索樹完全一樣,而后半部分添加了一些為了滿足其性質的操作。
紅黑樹的插入、刪除操作都依賴基本的平衡二叉樹的旋轉操作,這里就不介紹了,可以自行查閱相關資料。
(一)插入
當在一個紅黑樹插入一個節點時必須初始化此節點的顏色為紅色,但如果新節點父節點也為紅色,將會違背紅黑樹的性質:一條路徑上不能出現相鄰的兩個紅色節點。這時就要通過一系列操作來使紅黑樹保持平衡。
情況1:
當前紅黑樹為空,即插入的節點是根節點。此時需要將節點的顏色由紅色變為黑色以滿足性質2。
情況2:
插入節點N的父節點P為黑色,此時滿足性質4和性質5,不需要調整。
情況3:
插入節點N的父節點P是紅色,叔叔節點U也是紅色,由性質4得P和U的父節點G為黑色。
此時由於N和P均為紅色,破壞了性質4,需要進行調整。這種情況下,先將P和U的顏色染成黑色,再將G的顏色染成紅色。此時經過G路徑上的黑色節點的數量不變,性質5仍然滿足。但需要注意的是G染成紅色后,可能和它的父節點形成連續的紅色節點,此時需要遞歸向上調整。
情況4:
某次調整后,子樹中節點N的父節點為紅色,叔叔節點U為黑色。節點N是P的左孩子,且節點P是G的左孩子。
此時對G進行右旋,調整P和G的位置,並交換顏色。使得性質4被滿足。
注意:情況4中的節點N必然不能是新插入的節點,因為其父節點P是紅色,只有P有兩個葉子且均為黑色時才能滿足:樹中根節點到任一葉子的黑色節點數相同(性質5)。所以N必然新節點,但是N所處的為止有可能是調整過后導致。
情況5:
插入節點N的父節點為紅色,叔叔節點U為黑色。節點N是P的右孩子,且節點P是G的左孩子。
此時先對節點P進行左旋,調整N與P的位置。接下來按照情況4來處理,以滿足性質4。
注意:與情況4相同,N節點也必然不是新插入的節點,詳見情況4-注意。
(二)刪除
相較於插入操作,紅黑樹的刪除操作則要更為復雜一些。因為紅黑樹是有序的,所以首先我們要保證刪除某個節點N之后紅黑樹還是有序的。由於其刪除操作過於繁瑣,所以我們將它分為兩個過程:(1)刪除節點、(2)恢復平衡
(1)刪除節點
刪除操作首先要確定待刪除節點有幾個孩子,如果有兩個孩子,則不能直接刪除節點。而是要先找到該節點的前驅(即節點左子樹中最大的節點)或者后繼(即節點右子樹中最小的節點),當然習慣上我們使用后繼而不是前驅,然后將后繼節點的值復制到要刪除的節點中,然后再將原本的后繼節點刪除。
由於后繼節點至多只有一個孩子節點(否則這個節點肯定不是子樹中最大或者最小的),這樣我們就把原來要刪除的節點要修改兩個孩子的問題轉化為只調整一個節點的問題。能這樣做的原因是我們並不關心最終被刪除的節點是否是我們開始想要刪除的那個節點,只要節點里的值最終被刪除了就行,至於樹的結構如何變化,這個不是我們關心的。
相比較之下,若要刪除節點只有一個孩子,那么情況就相對簡單很多了。在下面我會首先分析只有一個孩子的情況。
在展開說明之前,為了方便我們先進行一些節點名稱代號的定義。這里假設最終被刪除的節點為N,其孩子節點為C。后繼節點為S,其左孩子為SL,右孩子為SR。接下來的討論是建立在節點N被刪除,節點S替換N的基礎上進行的。
在上面的基礎上,接下來就可以展開討論了。紅黑樹刪除有6種情況,分別是:
情況1
待刪除節點N最多只有一個孩子節點,這種情況相對簡單。
- 圖1,只有一個孩子,此時N的孩子節點C同時也是后繼節點S。C必然只能是紅色(性質5),則N只能是黑色(性質4)。此時直接將C變成黑色繼承N即可。
- 圖2,直接刪除N即可,不破壞紅黑樹的性質。
- 圖3,直接刪除N后,破壞了性質5,需要進行重新平衡,(2)恢復平衡中會進行講解。
情況2
待刪除節點N有兩個孩子,這時首先需要找到N的后繼節點S,此時又有兩種情況。第一種S是N的右孩子,此時只需要將后繼節點S繼承N的位置,將N的左孩子(如果有)變為S的左孩子即可(S一定沒有左孩子,見下方說明);第二種情況是S不是N的右孩子,說明是N的右子樹中的點。下面我將用例子來說明這兩種情況下判斷是否需要重新平衡的依據是相同的:S是否有右子樹。
S是N的右孩子的情況下,S是否有右子樹的情況分別如下,注意X節點存在的意義就是使初始的紅黑樹是平衡的,所以我們不必要在意N的左子樹的結構。
1)S有右子樹,此時無論其他節點的顏色如果,刪除N后直接使用S替換即可。所有可能的情況如下:
2)S沒有右子樹,此時替換就會產生沖突。
S是N的右子樹中的點,S是否有右子樹的情況分別如下。
1)S有右子樹,此時無論其他節點的顏色如果,刪除N后直接使用S替換即可,所有可能的情況都可以抽象為下面的三種情況。注意此時以下的結構中S和SR的顏色是確定的。
(2)S沒有右子樹,此時替換顯然會產生沖突。
總結(1):
上面描述了刪除操作的第一步——刪除節點。這里有比較多的情況,我們會有疑惑:為什么S沒有右子樹的情況下會發生不平衡呢?
大家可以思考下,我總結規律就是:因為我們使用S替換N時相當於刪除了S節點,如果S節點是葉子節點(即上述沒有右子樹),且S又是一個黑色節點,這時刪掉S必然會破壞紅黑樹的性質5造成不平衡。
注意:后繼節點S不可能有左孩子SL,因為如果有左孩子則它的左孩子更有可能成為待刪除結點N的后繼。因而S結點要不沒有孩子,要不則只有右孩子。
(2)恢復平衡
在(1)刪除節點中,我們得到結論:若找到的后繼結點是葉子節點且顏色是黑色,就需要在替換完成后對紅黑樹進行再平衡。下面我們就針對這種情況進一步分析。
由於篇幅受限,這里僅拿后繼節點S是待刪除結點N的右孩子為例。即如下圖所示:
情況1
后繼節點S其兄弟節點X為紅色,根據性質5,它肯定有兩個黑色的孩子。此類情況做如下的調整:
情況2:
后繼節點S其兄弟節點X為黑色,且有一個左孩子(可以斷定左孩子是紅色的)。此類情況做如下的調整:
情況3
后繼節點S其兄弟節點X為黑色,且有一個右孩子(可以斷定右孩子是紅色的)。此類情況做如下的調整:
情況4
后繼節點S其兄弟節點X為黑色,且有兩個節點(可以斷定,左右節點都是紅色的)。這個和情況2是相同的。
情況5
后繼節點S其兄弟節點X為黑色,且沒有子節點。
此時N的子樹是平衡了,但是刪掉S之后,可能上面的樹發生不平衡。所以需要遞歸向上尋找不平衡的點,例如:
總結(2):
紅黑樹刪除節點之后的恢復平衡操作比較復雜,涉及的情況較多。
結論
紅黑樹在每一次插入和刪除節點之后都會用O(logn)是時間對樹的結構進行調整,以保持樹的平衡。
紅黑樹的插入和刪除操作比一般的二叉樹操作要復雜許多,與紅黑樹的性質密切相關。掌握其插入刪除操作對理解紅黑樹的概念有很大幫助。
# 紅黑樹的應用:Linux內核虛擬內存的管理
Linux系統借助MMU建立了虛擬內存系統,虛擬內存使得進程在系統層面擁有連續的地址空間,更使得進程可以分配到比物理內存更大的運行空間。例如在32位系統上,每個進程理論上可以獲得2^32=4GB的地址空間。
使用紅黑樹有以下好處:
( 1)在紅黑樹中查找一個虛擬內存區域的速度快。使用雙向鏈表查找需要O(n)的時間復雜度,紅黑樹中可以提升到O(logn)。
( 2)增加一個新的區域時,先在紅黑樹中找到剛好在新區域前面的區域,然后向鏈表和樹中插入新區域,可以避免掃描鏈表。
進程的虛擬空間
在Linux內核中,進程的虛擬空間主要有兩個數據結構來描述(定義在mm_types.h文件中):
- mm_strcut結構描述了一個進程的整個虛擬空間
- vm_area_struct結構描述了虛擬地址空間的一個虛擬內存區域(VMA)
一個進程的虛擬地址空間中可能有多個虛擬內存區域(以下簡稱VMA)。在mm_strcut結構中定義了兩個指針:
struct mm_struct {
struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb; /* Red-Black tree of VMAs */
int map_count; /* number of VMAs */
...
}
mmap指向進程的第一個VMA,vm_area_struct中定義了前后指針,所以進程的VMAs組成一個雙向鏈表。VMA使用起始地址和結束地址描述,鏈表按起始地址增序排序;mm_rb指向紅黑樹的根,樹中的每個節點也是一個VMA,在樹中所有的VMA其左孩子指針指向相鄰的低地址VMA,右孩子指向相鄰的高地址VMA。他們倆的組織結構如下圖所示:
vm_area_struct結構體中主要是定義一個VMA的起始和結束的虛擬地址。
struct vm_area_struct {
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb; /* 紅黑樹節點 */
struct mm_struct *vm_mm; /* The address space we belong to. */ /* 此VMA屬於的進程地址空間 */
}
Linux內核中紅黑樹的實現
上面說mm_struct的結構成員mm_rb指向紅黑樹的根節點。紅黑樹的定義在/include/linux/rbtree.h中實現:
struct rb_node {
unsigned long __rb_parent_color; /* 顏色(Red/Black) */
struct rb_node *rb_right; /* The Right Children */
struct rb_node *rb_left; /* The Left Children */
}
struct rb_root {
struct rb_node *rb_node; /* 紅黑樹的根節點 */
};
Linux內核lib/rbtree.c中提供了紅黑樹的相關操作算法及其紅黑樹操作接口:
(一)插入操作的實現
為進程的虛擬空間添加一塊新的VMA,需要執行的操作有:(1)找到VMA插入的正確位置。(2)在紅黑樹中插入該節點。(3)調整紅黑樹以滿足其性質。
(1)尋找位置的實現方法可以拿mm/vmalloc.c中的__insert_vmap_area
函數舉例,rb_link_node
實現了新VMA節點的插入。
完成之后需要插入的節點va->rb_node
的parent指針已經正確設置。但是整棵樹還需要做調整以符合紅黑樹的性質。
struct rb_node **p = &vmap_area_root.rb_node;
struct rb_node *parent = NULL;
struct rb_node *tmp;
while (*p) { /* 尋找新節點插入的位置 */
struct vmap_area *tmp_va;
parent = *p;
tmp_va = rb_entry(parent, struct vmap_area, rb_node); /* 得到紅黑樹節點所屬的VMA */
if (va->va_start < tmp_va->va_end)
p = &(*p)->rb_left;
else if (va->va_end > tmp_va->va_start)
p = &(*p)->rb_right;
else
BUG();
}
rb_link_node(&va->rb_node, parent, p); /* 設置 rb_node 的 parent 指針 */
(2)rb_insert_color
是供外部調用的插入函數,真正實現插入算法的函數是__rb_insert
,該函數同時實現了。
為了與紅黑樹操作理論對應,這里只展示新插入節點N在其祖父節點P的左子樹的情況,右子樹情況對稱同理。
static __always_inline void
__rb_insert(struct rb_node *node, struct rb_root *root,
void (*augment_rotate)(struct rb_node *old, struct rb_node *new))
{
struct rb_node *parent = rb_red_parent(node), *gparent, *tmp;
while (true) {
/*
* Loop invariant: node is red
*
* If there is a black parent, we are done.
* Otherwise, take some corrective action as we don't
* want a red root or two consecutive red nodes.
*/
/* while 循環退出的條件:(1)尋找到根節點 (2)parent 為黑色 */
//---------------------------------------------------------- 情況1 & 情況2
if (!parent) {
rb_set_parent_color(node, NULL, RB_BLACK);
break;
} else if (rb_is_black(parent))
break;
/* 到了這里,node 的父節點顏色一定是 Red */
gparent = rb_red_parent(parent);
tmp = gparent->rb_right;
if (parent != tmp) { /* parent == gparent->rb_left */
if (tmp && rb_is_red(tmp)) {
//-------------------------------------------------- 情況3
/*
* Case 1 - color flips
*
* G g
* / \ / \
* p u --> P U
* / /
* n n
*
* However, since g's parent might be red, and
* 4) does not allow this, we need to recurse
* at g.
*/
rb_set_parent_color(tmp, gparent, RB_BLACK);
rb_set_parent_color(parent, gparent, RB_BLACK);
node = gparent;
parent = rb_parent(node);
rb_set_parent_color(node, parent, RB_RED);
continue;
}
tmp = parent->rb_right;
if (node == tmp) {
//-------------------------------------------------- 情況5
/*
* Case 2 - left rotate at parent
*
* G G
* / \ / \
* p U --> n U
* \ /
* n p
*
* This still leaves us in violation of 4), the
* continuation into Case 3 will fix that.
*/
parent->rb_right = tmp = node->rb_left;
node->rb_left = parent;
if (tmp)
rb_set_parent_color(tmp, parent,
RB_BLACK);
rb_set_parent_color(parent, node, RB_RED);
augment_rotate(parent, node);
parent = node;
tmp = node->rb_right;
}
//------------------------------------------------------ 情況4
/*
* Case 3 - right rotate at gparent
*
* G P
* / \ / \
* p U --> n g
* / \
* n U
*/
gparent->rb_left = tmp; /* == parent->rb_right */
parent->rb_right = gparent;
if (tmp)
rb_set_parent_color(tmp, gparent, RB_BLACK);
__rb_rotate_set_parents(gparent, parent, root, RB_RED);
augment_rotate(gparent, parent);
break;
} else {
... /* parent == gparent->rb_right */
}
}
}
(二)刪除操作的實現
與原理部分的講解結構相同,Liunx內核源碼的刪除操作實現也是分為兩步:(1)刪除節點,判斷是否需要rebalance。(2)重新調整樹的結構,恢復平衡。
(1)刪除節點,使用rebalance變量標記是否需要進行重新調整。代碼在:include/linux/rbtree_augmented.h
中。
static __always_inline struct rb_node *
__rb_erase_augmented(struct rb_node *node, struct rb_root *root,
const struct rb_augment_callbacks *augment)
{
struct rb_node *child = node->rb_right;
struct rb_node *tmp = node->rb_left;
struct rb_node *parent, *rebalance;
unsigned long pc;
if (!tmp) { /* 待刪除節點的左孩子為空 */
//----------------------------------------------------情況 1
/*
* Case 1: node to erase has no more than 1 child (easy!)
*
* Note that if there is one child it must be red due to 5)
* and node must be black due to 4). We adjust colors locally
* so as to bypass __rb_erase_color() later on.
*/
pc = node->__rb_parent_color;
parent = __rb_parent(pc);
__rb_change_child(node, child, parent, root);
if (child) {
/* 待刪除僅有右孩子 */
child->__rb_parent_color = pc;
rebalance = NULL;
} else /* 待刪除節點無孩子 */
rebalance = __rb_is_black(pc) ? parent : NULL;
tmp = parent;
} else if (!child) {
//----------------------------------------------------情況 1
/* Still case 1, but this time the child is node->rb_left */
/* 待刪除僅有左孩子 */
tmp->__rb_parent_color = pc = node->__rb_parent_color;
parent = __rb_parent(pc);
__rb_change_child(node, tmp, parent, root);
rebalance = NULL;
tmp = parent;
} else {
//----------------------------------------------------情況 2
/* 待刪除有兩個孩子節點 child = node->rb_right */
struct rb_node *successor = child, *child2;
tmp = child->rb_left;
if (!tmp) {
/* 后繼節點就是N的右孩子 */
/*
* Case 2: node's successor is its right child
*
* (n) (s)
* / \ / \
* (x) (s) -> (x) (c)
* \
* (c)
*/
parent = successor;
child2 = successor->rb_right;
augment->copy(node, successor);
} else {
/*
* Case 3: node's successor is leftmost under
* node's right child subtree
*
* (n) (s)
* / \ / \
* (x) (y) -> (x) (y)
* / /
* (p) (p)
* / /
* (s) (c)
* \
* (c)
*/
do {
parent = successor;
successor = tmp;
tmp = tmp->rb_left;
} while (tmp); /* 找后繼 */
child2 = successor->rb_right;
WRITE_ONCE(parent->rb_left, child2);
WRITE_ONCE(successor->rb_right, child);
rb_set_parent(child, successor);
augment->copy(node, successor);
augment->propagate(parent, successor);
}
/* 將N的左子樹移植到S節點 */
tmp = node->rb_left;
WRITE_ONCE(successor->rb_left, tmp);
rb_set_parent(tmp, successor);
/* N的父節點與S建立關系 */
pc = node->__rb_parent_color;
tmp = __rb_parent(pc);
__rb_change_child(node, successor, tmp, root);
/* child2 = successor->rb_right */
if (child2) { /* 節點S有右孩子 */
successor->__rb_parent_color = pc;
rb_set_parent_color(child2, parent, RB_BLACK);
rebalance = NULL;
} else {
unsigned long pc2 = successor->__rb_parent_color;
successor->__rb_parent_color = pc;
rebalance = __rb_is_black(pc2) ? parent : NULL;
}
tmp = successor;
}
augment->propagate(tmp, NULL);
return rebalance;
}
(2)恢復平衡。因為替換待刪除結點N的可能是前驅也可能是后繼。當然我們默認情況下都是選擇后繼,但是某些情況可能不存在后繼,此時就必須選擇前驅來替代。但限於篇幅,本文中我們不討論前驅的情況,所以省去了相關代碼。
/* parent 是 successor 的父節點, root 是紅黑樹的根節點 */
static __always_inline void
____rb_erase_color(struct rb_node *parent, struct rb_root *root,
void (*augment_rotate)(struct rb_node *old, struct rb_node *new))
{
struct rb_node *node = NULL, *sibling, *tmp1, *tmp2;
while (true) {
/*
* Loop invariants:
* - node is black (or NULL on first iteration)
* - node is not the root (parent is not NULL)
* - All leaf paths going through parent and node have a
* black node count that is 1 lower than other leaf paths.
*/
sibling = parent->rb_right;
if (node != sibling) { /* node == parent->rb_left */ /* 替換節點是左孩子 */
/* 省略S是左孩子(前驅)的代碼 */
} else { /* 替換節點是右孩子(后繼) */
sibling = parent->rb_left; /* sibling = brother of S */
if (rb_is_red(sibling)) {
//----------------------------------------------- 情況1
/* Case 1 - right rotate at parent */
parent->rb_left = tmp1 = sibling->rb_right;
sibling->rb_right = parent;
rb_set_parent_color(tmp1, parent, RB_BLACK);
__rb_rotate_set_parents(parent, sibling, root,
RB_RED);
augment_rotate(parent, sibling);
sibling = tmp1;
}
tmp1 = sibling->rb_left; /* tmp1 = S的兄弟節點的左孩子 */
if (!tmp1 || rb_is_black(tmp1)) { /* 兄弟節點沒有左孩子 || 左孩子是黑色 */
tmp2 = sibling->rb_right; /* 兄弟節點的右孩子 */
if (!tmp2 || rb_is_black(tmp2)) { /* 右孩子也不存在 || 右孩子是黑色 */
/* Case 2 - sibling color flip */
rb_set_parent_color(sibling, parent,
RB_RED); /* 設置兄弟節點為紅 */
if (rb_is_red(parent))
rb_set_black(parent);
else {
//------------------------------------------ 情況5(遞歸)
node = parent;
parent = rb_parent(node);
if (parent)
continue;
}
break;
}
/* Case 3 - right rotate at sibling */
//-------------------------------------------------- 情況3
sibling->rb_right = tmp1 = tmp2->rb_left;
tmp2->rb_left = sibling;
parent->rb_left = tmp2;
if (tmp1)
rb_set_parent_color(tmp1, sibling,
RB_BLACK);
augment_rotate(sibling, tmp2);
tmp1 = sibling;
sibling = tmp2;
}
/* Case 4 - left rotate at parent + color flips */
//------------------------------------------------------ 情況4
parent->rb_left = tmp2 = sibling->rb_right;
sibling->rb_right = parent;
rb_set_parent_color(tmp1, sibling, RB_BLACK);
if (tmp2)
rb_set_parent(tmp2, parent);
__rb_rotate_set_parents(parent, sibling, root,
RB_BLACK);
augment_rotate(parent, sibling);
break;
}
}
}
# 總結
據我了解紅黑樹不僅是在Linux內核的內存管理中有體現,在Linux的進程調度、JAVA HashMap數據結構的實現中都用到了紅黑樹,感覺還是一個比較重要的知識點。之前一直覺得這個紅黑樹從名字上看是一個比較難的知識點,沒有嘗試去學習。今天正好借助完成圖論大作業的機會,將紅黑樹及其在Linux內核虛擬內存管理的應用相關的知識進行整理記錄下來,既能算是圖論課程的一個拓展作業,也方便我以后用到時再進行查看。