nginx源代码分析 - 红黑树实现

在读代码之前需要先了解红黑树的特性以及插入,删除算法,插入比较简单,删除比较麻烦,以下是算法和代码的分析


红黑树是2-3树的变形

1. 每个节点要么是红的,要么是黑的。
2. 根节点是黑的
3. 叶节点是黑的(叶节点是NIL节点,空节点)
4. 红节点的两个孩子都是黑的
5. 任意节点,从这个节点出发,到叶节点的每一条路径上,黑节点数目都相同

一般总是插入红色的节点,因为这样可以在插入过程中尽量避免对树的调整。

情况1:插入的是根节点。
    直接把此节点涂为黑色

情况2:插入的节点的父节点是黑色。
    什么也不做。

情况3:当前节点的父节点是红色且祖父节点的另一个子节点(叔叔节点)是红色。
    祖父节点一定存在,否则插入前已经不是红黑树;父节点是祖父节点的左子或者右子;当前节点是父节点的左子或右子;
    都这样处理。
    将当前节点的父节点和叔叔节点涂黑,祖父节点涂红,把当前节点指向祖父节点,从新的当前节点(祖父节点)重新开始算法。
    
    简单的说就是如果某个节点的颜色在调整过程中被变成了红色,就要将它看做新节点,来进行分析判断并进行调整处理。

情况4:当前节点的父节点是红色,叔叔节点是黑色或者不存在,当前节点是其父节点的右子
    当前节点的父节点做为新的当前节点,以新当前节点为支点左旋。转换为情况5

情况5:当前节点的父节点是红色,叔叔节点是黑色或者不存在,当前节点是其父节点的左子
    父节点变为黑色,祖父节点变为红色,在祖父节点为支点右旋

    
删除的节点的方法与常规二叉搜索树中删除节点的方法是一样的
一般查找二叉树删除节点
删除的方案有很多,但一般都会选下面这种,因为对整棵树各个分支深度的影响较小。
a.当被删除节点n是叶子节点,直接删除
b.当被删除节点n只有一个孩子,删除n,用孩子替代该节点的位置
c.当被删除结点n存在左右孩子时,真正的删除点应该是n的中序遍在前驱,或者说是左子树最大的节点,
之后n的值替换为真正删除点的值。这就把c归结为a,b的问题。

因此,所有的删除问题都可以转化成删除叶子节点或单支节点(只有一个孩子)的问题

红黑树删除思路:
1. 如果节点A有两个非空子节点,则找到节点A的前驱(也可以是后继),然后记要删除的元素B=(A的前驱)
2. 否则只要A有一个空子节点,就记要删除的元素B=A
3. 记变量N为B的第一个非空子节点(先检查左孩子,后检查右孩子),如果B的两个孩子都为空,则记N为空
4. 如果B和A不同,则将B的内容拷贝到A
5. 删除节点B
6. 如果被删除节点B的颜色为红色,则删除结束,否则从节点N处开始对删除节点后的红黑树进行调整
调整策略
    假设删除节点后的树中(被删除的节点是B),N的父节点为P(N作为P的左孩子),兄弟节点为S,兄弟节点的左孩子为SL,
    兄弟节点的右孩子为SR。P可能还有祖先,SL,SR可能还有后代。
    
    1. N是根节点,如果N原来是黑色,则这样做不会改变其性质,如果原来为红色,需要保持红黑树的性质,要把它修改为黑色。
    2. N节点为红色
        此时的N必定为被删除的黑色节点的唯一非空子节点。
        证明:N红色,被删除的节点必为黑色;实际要删除的节点B,要么是入参,要么是入参的前驱,这两种情况B至多都只有一个孩子;证毕
        由于B是黑色的,被删除,这一路少了一个黑节点,所以N涂黑,就解决了
    
    下面的情况是删除B后,N分路比S分路少一层黑
    3. N、S、SL、SR、P都为黑色
        S的颜色修改为红色,则通过S和通过N的路径上的黑色节点数目变得相同了,但是P这一路整体少了一层黑色节点,所以令新
        的N = P,重新执行调整算法
    4. N、S、SL、SR都为黑色,P为红色
        将S和P的颜色互换即可完成调整。局部调整不影响全局
    5. N是黑色,S是黑色,SR是红色,P和SL是任意颜色
        将以P为根的子树进行左旋;交换P和S的颜色;将SR的颜色改为黑色
        这样N路多了一层黑,调整完成
    6. N是黑色,S是黑色,SL红色,SR是黑色,P是任意颜色
        对以S为根节点的子树进行右旋;交换S和SL的颜色,即S改为黑色,SL改为红色
        证明此时是情形5,
            由图得,旋转后,SL左路黑节点数保持不变,右路也不变;
            因为旋转前SL是红色,为了维持SL和SR平衡,所以SL必有孩子节点,而且孩子必是黑色(红节点的孩子是黑色);
            为了维持SL左右路平衡,SL不可能只有一个孩子,所以SL必有左孩子,且是黑色
            证毕
        转换为情形5
    7. N是黑色,S是红色
        由于S为红色,所以P,SL,SR都必定为黑色
        以P为根节点的子树进行左旋;S和P的颜色互换,即修改S的颜色为黑色,P的颜色为红色
        旋转后,S的右路黑高度不变,左边N还是少一层,SL不少。
        这样转换成N和兄弟节点(现在的兄弟是SL)都是黑色,P是红色,可能是4,5,6三种中的一种
    
nginx代码实现:

typedef ngx_uint_t  ngx_rbtree_key_t;
typedef ngx_int_t   ngx_rbtree_key_int_t;

节点定义
typedef struct ngx_rbtree_node_s  ngx_rbtree_node_t;

