Linux 中的紅黑樹 (rbtree)

日期:

2007 年 1 月 18 日

作者:

Rob Landley <rob@landley.net>

什麼是紅黑樹,它有什麼用?

紅黑樹是一種自平衡二叉搜尋樹,用於儲存可排序的鍵/值資料對。這與基數樹(用於高效儲存稀疏陣列,從而使用長整數索引來插入/訪問/刪除節點)和雜湊表(不保持有序以便按順序遍歷,並且必須針對特定大小和雜湊函式進行調優,而紅黑樹可以優雅地擴充套件以儲存任意鍵)不同。

紅黑樹與 AVL 樹相似,但在插入和刪除方面提供更快的即時有界最壞情況效能(分別最多兩次旋轉和三次旋轉以平衡樹),查詢時間稍慢(但仍為 O(log n))。

引用 Linux Weekly News 的話

核心中使用了許多紅黑樹。deadline 和 CFQ I/O 排程器使用紅黑樹來跟蹤請求;資料包 CD/DVD 驅動程式也這樣做。高精度計時器程式碼使用紅黑樹來組織未完成的計時器請求。ext3 檔案系統在紅黑樹中跟蹤目錄條目。虛擬記憶體區域 (VMAs) 使用紅黑樹進行跟蹤,epoll 檔案描述符、加密金鑰以及“分層令牌桶”排程器中的網路資料包也是如此。

本文件涵蓋 Linux 紅黑樹實現的用法。有關紅黑樹的性質和實現的更多資訊,請參閱

Linux Weekly News 關於紅黑樹的文章

https://lwn.net/Articles/184495/

維基百科紅黑樹條目

https://zh.wikipedia.org/wiki/紅黑樹

紅黑樹的 Linux 實現

Linux 的紅黑樹實現位於“lib/rbtree.c”檔案中。要使用它,請“#include <linux/rbtree.h>”。

Linux 紅黑樹實現經過速度最佳化,因此比更傳統的樹實現少了一層間接(並具有更好的快取區域性性)。它不使用指向單獨的 `rb_node` 和資料結構的指標,而是將 `struct rb_node` 的每個例項嵌入到它組織的資料結構中。並且不使用比較回撥函式指標,而是期望使用者編寫自己的樹搜尋和插入函式,這些函式呼叫提供的紅黑樹函式。鎖定也由紅黑樹程式碼的使用者負責。

建立新的紅黑樹

紅黑樹中的資料節點是包含 `struct rb_node` 成員的結構體

struct mytype {
      struct rb_node node;
      char *keystring;
};

當處理嵌入式 `struct rb_node` 的指標時,可以使用標準 `container_of()` 宏訪問包含的資料結構。此外,可以透過 `rb_entry(node, type, member)` 直接訪問單個成員。

每個紅黑樹的根是一個 `rb_root` 結構體,它透過以下方式初始化為空:

struct rb_root mytree = RB_ROOT;

在紅黑樹中搜索值

為你的樹編寫搜尋函式相當簡單:從根開始,比較每個值,並根據需要遵循左分支或右分支。

示例

struct mytype *my_search(struct rb_root *root, char *string)
{
      struct rb_node *node = root->rb_node;

      while (node) {
              struct mytype *data = container_of(node, struct mytype, node);
              int result;

              result = strcmp(string, data->keystring);

              if (result < 0)
                      node = node->rb_left;
              else if (result > 0)
                      node = node->rb_right;
              else
                      return data;
      }
      return NULL;
}

將資料插入紅黑樹

在樹中插入資料首先涉及搜尋新節點的插入位置,然後插入節點並重新平衡(“重新著色”)樹。

插入搜尋與先前的搜尋不同,它需要找到要嫁接新節點的指標位置。新節點還需要一個指向其父節點的連結,以用於重新平衡。

示例

int my_insert(struct rb_root *root, struct mytype *data)
{
      struct rb_node **new = &(root->rb_node), *parent = NULL;

      /* Figure out where to put new node */
      while (*new) {
              struct mytype *this = container_of(*new, struct mytype, node);
              int result = strcmp(data->keystring, this->keystring);

              parent = *new;
              if (result < 0)
                      new = &((*new)->rb_left);
              else if (result > 0)
                      new = &((*new)->rb_right);
              else
                      return FALSE;
      }

      /* Add new node and rebalance tree. */
      rb_link_node(&data->node, parent, new);
      rb_insert_color(&data->node, root);

      return TRUE;
}

在紅黑樹中刪除或替換現有資料

要從樹中刪除現有節點,請呼叫

void rb_erase(struct rb_node *victim, struct rb_root *tree);

示例

struct mytype *data = mysearch(&mytree, "walrus");

if (data) {
      rb_erase(&data->node, &mytree);
      myfree(data);
}

要用一個具有相同鍵的新節點替換樹中的現有節點,請呼叫

void rb_replace_node(struct rb_node *old, struct rb_node *new,
                      struct rb_root *tree);

