ngx_rbtree的基本使用方法
2016-08-06 23:46
232 查看
ngx_rbtree
导语:
红黑树是一个常用的高级数据结构,它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的:它可以在O(log n)时间内做查找,插入和删除,这里的n是树中元素的数目.
一: 红黑树的性质
1. 每个节点不是红的就是黑的 (必须上色嘛) 2. 根节点必须是黑的 3. 每个叶节点是黑色的(ngx 是sentinel) 4. 如果该节点是红色的,他的两个子节点就是黑色的(就是不能又两个连续的红色节点) 5. 每一个节点到他的子叶的所有路径上,黑色节点一样多 红黑树根据这些强制约束来保证了他的基本平衡性,从而保证了查找的效率。
二:ngx_rbtree相关的数据结构:
typedef ngx_uint_t ngx_rbtree_key_t; typedef ngx_int_t ngx_rbtree_key_int_t; typedef structngx_rbtree_node_s ngx_rbtree_node_t; struct ngx_rbtree_node_s { ngx_rbtree_key_t key;//key ngx_rbtree_node_t *left;//左节点 ngx_rbtree_node_t *right;//右节点 ngx_rbtree_node_t *parent;//父节点 u_char color;//颜色 u_char data;//数据 };
对于 rb_tree_node的定义。 可能大家看到那个data字段,感觉就是一个u_char的话,是不是有点不够用,其实实际使用的时候 一般来是采用的内存和结构体直接转换的的特性来使用
比如:
typedef struct { ngx_rbtree_node_t node; ngx_str_t str; } ngx_str_node_t;
扩展了一个str字段,还不够的话还可以继续扩展。使用的的时候 把 ngx_rbtree_node_t 强制转换ngx_str_node_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_node_t的扩展,所以插入的时候比较一个ngx_rbtree_key_t可能不是一个很好的选择。所以需要提供一个插入函数,来完成操作。
初始化的一些宏定义
#define ngx_rbtree_init(tree, s, i) \ ngx_rbtree_sentinel_init(s); \ (tree)->root = s; \ (tree)->sentinel = s; \ (tree)->insert = i #define ngx_rbt_red(node) ((node)->color = 1) #define ngx_rbt_black(node) ((node)->color = 0) #define ngx_rbt_is_red(node) ((node)->color) #define ngx_rbt_is_black(node) (!ngx_rbt_is_red(node)) #define ngx_rbt_copy_color(n1, n2) (n1->color = n2->color) #define ngx_rbtree_sentinel_init(node) ngx_rbt_black(node)
这些宏从名字就可以看出他的作用,就不一一的述说了。
三:插入
要说红黑树的插入首先都是不得不先说一下左旋和右旋。1.左旋: 就是以一个节点p和他的右孩子y为支轴进行,让y成为新的根,p成为y的左孩子,y的左孩子变成p的右孩子。
static ngx_inline void ngx_rbtree_left_rotate(ngx_rbtree_node_t **root,ngx_rbtree_node_t *sentinel, ngx_rbtree_node_t *node) { ngx_rbtree_node_t *temp; temp = node->right;//获取右节点 node->right = temp->left;//node的右节点设置为他原来右节点的左节点 node->right = node->right->left; if (temp->left != sentinel) {//因为修改了指向在不为空的情况下需要更改praent temp->left->parent = node;//node->right->parent = node } temp->parent = node->parent;//右节点将会变成原来node的父节点。 if (node == *root) {//是不是根节点的判断 *root = temp; } elseif (node == node->parent->left) {//然后把右节点的信息和原来node的parent进行维护 node->parent->left = temp; } else { node->parent->right = temp; } temp->left = node;//现在node变回他原来右节点的子节点了 node->parent = temp;//所以他的parent变成temp }
右旋:
static ngx_inline void ngx_rbtree_right_rotate(ngx_rbtree_node_t **root,ngx_rbtree_node_t *sentinel, ngx_rbtree_node_t *node) { ngx_rbtree_node_t *temp; temp = node->left; node->left = temp->right; if (temp->right != sentinel) { temp->right->parent = node; } temp->parent = node->parent; if (node == *root) { *root = temp; } elseif (node == node->parent->right) { node->parent->right = temp; } else { node->parent->left = temp; } temp->right = node; node->parent = temp; }
因为两个差不多就不进行叙述了。
在红黑树中,使用左旋右旋主要是为了把当前情况转换为另外一种已知的情况来进行处理。
旋转操作会改变红黑树的红黑性质,但是树的搜索性质不会变。
旋转说了之后来看插入:
红黑树的插入分为两个步骤,首先就是给他找个位置放上,让后插入可能会违反红黑树的一些性质,所以需要维护一下红黑树。
—插入:和节点不断的比较大小,选择左右枝,然后知道到底,然后维护上下的父子关系。然后jiang这个节点设置为红色(因为红黑树要求每一条路径上面黑色的节点的数量是必须一样的,你弄个黑色过去,那这条路绝逼不一样了,弄个红色的话,还有可能啥都不用维护就收工了);
插入完成之后,有可能会破坏红黑树的性质,所以我们需要来恢复他的性质,总体来说,恢复过程不复杂,把几种情况分析处理了就ok(当然还要分左子右子,因为是镜像的,所以就以左子为例)
最简单的,插入进去就是根节点
处理办法:涂黑就行了。
插入节点的父节点是黑色
处理办法:哎呀运气不错 啥时候也不用做。
当前节点的父节点是红色,叔叔节点也是红色。
处理办法:祖父节点是肯定存在的,因为父节点是红色,红色不能为根嘛,然后这个时候我们还要分他的父节点是左子还是右子。但是一般来说,对于对称性,说一边的就好。
讲当前节点的父节点和叔叔节点涂黑,祖父节点涂红(这样就保证了这条路线上的黑色节点的个数正确)但是改变了祖父节点的颜色,可能破坏了红黑树的性质,所以需要讲当前节点指向祖父节点,再次进行恢复。(这样操作保证了祖父节点这条线路上 黑色子的数量是正确的,但是改变了祖父节点的颜色,所以需要指向祖父节点重新维护下红黑树的特性)
当前节点的父亲节点为红色,叔叔节点为黑色,当前节点为左子。
处理办法:把他的父节点涂黑,祖父节点涂红,以祖父节点为支点右旋。(把父节点涂黑的时候,在祖父节点看来他的右子那侧的黑子数量是比左侧是要多1个的 所以通过右旋,让这个多出来的节点变成他们的祖父节点,就不会有多1的问题,而且还不会破坏原来的红黑树的特性,因为原来的祖父节点也是黑色,旋转后变成右子被涂为红色了)
当前节点的父亲节点是红色,叔叔节点是黑色,当前节点为右子。
处理办法:很简单,以他的父亲节点作为支点,左旋,这个时候就变成情况4了。
总结下:恢复红黑树的方法,其实就是每个情况都是这五个中的一个,理解这几种情况,就可以处理了。
void ngx_rbtree_insert_value(ngx_rbtree_node_t *temp,ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel) { ngx_rbtree_node_t **p; for ( ;; ) {//找到插入点 p = (node->key < temp->key) ? &temp->left : &temp->right; if (*p == sentinel) { break; } temp = *p; } *p = node; node->parent = temp;//插入操作 node->left = sentinel; node->right = sentinel; ngx_rbt_red(node);//弄成红色 } void ngx_rbtree_insert(ngx_thread_volatilengx_rbtree_t *tree, ngx_rbtree_node_t *node) { ngx_rbtree_node_t **root, *temp, *sentinel; /* a binary tree insert */ root = (ngx_rbtree_node_t **) &tree->root; sentinel = tree->sentinel; if (*root == sentinel) {//当前树还是空的插入进去就是根节点 node->parent =NULL; node->left = sentinel; node->right = sentinel; ngx_rbt_black(node);//弄成黑色 *root = node; //设为根节点 return; } tree->insert(*root, node, sentinel);//插入操作 /* re-balance tree */ while (node != *root &&ngx_rbt_is_red(node->parent)) {//父节点还是红色的 if (node->parent == node->parent->parent->left) {//左子 temp = node->parent->parent->right;//叔叔节点 if (ngx_rbt_is_red(temp)) {//叔叔节点是红色的 case 3 ngx_rbt_black(node->parent);//父节点弄黑 ngx_rbt_black(temp);//祖父节点涂红 ngx_rbt_red(node->parent->parent);//祖父节点涂红 node = node->parent->parent;//然后指向祖父节点,继续去维护红黑树性质 } else {//叔叔节点是黑色的 if (node == node->parent->right) {//case 5 node = node->parent; ngx_rbtree_left_rotate(root, sentinel, node);//左旋一下变成case 4 } //case 4 ngx_rbt_black(node->parent);//把父节点涂黑 ngx_rbt_red(node->parent->parent);//祖父节点涂红 ngx_rbtree_right_rotate(root, sentinel, node->parent->parent);//右旋 } } else {//右子 temp = node->parent->parent->left;//叔叔节点 if (ngx_rbt_is_red(temp)) {//叔叔节点为红色 case3 ngx_rbt_black(node->parent); ngx_rbt_black(temp); ngx_rbt_red(node->parent->parent); node = node->parent->parent; } else {//左右是反的 if (node == node->parent->left) {//case5 node = node->parent; ngx_rbtree_right_rotate(root, sentinel, node); } //case 4 ngx_rbt_black(node->parent); ngx_rbt_red(node->parent->parent); ngx_rbtree_left_rotate(root, sentinel, node->parent->parent); } } } ngx_rbt_black(*root);//根节点涂黑 }
四:删除
节点的删除和插入一样,都会进行两步,第一步就是删除,然后需要恢复下红黑树的性质。因为可能会破坏红黑树的性质。所以需要恢复删除时候的情况:
1. 如果要被删除的节点没有孩子,那么就直接删除。
2. 如果删除的节点有一个孩子,删除之后,用它的孩子还代替他。
3. 如果有两个孩子,这个时候你可以选择左孩子那条路径里最大的值,或者右孩子最小的值来进行删除,然后这个孩子去替代原来节点的位置。
删除操作总体来说很简单,就是要保证被删除的节点只有一个或者没有孩子就行了。
删除完成之后来看下接下来的恢复工作,也是格式化的几种情况(如果是红色,就直接返回,因为没有影响到红黑树的性质 删除后,被删除的节点的孩子会顶替到删除节点的位置,孩子节点我们记为temp因为恢复操作就是围绕着temp来进行的)咱们只讨论左孩子,右孩子镜像。
如果temp的兄弟节点w为红色。那个他们的父亲节点为黑色
处理办法:把w弄成黑色,把parent弄成红色进行一次左旋,然后将情况转换为后面一种已知的情况 然后将temp指向temp的兄弟。
如果temp的兄弟w为黑色,这个时候他们的父亲节点就可红可黑了,因为temp这条路径是比他兄弟那条路径少一个黑色的节点,如果这个时候他兄弟节点w的两个孩子都是黑色的,那么咱们就直接把w节点设置为红色,这个时候两个兄弟节点的黑色数量一样,但是w parent这条路劲上的黑色节点少一个所以指向w的parent继续循环。
如果temp的兄弟节点w为黑色,并且他的右孩子也是黑色,左孩子为红色,那么我们就将w的左孩子设置为黑色,把设置为红色,并且以w进行一次右旋,转换为情况4.
如果temp的兄弟节点w为黑色,并且他的左孩子为黑色,右孩子为红色,这时候就把w的颜色设置为parent的颜色,parent设置为黑色,w的右孩子设置为黑色,然后以parent为节点进行右旋,恢复红黑树的性质。
ngx的实现代码
void ngx_rbtree_delete(ngx_thread_volatile ngx_rbtree_t *tree, ngx_rbtree_node_t *node) { ngx_uint_t red; ngx_rbtree_node_t **root, *sentinel, *subst, *temp, *w; /* a binary tree delete */ root = (ngx_rbtree_node_t **) &tree->root; sentinel = tree->sentinel; if (node->left == sentinel) {//如果左边为空 这个时候右孩子可能为空 可能有 temp = node->right; subst = node; // } else if (node->right == sentinel) {//如果右边为空 左边有数据 temp = node->left; subst = node; } else {//两个都不为空 subst = ngx_rbtree_min(node->right, sentinel);//选择右孩子最小 if (subst->left != sentinel) { temp = subst->left; } else { temp = subst->right; } } if (subst == *root) {//真正删除的节点是根节点 *root = temp; ngx_rbt_black(temp); /* DEBUG stuff */ node->left = NULL; node->right = NULL; node->parent = NULL; node->key = 0; return; } red = ngx_rbt_is_red(subst); if (subst == subst->parent->left) {//左支 subst->parent->left = temp; } else {//右枝 subst->parent->right = temp; } if (subst == node) {//需要删除的节点是本身 temp->parent = subst->parent; } else { if (subst->parent == node) {//只有一个节点的差距。 temp->parent = subst;// } else { temp->parent = subst->parent;//常规情况 } subst->left = node->left;//把node的信息拷贝到subst里面去 subst->right = node->right; subst->parent = node->parent; ngx_rbt_copy_color(subst, node); if (node == *root) {//如果是root节点 *root = subst; } else {//维护父子信息 if (node == node->parent->left) {// node->parent->left = subst; } else { node->parent->right = subst; } } if (subst->left != sentinel) { subst->left->parent = subst; } if (subst->right != sentinel) { subst->right->parent = subst; } } /* DEBUG stuff */ node->left = NULL; node->right = NULL; node->parent = NULL; node->key = 0; if (red) {//如果是红色的 return; } /* a delete fixup */ while (temp != *root && ngx_rbt_is_black(temp)) { if (temp == temp->parent->left) {//左子 w = temp->parent->right; if (ngx_rbt_is_red(w)) {//兄弟是红色的 case1 ngx_rbt_black(w); ngx_rbt_red(temp->parent); ngx_rbtree_left_rotate(root, sentinel, temp->parent); w = temp->parent->right; } if (ngx_rbt_is_black(w->left) && ngx_rbt_is_black(w->right)) {//case 2 ngx_rbt_red(w); temp = temp->parent; } else { if (ngx_rbt_is_black(w->right)) {//case 3 ngx_rbt_black(w->left); ngx_rbt_red(w); ngx_rbtree_right_rotate(root, sentinel, w); w = temp->parent->right; } //case 4 ngx_rbt_copy_color(w, temp->parent); ngx_rbt_ a172 black(temp->parent); ngx_rbt_black(w->right); ngx_rbtree_left_rotate(root, sentinel, temp->parent); temp = *root; } } else { w = temp->parent->left; if (ngx_rbt_is_red(w)) { ngx_rbt_black(w); ngx_rbt_red(temp->parent); ngx_rbtree_right_rotate(root, sentinel, temp->parent); w = temp->parent->left; } if (ngx_rbt_is_black(w->left) && ngx_rbt_is_black(w->right)) { ngx_rbt_red(w); temp = temp->parent; } else { if (ngx_rbt_is_black(w->left)) { ngx_rbt_black(w->right); ngx_rbt_red(w); ngx_rbtree_left_rotate(root, sentinel, w); w = temp->parent->left; } ngx_rbt_copy_color(w, temp->parent); ngx_rbt_black(temp->parent); ngx_rbt_black(w->left); ngx_rbtree_right_rotate(root, sentinel, temp->parent); temp = *root; } } } ngx_rbt_black(temp); }
总结下:如果w为红色那么就通过旋转转换成后面的情况处理,其实最终恢复红黑树性质的是情况4,w的右孩子为红色,通过增加黑色,旋转来平衡,可以具体旋转试下,旋转后根节点变成了w因为w拷贝了原来根节点的颜色,所以黑色数量不会影响,然后原来的根节点来到了左支,补充了缺失的黑色节点。然后将原来的w的右孩子设置为黑色,补充旋转走掉的w所缺失的黑色,这个时候,完成了平衡。
相关文章推荐
- eclipse +cvs 的基本使用方法(二)
- Vi编辑器的基本使用方法!
- [导入]Vi编辑器的基本使用方法 (转载)
- Log4j基本使用方法
- Log4j基本使用方法
- Sailprint打印组件的基本使用方法。
- Log4j基本使用方法
- Vi编辑器的基本使用方法
- Vi编辑器的基本使用方法
- Vi编辑器的基本使用方法
- Tiles组件的基本使用方法
- Log4j基本使用方法
- 基本技术:使用委派对方法进行异步调用/异步消费Web Services
- Vi编辑器的基本使用方法
- Log4j基本使用方法
- eclipse +cvs 的基本使用方法(三)
- Log4j基本使用方法
- Log4j基本使用方法
- Vi编辑器的基本使用方法
- Vi编辑器的基本使用方法