struct ngx_rbtree_node_s {
    ngx_rbtree_key_t       key;
    ngx_rbtree_node_t     *left;
    ngx_rbtree_node_t     *right;
    ngx_rbtree_node_t     *parent;
    u_char                 color;
    u_char                 data;
};

树定义        
typedef struct ngx_rbtree_s  ngx_rbtree_t;

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; 哨兵节点
    ngx_rbtree_insert_pt   insert; 插入函数
};    

ngx_rbtree_init(tree, s, i)
置节点s的颜色为黑色
(tree)->root = s;
(tree)->sentinel = s;
(tree)->insert = i 插入函数可以有不同的实现,
例如ngx_rbtree_insert_value,ngx_rbtree_insert_timer_value,ngx_http_limit_conn_rbtree_insert_value

左旋函数
ngx_rbtree_left_rotate(ngx_rbtree_node_t **root, ngx_rbtree_node_t *sentinel, ngx_rbtree_node_t *node)
node的右孩子node_r取代node,node向左下降一层,node的右孩子重新赋值为node_r的左孩子node_r_l

如果node_r_l不是空节点,则node_r_l的父结点赋值为node
node_r的父结点赋值为node的父结点

如果以前node是根节点,则新根是node_r
否则如果以前node是其父结点的左孩子,则其父结点的左孩子node->parent->left赋值为node_r
如果以前node是其父结点的右孩子,则其父结点的右孩子node->parent->left赋值为node_r

node_r的左孩子赋值为node,node的parent赋值为node_r

右旋函数
node的左孩子node_l取代node的位置,node向右下降一层作为node_r的右孩子,node_l的右孩子node_l_r变为node的左孩子

红黑树插入
void ngx_rbtree_insert(ngx_rbtree_t *tree, ngx_rbtree_node_t *node)
if (*root == sentinel)新根,涂黑,返回(对应插入情况1)

调用tree->insert(*root, node, sentinel);
以ngx_rbtree_insert_value(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel)为例
查找node应该插入的位置,插入,置node的颜色为红色

下面开始调整树
while (node != *root && ngx_rbt_is_red(node->parent))
调整的条件node不是根,并且node的父结点是红节点(对应插入情况2)
如果node的parent是其祖父节点的左孩子
    取得node的叔叔节点
    如果叔叔是红节点
        父结点置黑,叔叔节点置黑,祖父节点置红,node指向祖父节点,开始下轮循环(对应插入情况3)
    如果叔叔是黑节点(这种情况是在调整的过程中产生的,一开始插入不可能)
        如果node是父节点的右孩子,node指向父结点,并左旋node,原来的node和parent互换角色,但node始终在低层(对应插入情况4)
        是左孩子什么也不做
        
        (对应插入情况5)
        置node->parent为黑色,祖父节点置红,右旋祖父节点(这样左路添加了一层黑节点,右路黑节点数不变, 两边平衡了)
        继续下轮循环(实际调整完成了,因为node的父结点是黑色了)
        
否则node的parent是其祖父节点的右孩子
    处理类似

最后置根节点是黑色


红黑树的删除
void ngx_rbtree_delete(ngx_rbtree_t *tree, ngx_rbtree_node_t *node)
上来就是寻找要替换的节点,要用temp替代subst,如果要删除的节点有2个孩子,转换成只有1个孩子的情况,寻找其右子树的最小节点
如果subst是根,用temp作为新根,自temp为黑色,返回(对应(删除思路----调整策略1))

subst->parent->left/right = temp;更新subst的父结点的孩子节点为temp

如果subst就是原生的node(原来要删除的node只有一个孩子),temp->parent = subst->parent;
否则是通过转换来的,需要用subst替换node,颜色还是用node的
    如果subst->parent == node,转换的subst和原生的node只差一代,temp->parent = subst;
    否则temp->parent = subst->parent;

    node的内容拷贝到subst,除了value
    subst->left = node->left;
    subst->right = node->right;
    subst->parent = node->parent;
    ngx_rbt_copy_color(subst, node);

    node->parent->left/right = subst;
    
    现在subst取代了原来node的位置,并且是平衡的

清空node

如果subst是红色节点,即要删除的是红色节点,不影响黑节点高度,直接返回(对应删除思路6)

此时temp已经取代subst了,但是可能会破坏红黑树的性质,需要调整
while (temp != *root && ngx_rbt_is_black(temp))
调整的条件是temp不是root,并且temp是黑节点 (对应(删除思路----调整策略2)如果是红色节点直接到最后涂黑temp即可)

    temp作为左子
        取得temp的兄弟节点
        如果兄弟节点为红 对应(删除思路----调整策略7))
            置兄弟为黑,父为红,以父为支点左旋,兄弟节点更新为新的兄弟节点,即以前的兄弟节点的左孩子
        如果兄弟节点未黑,暂不做操作
        
        到此的情况是temp黑,temp兄弟黑,父红。
        
        如果temp兄弟的两个孩子都为黑 对应(删除思路----调整策略4))
            兄弟节点置红,temp指向parent,进行下次循环(因为parent为红,会跳出循环,循环外面最后会置parent为黑)
        
        到此的情况是temp黑,temp兄弟黑,父红,temp兄弟的两个孩子不全黑。
        如果temp兄弟的右孩子为黑 对应(删除思路----调整策略6))
            temp兄弟的左孩子置黑,temp兄弟置红,右旋temp兄弟,
            此时temp的新兄弟是原来兄弟的左孩子
        
        到此的情况是temp黑,temp兄弟黑,父红,temp兄弟的右孩子为红 对应(删除思路----调整策略5))
        
        父结点的颜色赋给兄弟节点,父结点置黑,兄弟节点的右孩子置黑,左旋父结点
        temp = *root;调整结束
        
    temp作为右子

最后将ngx_rbt_black(temp);置黑

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值