以這種方式替換節點不會重新排序樹:如果新節點與舊節點沒有相同的鍵,紅黑樹可能會損壞。

迭代紅黑樹中儲存的元素(按排序順序)

提供了四個函式,用於按排序順序迭代紅黑樹的內容。這些函式適用於任意樹,不應需要修改或封裝(除了鎖定目的)

struct rb_node *rb_first(struct rb_root *tree);
struct rb_node *rb_last(struct rb_root *tree);
struct rb_node *rb_next(struct rb_node *node);
struct rb_node *rb_prev(struct rb_node *node);

要開始迭代,呼叫 `rb_first()` 或 `rb_last()` 並傳入樹根的指標,這將返回指向樹中第一個或最後一個元素中包含的節點結構的指標。要繼續,透過在當前節點上呼叫 `rb_next()` 或 `rb_prev()` 來獲取下一個或上一個節點。當沒有更多節點時,這將返回 NULL。

迭代器函式返回指向嵌入式 `struct rb_node` 的指標,可以利用 `container_of()` 宏訪問其包含的資料結構,並可以透過 `rb_entry(node, type, member)` 直接訪問單個成員。

示例

struct rb_node *node;
for (node = rb_first(&mytree); node; node = rb_next(node))
      printk("key=%s\n", rb_entry(node, struct mytype, node)->keystring);

快取紅黑樹

計算最左(最小)節點對於二叉搜尋樹來說是一項非常常見的任務,例如用於遍歷或依賴特定順序進行自身邏輯的使用者。為此,使用者可以使用 'struct rb_root_cached' 將 O(logN) 的 `rb_first()` 呼叫最佳化為簡單的指標獲取,避免潛在昂貴的樹迭代。這在維護上執行時開銷可忽略不計;儘管記憶體佔用較大。

與 `rb_root` 結構類似,快取的紅黑樹透過以下方式初始化為空:

struct rb_root_cached mytree = RB_ROOT_CACHED;

快取紅黑樹只是一個普通的 `rb_root`,額外有一個指標用於快取最左節點。這使得 `rb_root_cached` 可以在 `rb_root` 存在的任何地方存在,從而允許支援增強樹以及一些額外的介面

struct rb_node *rb_first_cached(struct rb_root_cached *tree);
void rb_insert_color_cached(struct rb_node *, struct rb_root_cached *, bool);
void rb_erase_cached(struct rb_node *node, struct rb_root_cached *);

插入和刪除呼叫都有其各自的增強樹對應物

void rb_insert_augmented_cached(struct rb_node *node, struct rb_root_cached *,
                                bool, struct rb_augment_callbacks *);
void rb_erase_augmented_cached(struct rb_node *, struct rb_root_cached *,
                               struct rb_augment_callbacks *);

支援增強紅黑樹

增強紅黑樹是一種紅黑樹,每個節點中儲存了一些附加資料,其中節點 N 的附加資料必須是根植於 N 的子樹中所有節點內容的一個函式。這些資料可用於為紅黑樹增強一些新功能。增強紅黑樹是在基本紅黑樹基礎設施之上構建的可選功能。需要此功能的紅黑樹使用者在插入和刪除節點時,必須呼叫增強函式並提供使用者定義的增強回撥。

實現增強紅黑樹操作的 C 檔案必須包含 <linux/rbtree_augmented.h>,而不是 <linux/rbtree.h>。請注意,linux/rbtree_augmented.h 暴露出一些你本不應依賴的紅黑樹實現細節;請堅持那裡文件化的 API,並且也不要從標頭檔案中包含 <linux/rbtree_augmented.h>,以儘量減少你的使用者意外依賴這些實現細節的可能性。

在插入時,使用者必須更新導致插入節點的路徑上的增強資訊,然後像往常一樣呼叫 `rb_link_node()` 並呼叫 `rb_augment_inserted()` 而不是通常的 `rb_insert_color()` 呼叫。如果 `rb_augment_inserted()` 重新平衡紅黑樹,它將回調到使用者提供的函式中以更新受影響子樹上的增強資訊。

刪除節點時,使用者必須呼叫 `rb_erase_augmented()` 而不是 `rb_erase()`。 `rb_erase_augmented()` 會回撥使用者提供的函式以更新受影響子樹上的增強資訊。

在這兩種情況下,回撥都透過 `struct rb_augment_callbacks` 提供。必須定義 3 個回撥:

  • 一個傳播回撥,它更新給定節點及其祖先的增強值,直到給定的停止點(或 NULL 表示一直更新到根)。

  • 一個複製回撥,它將給定子樹的增強值複製到新分配的子樹根。

  • 一個樹旋轉回調,它將給定子樹的增強值複製到新分配的子樹根,並重新計算原子樹根的增強資訊。

`rb_erase_augmented()` 的編譯程式碼可能會內聯傳播和複製回撥,這導致函式體很大,因此每個增強紅黑樹使用者應只有一個 `rb_erase_augmented()` 呼叫站點,以限制編譯程式碼大小。

