nginx學習筆記(5):高階資料結構ngx_rbtree_t

li27z發表於2017-03-12

ngx_rbtree_t是使用紅黑樹實現的一種關聯容器,nginx的核心模組(如定時器管理、檔案快取模組等)在需要快速檢索、查詢的場合下都使用了ngx_rbtree_t容器。

什麼是紅黑樹

在介紹ngx_rbtree_t之前,我們先來了解一下紅黑樹的相關知識。

紅黑樹實際上是一種自平衡二叉查詢樹,那麼自平衡、二叉樹、二叉查詢樹又是什麼呢?

二叉樹是每個節點最多有兩個子樹的樹結構,每個節點都可用於儲存資料,可以由任一個節點訪問它的左右子樹或者父節點。

二叉查詢樹或者是一棵空樹,或者是具有下列性質的二叉樹:
1)每個節點都有一個作為查詢依據的值,所有節點的值互不相同;
2)若左子樹不空,則左子樹上所有節點的值均小於它的根結點的值;
3)若右子樹不空,則右子樹上所有節點的值均大於它的根結點的值;
4)左、右子樹也分別為二叉排序樹;
(注:有些二叉查詢樹的定義中允許有相同值的節點存在)
這樣,一棵二叉查詢樹的所有元素節點都是有序的。

關於自平衡的概念,我們通過一個例子來理解。
我們知道,一般情況下,二叉查詢樹的查詢複雜度是與目標節點到根節點的距離(即深度)有關的。然而,不斷地增加、刪除節點,可能造成二叉查詢樹形態非常不平衡,在極端情形下它會變成單連結串列,檢索效率也就會變得低下。

例如,依次將1、6、8、11、13、15、17、22、25、27新增到一棵普通的空二叉查詢樹中,它的最終形態如下圖:
這裡寫圖片描述

其最終形態相當於單連結串列了,由於樹的深度太大,因此各種操作的效率都會很低下。

什麼是自平衡二叉查詢樹?在不斷地向二叉查詢樹中新增、刪除節點時,二叉查詢樹自身通過形態的變換,始終保持著一定程度上的平衡,即為自平衡二叉查詢樹。自平衡二叉查詢樹只是一個概念,它有許多種不同的實現方式,如AVL樹和紅黑樹。

回到我們所討論的主角——紅黑樹,紅黑樹除了符合二叉查詢樹的一般要求外,它還有如下的額外的特性:
1)節點是紅色或黑色;
2)根節點是黑色;
3)所有葉子節點都是黑色(葉子節點是NIL節點,也叫“哨兵”);
4)每個紅色節點的兩個子節點都是黑色(每個葉子節點到根節點的所有路徑上不能有兩個連續的紅色節點);
5)從任一節點到其每個葉子節點的所有簡單路徑都包含相同數目的黑色節點。

這些約束加強了紅黑樹的關鍵性質從根節點到葉子節點的最大可能路徑長度不大於最短可能路徑的兩倍,這樣這個樹大致上就是平衡的了。特性4)實際上決定了1個路徑不能有兩個毗鄰的紅色節點,那麼最短的可能路徑都是黑色節點,最長的可能路徑有交替的紅色節點和黑色節點。根據特性5)可知,所有最長的路徑都有相同數目的黑色節點,這就表明了沒有路徑能大於其他路徑長度的兩倍。

仍以新增上述元素為例,將其按順序新增到空的ngx_rbtree_t紅黑樹容器中,紅黑樹的最終形態如下圖:
這裡寫圖片描述

它的形態相對平衡,滿足紅黑樹的5個特性,最長路徑長度不大於最短路徑的2倍。
(ngx_rbtree_t紅黑樹在發現自身滿足不了上述的特性時,便會通過旋轉子樹來使樹達到平衡)

紅黑樹的使用方法

1.ngx_rbtree_node_t結構體
ngx_rbtree_node_t結構體用來表示紅黑樹中的一個節點,是紅黑樹實現中必須用到的資料結構,其定義如下:

typedef ngx_uint_t ngx_rbtree_key_t;

typedef struct ngx_rbtree_node_s  ngx_rbtree_node_t;

struct ngx_rbtree_node_s {
    // key成員是每個紅黑樹節點的關鍵字,它必須是整型。紅黑樹的排序主要依據就是key成員
    ngx_rbtree_key_t       key;  //無符號整型的關鍵字
    ngx_rbtree_node_t     *left;  // 左子節點
    ngx_rbtree_node_t     *right;  // 右子節點
    ngx_rbtree_node_t     *parent;  // 父節點
    u_char                 color;  // 節點的顏色,0表示黑色,l表示紅色
    u_char                 data;  // 僅1個位元組的節點資料。由於表示的空間太小,所以一般很少使用
};

一般我們把ngx_rbtree_node_t放到結構體的第一個成員中,這樣方便把自定義的結構體強制轉換成ngx_rbtree_node_t型別。例如,如果希望容器中元素的資料型別是TestRBTreeNode,那麼只需要在第1個成員中放上ngx_rbtree_node_t型別的node即可:

typedef struct {
    // 一般都將ngx_rbtree_node_t節點結構體放在自走義資料型別的第1位,以方便型別的強制轉換 
    ngx_rbtree_node_t node;
    ngx_uint_t num;
} TestRBTreeNode;

