nginx学习笔记(5):高级数据结构ngx_rbtree_t

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值