使用示例

區間樹是增強紅黑樹的一個例子。參考——Cormen、Leiserson、Rivest 和 Stein 的《演算法導論》。有關區間樹的更多細節:

經典紅黑樹只有一個鍵,不能直接用於儲存像 [lo:hi] 這樣的區間範圍,也無法快速查詢與新 lo:hi 的任何重疊或查詢新 lo:hi 的精確匹配。

然而,紅黑樹可以增強以結構化方式儲存此類區間範圍,從而可以進行高效的查詢和精確匹配。

每個節點中儲存的“額外資訊”是其所有後代節點中的最大 hi (max_hi) 值。此資訊只需檢視節點及其直接子節點即可在每個節點處維護。這將用於 O(log n) 查詢最低匹配(所有可能匹配中最低的起始地址),例如:

struct interval_tree_node *
interval_tree_first_match(struct rb_root *root,
                          unsigned long start, unsigned long last)
{
      struct interval_tree_node *node;

      if (!root->rb_node)
              return NULL;
      node = rb_entry(root->rb_node, struct interval_tree_node, rb);

      while (true) {
              if (node->rb.rb_left) {
                      struct interval_tree_node *left =
                              rb_entry(node->rb.rb_left,
                                       struct interval_tree_node, rb);
                      if (left->__subtree_last >= start) {
                              /*
                               * Some nodes in left subtree satisfy Cond2.
                               * Iterate to find the leftmost such node N.
                               * If it also satisfies Cond1, that's the match
                               * we are looking for. Otherwise, there is no
                               * matching interval as nodes to the right of N
                               * can't satisfy Cond1 either.
                               */
                              node = left;
                              continue;
                      }
              }
              if (node->start <= last) {              /* Cond1 */
                      if (node->last >= start)        /* Cond2 */
                              return node;    /* node is leftmost match */
                      if (node->rb.rb_right) {
                              node = rb_entry(node->rb.rb_right,
                                      struct interval_tree_node, rb);
                              if (node->__subtree_last >= start)
                                      continue;
                      }
              }
              return NULL;    /* No match */
      }
}

插入/刪除是使用以下增強回撥定義的:

static inline unsigned long
compute_subtree_last(struct interval_tree_node *node)
{
      unsigned long max = node->last, subtree_last;
      if (node->rb.rb_left) {
              subtree_last = rb_entry(node->rb.rb_left,
                      struct interval_tree_node, rb)->__subtree_last;
              if (max < subtree_last)
                      max = subtree_last;
      }
      if (node->rb.rb_right) {
              subtree_last = rb_entry(node->rb.rb_right,
                      struct interval_tree_node, rb)->__subtree_last;
              if (max < subtree_last)
                      max = subtree_last;
      }
      return max;
}

static void augment_propagate(struct rb_node *rb, struct rb_node *stop)
{
      while (rb != stop) {
              struct interval_tree_node *node =
                      rb_entry(rb, struct interval_tree_node, rb);
              unsigned long subtree_last = compute_subtree_last(node);
              if (node->__subtree_last == subtree_last)
                      break;
              node->__subtree_last = subtree_last;
              rb = rb_parent(&node->rb);
      }
}

static void augment_copy(struct rb_node *rb_old, struct rb_node *rb_new)
{
      struct interval_tree_node *old =
              rb_entry(rb_old, struct interval_tree_node, rb);
      struct interval_tree_node *new =
              rb_entry(rb_new, struct interval_tree_node, rb);

      new->__subtree_last = old->__subtree_last;
}

static void augment_rotate(struct rb_node *rb_old, struct rb_node *rb_new)
{
      struct interval_tree_node *old =
              rb_entry(rb_old, struct interval_tree_node, rb);
      struct interval_tree_node *new =
              rb_entry(rb_new, struct interval_tree_node, rb);

      new->__subtree_last = old->__subtree_last;
      old->__subtree_last = compute_subtree_last(old);
}

static const struct rb_augment_callbacks augment_callbacks = {
      augment_propagate, augment_copy, augment_rotate
};

void interval_tree_insert(struct interval_tree_node *node,
                          struct rb_root *root)
{
      struct rb_node **link = &root->rb_node, *rb_parent = NULL;
      unsigned long start = node->start, last = node->last;
      struct interval_tree_node *parent;

      while (*link) {
              rb_parent = *link;
              parent = rb_entry(rb_parent, struct interval_tree_node, rb);
              if (parent->__subtree_last < last)
                      parent->__subtree_last = last;
              if (start < parent->start)
                      link = &parent->rb.rb_left;
              else
                      link = &parent->rb.rb_right;
      }

      node->__subtree_last = last;
      rb_link_node(&node->rb, rb_parent, link);
      rb_insert_augmented(&node->rb, root, &augment_callbacks);
}

void interval_tree_remove(struct interval_tree_node *node,
                          struct rb_root *root)
{
      rb_erase_augmented(&node->rb, root, &augment_callbacks);
}