在呼叫ngx_rbtree_t容器所提供的方法時,需要的引數都是ngx_rbtree_node_t型別,這時將TestRBTreeNode型別的指標強制轉換成ngx_rbtree_node_t即可。

2.ngx_rbtree_t結構體
紅黑樹容器由ngx_rbtree_t結構體承載,ngx_rbtree_t結構體的定義如下:

typedef struct ngx_rbtree_s ngx_rbtree_t;

// 為解決不同節點含有相同關鍵字的元素衝突問題,紅黑樹設定了ngx_rbtree_insert_pt指標,這樣就可以靈活地新增衝突元素
typedef void (*ngx_rbtree_insert_pt) (ngx_rbtree_node_t *root,
    ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel); 

struct ngx_rbtree_s {
    ngx_rbtree_node_t     *root;      // 指向樹的根節點。注意,根節點也是資料元素
    ngx_rbtree_node_t     *sentinel;  // 指向NIL哨兵節點
    ngx_rbtree_insert_pt   insert;    // 表示紅黑樹新增元素的函式指標,它決定在新增新節點時的行為究竟是替換還是新增
};

ngx_rbtree_insert_pt型別的insert成員的意義在哪裡呢?

紅黑樹是一個通用的資料結構,它的節點(或者稱為容器的元素)可以是包含基本紅黑樹節點的任意結構體。對於不同的結構體,很多場合下是允許不同的節點擁有相同的關鍵字的。例如,不同的字串可能會雜湊出相同的關鍵字,這時它們在紅黑樹中的關鍵字是相同的,然而它們又是不同的節點,這樣在新增時就不可以覆蓋原有同名關鍵位元組點,而是作為新插入的節點存在。因此,在新增元素時,需要考慮到這種情況。將新增元素的方法抽象出ngx_rbtree_insert_pt函式指標可以很好地實現這一思想,使用者也可以靈活地定義自己的行為。Nginx幫助使用者實現了3種簡單行為的新增節點方法,如下表:
這裡寫圖片描述

以ngx_str_rbtree_insert_value為例,其節點的識別符號是字串,紅黑樹第一排序依據仍是節點的key關鍵字,第二排序依據則是節點的字串。因此,使用ngx_str_rbtree_insert_value時表示紅黑樹節點的結構體必須是ngx_str_node_t:

typedef struct {
    ngx_rbtree_node_t  node;
    ngx_str_t          str;
} ngx_str_node_t;

3.紅黑樹容器、紅黑樹節點的操作方法
紅黑樹容器操作的方法如下表:
這裡寫圖片描述

在初始化紅黑樹時,需要先分配好儲存紅黑樹的ngx_rbtree_t結構體,以及ngx_rbtree_node_t型別的哨兵節點,並選擇或者自定義ngx_rbtree_insert_pt型別的節點新增函式。

對於紅黑樹的每個節點來說,它們都具備下表中的7個方法:
這裡寫圖片描述

紅黑樹使用示例

1)初始化
首先分配rbtree紅黑樹容器結構體以及哨兵節點sentinel,本例以key關鍵字作為每個節點的唯一標識,這樣就可以採用預設的ngx_rbtree_insert_value方法了,然後呼叫ngx_rbtree_init方法初始化紅黑樹:

ngx_rbtree_t rbtree;
ngx_rbtree_node_t sentinel;
ngx_rbtree_init(&rbtree, &sentinel, ngx_rbtree_insert_value);

2)新增元素
本例的結構體採用上述自定義的TestRBTreeNode結構體,向紅黑樹中依次新增1、6、8、11、13、15、17、22、25、27:

TestRBTreeNode rbTreeNode[10];
rbTreeNode[0].num = 1;
rbTreeNode[1].num = 6;
rbTreeNode[2].num = 8;
rbTreeNode[3].num = 11;
rbTreeNode[4].num = 13;
rbTreeNode[5].num = 15;
rbTreeNode[6].num = 17;
rbTreeNode[7].num = 22;
rbTreeNode[8].num = 25;
rbTreeNode[9].num = 27;
for(i = 0; i < 10; i++)
{
    rbTreeNode[i].node.key = rbTreeNode[i].num;
    ngx_rbtree_insert(&rbtree, &rbTreeNode[i].node);
}

3)節點操作

// 找出當前紅黑樹中最小的節點,引數不使用根節點,使用任一節點也是可以的
ngx_rbtree_node_t *tmpnode = ngx_rbtree_min(rbtree.root, &sentinel);
// 檢索節點,例如尋找key為13的節點
ngx_uint_t lookupkey = 13;
tmpnode = rbtree.root;
TestRBTreeNode *lookupNode;
while (tmpnode != &sentinel) {
    if (lookupkey != tmpnode->key) {
        // 根據key關鍵字與當前節點的大小比較,決定是檢索左子樹還是右子樹
        tmpnode = (lookupkey < tmpnode->key) ? tmpnode->left: tmpnode->right;
        continue:
    }
    // 找到了值為13的樹節點
    lookupNode = (TestRBTreeNode*) tmpnode;
    break;
}
// 刪除節點,例如刪除剛剛找到的節點
ngx_rbtree_delete(&rbtree, &lookupNode->node);

參考資料:
陶輝.深入理解Nginx 模組開發與架構解析.北京:機械工業出版社,2013

相關文章