在读代码之前需要先了解红黑树的特性以及插入,删除算法,插入比较简单,删除比较麻烦,以下是算法和代码的分析
红黑树是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);置黑