之前博主稍微讲解了下红黑树的原理,那么在这篇博客博主想要把红黑树讲的更加的透彻,以便于更多的人了解红黑树
(本博客会更加详细的介绍之前的博客没介绍到的,所以各位看官不同再回去翻看博主之前那篇红黑树的原理讲解了。)
博主之前也看过很多对红黑树的介绍但是博主感觉大多都没用讲的很清楚,所以博主就有一个大胆的想法,希望自己的这篇博客能把NGINX下的红黑树讲明白,让大家不再恐惧红黑树。
下面博主就给出NGINX下关于红黑树的代码(博主上面已经加上了注释以便于大家理解),这一大块内容当然是很难啃下来的,所以博主会把代码进行分块讲解
博主推荐大家看代码使用一个神器 source insight 看代码非常方便,有需要的朋友自行百度吧,博主就不再介绍如何安装和使用了。
在看代码之前大家还是和博主来回顾一下红黑树的5大性质吧。
红黑树的性质:
1)节点是红色或黑色;
2)根节点是黑色;
3)所有叶子节点都是黑色节点(NULL);
4)每个红色节点必须有两个黑色的子节点(如果叶子结点是红色,那么我的黑色结点可以不画出来)。(从每个叶子到根的所有路径上不能有两个连 续的红色节点。)
5)从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
不用担心记不到,博主在后面的分块内容还会列出来的。只需要按照博主的安排一步一步的,我们来慢慢了解红黑树即可。
红黑树的代码在nginx的: nginx-1.17.2.tar.gz\nginx-1.17.2\src\core
希望大家看到代码不要怕,后面会讲的
ngx_rbtree.h
/*
* Copyright (C) Igor Sysoev
* Copyright (C) Nginx, Inc.
*/
#ifndef _NGX_RBTREE_H_INCLUDED_
#define _NGX_RBTREE_H_INCLUDED_
#include <ngx_config.h>
#include <ngx_core.h>
typedef ngx_uint_t ngx_rbtree_key_t; //unsigned int
typedef ngx_int_t ngx_rbtree_key_int_t; //int/long int
typedef struct ngx_rbtree_node_s ngx_rbtree_node_t; //表示结点,以后用来插入用的。
struct ngx_rbtree_node_s {
ngx_rbtree_key_t key; //unsigned int 无符号整形来定义 key关键字
ngx_rbtree_node_t *left; //指向左孩子节点
ngx_rbtree_node_t *right; //指向右孩子结点
ngx_rbtree_node_t *parent; //指向父节点
u_char color; //无符号型的 char 这个用来标识颜色
u_char data; //无符号型的 char这个用来表示数据
};
typedef struct ngx_rbtree_s ngx_rbtree_t; //“_s”是结构体“_t”是类型
//下面是一个函数指针变量类型的定义,是红黑树插入函数的指针
//参数有树根结点、插入结点和哨兵结点的指针
//插入函数指针。可以调用ngx_rbtree_insert_value(作用是找到合适的 插入点)
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; //插入函数指针
};
//将函数指针变量作为结构体成员变量以达成可以把结构体当做类来使用(既有成员变量又有成员方法)的效果,
//这种手法在nginx的源码中相当普遍。关于函数,nginx还有一种更神奇的手段——宏:
/* 初始化红黑树,即为空的红黑树 */
/* tree 是指向红黑树的指针,
* s 是红黑树的一个NIL节点, 表示无值,任何变量在没有被赋值之前的值都为nil。
* i 表示函数指针,决定节点是新增还是替换
*/
#define ngx_rbtree_init(tree, s, i) \
ngx_rbtree_sentinel_init(s); \
(tree)->root = s; \
(tree)->sentinel = s; \
(tree)->insert = i 这里insert函数指针的赋值实现了多态
void ngx_rbtree_insert(ngx_rbtree_t *tree, ngx_rbtree_node_t *node); //插入
void ngx_rbtree_delete(ngx_rbtree_t *tree, ngx_rbtree_node_t *node); //删除
void ngx_rbtree_insert_value(ngx_rbtree_node_t *root, ngx_rbtree_node_t *node, //插入函数
ngx_rbtree_node_t *sentinel)
void ngx_rbtree_insert_timer_value(ngx_rbtree_node_t *root,
ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel); //ngx_rbtree_insert_timer_value函数跟ngx_rbtree_insert_value函数唯一区别就是判断大小时,采用了两个值相减,避免溢出
ngx_rbtree_node_t *ngx_rbtree_next(ngx_rbtree_t *tree,
ngx_rbtree_node_t *node);
/* 给节点着色,1表示红色,0表示黑色 */
#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)
/* a sentinel must be black */
/* 节点着黑色的宏定义 */
#define ngx_rbtree_sentinel_init(node) ngx_rbt_black(node)
static ngx_inline ngx_rbtree_node_t *
ngx_rbtree_min(ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel)
{ //寻找 nginx下的最小值,在.c中你看到的调用其实就是 找到右子树的最小值用来作为后继节点
while (node->left != sentinel) { //在不溢出的情况下执行下面的语句
node = node->left;
}
return node;
}
//ngx_inline是一个宏,实际值就是关键字inline。这个内联函数非常好懂,
#endif /* _NGX_RBTREE_H_INCLUDED_ */
ngx_rbtree.c
/*
* Copyright (C) Igor Sysoev
* Copyright (C) Nginx, Inc.
*/
#include <ngx_config.h>
#include <ngx_core.h>
/*
* The red-black tree code is based on the algorithm described in
* the "Introduction to Algorithms" by Cormen, Leiserson and Rivest.
*/
static ngx_inline void ngx_rbtree_left_rotate(ngx_rbtree_node_t **root,
ngx_rbtree_node_t *sentinel, ngx_rbtree_node_t *node); //左旋
static ngx_inline void ngx_rbtree_right_rotate(ngx_rbtree_node_t **root,
ngx_rbtree_node_t *sentinel, ngx_rbtree_node_t *node); //右旋
//红黑树的插入
/* 插入节点 */
/* 插入节点的步骤:
* 1、首先按照二叉查找树的插入操作插入新节点;
* 2、然后把新节点着色为红色(避免破坏红黑树性质5);
* 3、为维持红黑树的性质,调整红黑树的节点(着色并旋转),使其满足红黑树的性质;
*/
void
ngx_rbtree_insert(ngx_rbtree_t *tree, ngx_rbtree_node_t *node)
{
ngx_rbtree_node_t **root, *temp, *sentinel; //这三个都有独立的结构
/* a binary tree insert */
root = &tree->root; //树根指针赋给了root
sentinel = tree->sentinel; //哨兵指针赋给了哨兵指针
//如果树是空树的话,那么插入的结点就会变成根节点,伴随着的左右子节点就会变成哨兵
if (*root == sentinel) { //特判,如果根是哨兵,即树是空的
node->parent = NULL; //新插入的结点变成了根
node->left = sentinel; //新结点的左子结点是哨兵
node->right = sentinel; //新结点的右子结点也是哨兵
ngx_rbt_black(node); //新根染黑
*root = node; //确认新结点为新根
return; //插入结束
}
//树初始化时给了insert指针一个函数地址
//查看前面的宏ngx_rbtree_init(tree, s, i)
//发现只是把指定结点染黑,同时赋为根和哨兵,给insert指针指定一个函数
//ngx_rbtree.c中有两个参数表符合的可选函数:插入值、插入计时器值
//稍后来看两种插入分别如何实现又有什么区别
// insert 就对应 .h的那一个函数指针(61,54 行就能看懂了)
// 函数指针里面的参数有 根节点、插入结点、哨兵结点的指针
tree->insert(*root, node, sentinel); //插入操作(通过这个函数指针的调用就能找到我们的插入点了)
/* 若红黑树不为空,则按照二叉查找树的插入操作进行
* 该操作由函数指针提供(跳转到插入函数)
*/
/* re-balance tree */
//如果新结点不是根结点而且其父结点是红的,循环
while (node != *root && ngx_rbt_is_red(node->parent)) {
//最坏的情况,就是 情况:3,4,5全部走一遍
// 之前博客的情况3
if (node->parent == node->parent->parent->left) { //如果父结点是左子结点,temp 获得父结点的右兄弟
temp = node->parent->parent->right;
if (ngx_rbt_is_red(temp)) { //如果父结点的右兄弟是红的
ngx_rbt_black(node->parent); //父结点染黑
ngx_rbt_black(temp); //父结点的右兄弟染黑
ngx_rbt_red(node->parent->parent); //父结点的父结点染红
node = node->parent->parent; //父结点的父结点成为当前结点,继续去维护红黑树性质
//之前博客的情况4
} else { //如果父结点的右兄弟是黑的
if (node == node->parent->right) { //如果新结点是右子结点
node = node->parent; //父结点成为新node
ngx_rbtree_left_rotate(root, sentinel, node); //node左旋 --->跳转到子函数
}
//之前博客的情况5
ngx_rbt_black(node->parent); //node的父结点染黑
ngx_rbt_red(node->parent->parent); //node的父结点的父结点染红
ngx_rbtree_right_rotate(root, sentinel, node->parent->parent);//node的父结点的父结点右旋 ---->跳转到子函数
}
}
//与上面的情况对称
else { //如果父结点是右子结点,获得父结点的左兄弟
temp = node->parent->parent->left;
if (ngx_rbt_is_red(temp)) {//如果父结点的左兄弟是红的
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) { //如果新结点是左子结点
node = node->parent; //父结点成为当前结点
ngx_rbtree_right_rotate(root, sentinel, node); //当前结点右旋
}
ngx_rbt_black(node->parent); //当前结 点染黑
ngx_rbt_red(node->parent->parent); //当前结点父结点的父结点染红
ngx_rbtree_left_rotate(root, sentinel, node->parent->parent); //当前结点的父结点的父结点左旋
}
}
}
ngx_rbt_black(*root); //根结点染黑,因为根节点必须为黑色
}
//然后是对应ngx_rbtree_insert_pt指针的基础的结点插入函数:
//上文提到nginx提供一种机制自定义插入函数解决key值冲突,不过nginx提供几个默认的供我们使用:
//root是容器指针
//node是待插入节点指针
//sentinel是哨兵节点
//向红黑树插入节点,每个节点key都是唯一的
void
ngx_rbtree_insert_value(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node,
ngx_rbtree_node_t *sentinel)
{//根节点temp、插入结点node、哨兵结点的指针sentinel
ngx_rbtree_node_t **p;
for ( ;; ) {//无条件循环或者说死循环,等同于while(1)但节省了一个字符,为了找到插入点
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);
}
// ngx_rbtree_insert_timer_value函数跟ngx_rbtree_insert_value函数唯一区别就是判断大小时,采用了两个值相减,避免溢出。
//向红黑书插入数据节点,每个数据节点的关键字表示时间或者时间差
void
ngx_rbtree_insert_timer_value(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node,
ngx_rbtree_node_t *sentinel)
{
ngx_rbtree_node_t **p;
for ( ;; ) {
/*
* Timer values
* 1) are spread in small range, usually several minutes,
* 2) and overflow each 49 days, if milliseconds are stored in 32 bits.
* The comparison takes into account that overflow.
*/
/* node->key < temp->key */
p = ((ngx_rbtree_key_int_t) (node->key - temp->key) < 0)
? &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);
}
//红黑树的删除!!!!!
//据说红黑树和AVL树的区别主要体现在删除节点时,我们就来看一看。
void
ngx_rbtree_delete(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 = &tree->root; //树根指针的指针赋给了root
sentinel = tree->sentinel; //哨兵指针赋给了哨兵指针
/* 下面是获取temp节点值,temp保存的节点是准备替换节点node ;
* subst是保存要被替换的节点的后继节点;
*/
/* case1:若node节点没有左孩子(这里包含了存在或不存在右孩子的情况)*/
if (node->left == sentinel) { //如果左子结点是哨兵或左右子结点都是哨兵
temp = node->right; //获得右子结点,后面让它接替node位置
subst = node; //node赋给subst
}
/* case2:node节点存在左孩子,但是不存在右孩子 */
else if (node->right == sentinel) { //如果右子结点是哨兵
temp = node->left; //获得左子结点,后面让它接替node位置
subst = node; //node赋给subst
/* case3:node节点既有左孩子,又有右孩子 */
} else { //如果左右子结点都不是哨兵
/* 获取node节点的后续节点 */
subst = ngx_rbtree_min(node->right, sentinel); //获得右子树中最小的结点---->头结点里面有一个内联函数来着
if (subst->left != sentinel) { //如果右子树的最小结点的左子结点不是哨兵,基本上会不执行这条语句
temp = subst->left; //获得右子树的最小结点的左子结点
} else { //否则获得右子树最小结点的右子结点
temp = subst->right; //看起来subst将被从原位置删掉然后接替node的位置
}
}
//下面我们来看看temp和subst要干什么用:
/* 若被替换的节点subst是根节点,则temp直接替换subst成为根节点 */
if (subst == *root) { //如果subst是根 --->//真正删除的节点是根节点
*root = temp; //temp接替根
ngx_rbt_black(temp); //染黑temp
/* DEBUG stuff */
node->left = NULL; //清空了待删结点
node->right = NULL;
node->parent = NULL;
node->key = 0;
return;
}
//将我们的后继节点从书上脱离出来
/* red记录subst节点的颜色 */
red = ngx_rbt_is_red(subst); //获得subst是否是红色
/* temp节点替换subst 节点 */
if (subst == subst->parent->left) { //如果subst是左子结点
subst->parent->left = temp; //把接替结点挂到subst位置
} else { //如果subst是右子结点
subst->parent->right = temp; //把接替结点挂到subst位置
}
/* 根据subst是否为node节点进行处理 */
if (subst == node) { //如果subst是待删结点--->//需要删除的节点是本身
temp->parent = subst->parent; //接替结点直接接替,删除完成
} else { //如果subst不是待删结点
if (subst->parent == node) { //如果subst的父结点就是待删结点
temp->parent = subst; //接替结点挂在subst上
} else { {//如果待删结点比subst的父结点更高
temp->parent = subst->parent; //把接替结点挂在subst的父结点上 -->//常规情况
}
/* 复制node节点属性 */
//subst接替待删结点node的位置,复制待删结点跟周围结点的关系 --->把node的信息拷贝到subst里面去
subst->left = node->left;
subst->right = node->right;
subst->parent = node->parent;
ngx_rbt_copy_color(subst, node);
if (node == *root) { //如果是root节点 //如果待删结点是根
*root = subst; //subst接替根
} else { //如果待删结点不是根,subst接替它 -->维护父子信息
if (node == node->parent->left) {
node->parent->left = subst;
} else {
node->parent->right = subst;
}
}
//这里就是将node完全脱离我们的红黑树了
if (subst->left != sentinel) { //如果subst左子结点不是哨兵
subst->left->parent = subst; //subst的左子结点放弃node,挂上来
}
if (subst->right != sentinel) { //如果subst右子结点不是哨兵
subst->right->parent = subst; //subst右子结点放弃node,挂上来
}
}
//清空待删结点node
/* DEBUG stuff */
node->left = NULL;
node->right = NULL;
node->parent = NULL;
node->key = 0;
//如果subst是红色,红黑树约束依然被遵守,删除工作就可以结束了
if (red) {
return;
}
/* 下面开始是调整红黑树的性质 */
//看起来结点的删除过程已经顺利完成了,但是如果subst是黑色,我们需要修复红黑树的约束。
//下面这一段代码的主角是接替subst位置的temp结点:
/* a delete fixup */
//当subst的接替结点不是根且为黑色,循环
/* 根据temp节点进行处理 ,若temp不是根节点且为黑色 */
while (temp != *root && ngx_rbt_is_black(temp)) {
/* 若temp是其父亲节点的左孩子 */
if (temp == temp->parent->left) { //如果temp是左子结点
w = temp->parent->right; //获得其右兄弟 /* w为temp的兄弟节点 */
/* case A:temp兄弟节点为红色 */
/* 解决办法:
* 1、改变w节点及temp父亲节点的颜色;
* 2、对temp父亲节的做一次左旋转,此时,temp的兄弟节点是旋转之前w的某个子节点,该子节点颜色为黑色;
* 3、此时,case A已经转换为case B、case C 或 case D;
*/
if (ngx_rbt_is_red(w)) { //如果temp的右兄弟是红色
ngx_rbt_black(w); //染黑temp的右兄弟
ngx_rbt_red(temp->parent); //染红temp的父结点
ngx_rbtree_left_rotate(root, sentinel, temp->parent); //temp的父结点左旋
w = temp->parent->right; //获得temp的新右兄弟
}
/* case B:temp的兄弟节点w是黑色,且w的两个子节点都是黑色 */
/* 解决办法:
* 1、改变w节点的颜色;
* 2、把temp的父亲节点作为新的temp节点;
*/
if (ngx_rbt_is_black(w->left) && ngx_rbt_is_black(w->right)) { //如果temp右兄弟的左右子结点都是黑的
ngx_rbt_red(w); //染红temp的右兄弟
temp = temp->parent; //获得temp的父结点为新temp
} else { //如果temp右兄弟的子结点不全为黑
/* case C:temp的兄弟节点是黑色,且w的左孩子是红色,右孩子是黑色 */
/* 解决办法:
* 1、将改变w及其左孩子的颜色;
* 2、对w节点进行一次右旋转;
* 3、此时,temp新的兄弟节点w有着一个红色右孩子的黑色节点,转为case D;
*/
if (ngx_rbt_is_black(w->right)) { //如果其右子结点是黑色
ngx_rbt_black(w->left); //染黑左子结点
ngx_rbt_red(w); //染红temp的右兄弟
ngx_rbtree_right_rotate(root, sentinel, w); //右兄弟右旋
w = temp->parent->right; //获得temp的新右兄弟
}
/* case D:temp的兄弟节点w为黑色,且w的右孩子为红色 */
/* 解决办法:
* 1、将w节点设置为temp父亲节点的颜色,temp父亲节点设置为黑色;
* 2、w的右孩子设置为黑色;
* 3、对temp的父亲节点做一次左旋转;
* 4、最后把根节点root设置为temp节点;*/
ngx_rbt_copy_color(w, temp->parent); //temp右兄弟复制temp父结点颜色
ngx_rbt_black(temp->parent); //染黑temp父结点
ngx_rbt_black(w->right); //染黑temp右兄弟的右子结点
ngx_rbtree_left_rotate(root, sentinel, temp->parent); //temp父结点左旋
temp = *root; //获得根
}
/* 这里针对的是temp节点为其父亲节点的左孩子的情况 */
} else{//如果temp是右子结点,做对称的事
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); //染黑当前temp
}
//左旋 就是以一个节点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; //获取当前右节点 此时temp就是当前节点的右节点了
node->right = temp->left; ///node的右节点设置为他原来右节点的左节点
if (temp->left != sentinel) { //如果右子结点的左结点不为哨兵
temp->left->parent = node; //右子结点的左子结点挂在左旋结点上
}
temp->parent = node->parent; //右节点将会变成原来node的父节点。
if (node == *root) { //是不是根节点的判断
*root = temp;
} else if (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; //根节点赋为左子结点
} else if (node == node->parent->right) { //如果右旋结点为右子结点
node->parent->right = temp; //左子结点挂父结点右边
} else { //否则左子结点挂父结点左边
node->parent->left = temp;
}
temp->right = node; //现在node变回他原来左节点的子节点了
node->parent = temp; //所以他的parent变成temp
}
ngx_rbtree_node_t *
ngx_rbtree_next(ngx_rbtree_t *tree, ngx_rbtree_node_t *node)
{
ngx_rbtree_node_t *root, *sentinel, *parent;
sentinel = tree->sentinel;
if (node->right != sentinel) {
return ngx_rbtree_min(node->right, sentinel);
}
root = tree->root;
for ( ;; ) {
parent = node->parent;
if (node == root) {
return NULL;
}
if (node == parent->left) {
return parent;
}
node = parent;
}
}
我们就先看一下红黑树的插入吧
给出分块代码:
在树为空的情况下
//红黑树的插入
/* 插入节点 */
/* 插入节点的步骤:
* 1、首先按照二叉查找树的插入操作插入新节点;
* 2、然后把新节点着色为红色(避免破坏红黑树性质5);
* 3、为维持红黑树的性质,调整红黑树的节点(着色并旋转),使其满足红黑树的性质;
*/
void
ngx_rbtree_insert(ngx_rbtree_t *tree, ngx_rbtree_node_t *node)
{
ngx_rbtree_node_t **root, *temp, *sentinel; //这三个都有独立的结构
/* a binary tree insert */
root = &tree->root; //树根指针赋给了root
sentinel = tree->sentinel; //哨兵指针赋给了哨兵指针
//如果树是空树的话,那么插入的结点就会变成根节点,伴随着的左右子节点就会变成哨兵
if (*root == sentinel) { //特判,如果根是哨兵,即树是空的
node->parent = NULL; //新插入的结点变成了根
node->left = sentinel; //新结点的左子结点是哨兵
node->right = sentinel; //新结点的右子结点也是哨兵
ngx_rbt_black(node); //新根染黑
*root = node; //确认新结点为新根
return; //插入结束
}
两次截图:
还是插入的部分(只是吧代码分块,这样方便演示博主的gif动画)
在这里我们就讨论一种情况因为另一种情况其实就是镜像
//树初始化时给了insert指针一个函数地址
//查看前面的宏ngx_rbtree_init(tree, s, i)
//发现只是把指定结点染黑,同时赋为根和哨兵,给insert指针指定一个函数
//ngx_rbtree.c中有两个参数表符合的可选函数:插入值、插入计时器值
//稍后来看两种插入分别如何实现又有什么区别
// insert 就对应 .h的那一个函数指针(61,54 行就能看懂了)
// 函数指针里面的参数有 根节点、插入结点、哨兵结点的指针
tree->insert(*root, node, sentinel); //插入操作(通过这个函数指针的调用就能找到我们的插入点了)
//现在就要跳转到了!!!!!所以我们就来到跳转的函数吧。
跳转到的插入函数操作(寻找到合适的插入点然后返回到我们后面的代码块,进行旋转染色的操作)
//然后是对应ngx_rbtree_insert_pt指针的基础的结点插入函数:
//上文提到nginx提供一种机制自定义插入函数解决key值冲突,不过nginx提供几个默认的供我们使用:
//root是容器指针
//node是待插入节点指针
//sentinel是哨兵节点
//向红黑树插入节点,每个节点key都是唯一的
void
ngx_rbtree_insert_value(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node,
ngx_rbtree_node_t *sentinel)
{//根节点temp、插入结点node、哨兵结点的指针sentinel
ngx_rbtree_node_t **p;
for ( ;; ) {//无条件循环或者说死循环,等同于while(1)但节省了一个字符,为了找到插入点
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);
}
由于性质这块比较模糊,大家可以去翻看博主之前的博客,里面对性质写的还是较为详细的。
这个是插入到应该的点应该去变色和旋转了(到我们最后的插入模块了)
最后的插入代码(其实就是对红黑树的修复)
/* 若红黑树不为空,则按照二叉查找树的插入操作进行
* 该操作由函数指针提供(跳转到插入函数)
*/
/* re-balance tree */
//如果新结点不是根结点而且其父结点是红的,循环
while (node != *root && ngx_rbt_is_red(node->parent)) {
//最坏的情况,就是 情况:3,4,5全部走一遍
// 之前博客的情况3
if (node->parent == node->parent->parent->left) { //如果父结点是左子结点,temp 获得父结点的右兄弟
temp = node->parent->parent->right;
if (ngx_rbt_is_red(temp)) { //如果父结点的右兄弟是红的
ngx_rbt_black(node->parent); //父结点染黑
ngx_rbt_black(temp); //父结点的右兄弟染黑
ngx_rbt_red(node->parent->parent); //父结点的父结点染红
node = node->parent->parent; //父结点的父结点成为当前结点,继续去维护红黑树性质
//之前博客的情况4
} else { //如果父结点的右兄弟是黑的
if (node == node->parent->right) { //如果新结点是右子结点
node = node->parent; //父结点成为新node
ngx_rbtree_left_rotate(root, sentinel, node); //node左旋 --->跳转到子函数
}
//之前博客的情况5
ngx_rbt_black(node->parent); //node的父结点染黑
ngx_rbt_red(node->parent->parent); //node的父结点的父结点染红
ngx_rbtree_right_rotate(root, sentinel, node->parent->parent);//node的父结点的父结点右旋 ---->跳转到子函数
}
}
下面的动画还没涉到旋转,只是染色:gif动画(包含图片和代码的演示。)
涉及的情况
情况1:插入的是根节点。
原树是空树,此情况只会违反性质2。
对策:直接把此节点涂为黑色。
情况3:变化前[当前结点为4节点]:
当前节点的父节点是红色且祖父节点的另一个子节点(叔叔节点)是红色。
对策:将当前节点的父节点和叔叔节点涂黑,祖父节点涂红,把当前节点指向祖父节点,从新的当前节点重新开始算法。
暂停的截图:
为了节省大家的时间,博主就直接点插入到我们的最坏的情况了。
因为之后会涉及到左右旋转,所以博主就将之前所写的博客直接办过来,了解的朋友可以跳过
接下来给大家介绍一下平衡二叉树的旋转(红黑树的旋转也是和这个一样的)。
右旋的情况
- LL型(右旋):此时平衡因子大于1.所以我们现在就要进行旋转了,方法如下。
以B为支点让A进行右旋
LR型(先转换成LL型在右旋):
首先我们交换B、C的位置,由性质我们知道此时B小于C所以此时我们的C就会在B的左边,就会变成第二个图片所示,现在就变成了我们的第一种情况(LL型了。我们只需要让其右旋就可以使平衡因子的绝对值不大于1)
左旋的情况!
1.RR型(左旋):RR型其实和LL行的型的情况的情况相反 我们只需要逆向思考就行。以B为支点让A进行左旋
RL型: 首先我们交换B、C的位置,由性质我们知道此时B大于C所以此时我们的C就会在B的右边,就会变成第二个图片所示,现在就变成了我们的第一种情况(RR型了。我们只需要让其右旋就可以使平衡因子的绝对值不大于1)
这就是之后要用的旋转,那么我们就开始这个最难的操作过程吧。(如果gif传不了高画质,博主就只能传低画质了,大家看懂过程即可,gif中对比的具体性质博主会精简的发到后面)
(其实也就是之前博客的动画演示)
那我们就接着之前的gif继续吧
涉及到的 3/4、5情况(精简版)
情况3:变化前[当前结点为4节点]:
当前节点的父节点是红色且祖父节点的另一个子节点(叔叔节点)是红色。
对策:将当前节点的父节点和叔叔节点涂黑,祖父节点涂红,把当前节点指向祖父节点,从新的当前节点重新开始算法。
情况4:[当前节点为7节点]:
当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点的右子
对策:当前节点的父节点做为新的当前节点,以新当前节点为支点左旋。
情况5:[当前节点为2节点]
当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点的左子
对策:父节点变为黑色,祖父节点变为红色,把祖父节点右旋
下面是屏幕截图:(实在觉得网页不够看,博主鼓励多开几个!!!)
看到没有,以上就只是对着原理进行分析都能这么长(相信大家应该知道了吧),但是有时候知道原理并不代表你能看懂代码(博主亲身体验!!)所以博主就对着代码来给大家分析一下红黑树染完色之后的左右旋吧(只讲左右旋就已经很复杂了。希望大家对红黑树的理解更加深刻!!!!!)
下面是情况四的左旋:
我就把有用的代码粘过来了:
//之前博客的情况4
} else { //如果父结点的右兄弟是黑的
if (node == node->parent->right) { //如果新结点是右子结点
node = node->parent; //父结点成为新node
ngx_rbtree_left_rotate(root, sentinel, node); //node左旋 --->跳转到子函数
}
//左旋
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; //获取当前右节点 此时temp就是当前节点的右节点了
node->right = temp->left; ///node的右节点设置为他原来右节点的左节点
if (temp->left != sentinel) { //如果右子结点的左结点不为哨兵
temp->left->parent = node; //右子结点的左子结点挂在左旋结点上
}
temp->parent = node->parent; //右节点将会变成原来node的父节点。
if (node == *root) { //是不是根节点的判断
*root = temp;
} else if (node == node->parent->left) { //然后把右节点的信息和原来node的parent进行维护
node->parent->left = temp;
} else {
node->parent->right = temp;
}
temp->left = node; //现在node变回他原来右节点的子节点了
node->parent = temp; //所以他的parent变成temp
}
染色的博主就不管了,博主这个GIF只是针对于旋转代码是如何弄的。
虽然说上面的连线是有点乱的,但是精髓博主全部都讲到了,好好理解一定能掌握的!!!!(我只能穿模糊的了,大家可以自己找到原图跟着博主的思路)
画图太小,导致有一条语句丢失,博主画完图又仔细看了一下逻辑,不然大家可能会接受到不完成的旋转
最后的图片,大家可以对照:
经过情况四之后的情况五的右旋:
ngx_rbt_black(node->parent); //node的父结点染黑
ngx_rbt_red(node->parent->parent); //node的父结点的父结点染红
ngx_rbtree_right_rotate(root, sentinel, node->parent->parent);//node的父结点的父结点右旋 ---->跳转到子函数
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; //根节点赋为左子结点
} else if (node == node->parent->right) { //如果右旋结点为右子结点
node->parent->right = temp; //左子结点挂父结点右边
} else { //否则左子结点挂父结点左边
node->parent->left = temp;
}
temp->right = node; //现在node变回他原来左节点的子节点了
node->parent = temp; //所以他的parent变成temp
}
再看看我们的插入代码是不是会有感觉了呢??
void
ngx_rbtree_insert(ngx_rbtree_t *tree, ngx_rbtree_node_t *node)
{
ngx_rbtree_node_t **root, *temp, *sentinel; //这三个都有独立的结构
/* a binary tree insert */
root = &tree->root; //树根指针赋给了root
sentinel = tree->sentinel; //哨兵指针赋给了哨兵指针
//如果树是空树的话,那么插入的结点就会变成根节点,伴随着的左右子节点就会变成哨兵
if (*root == sentinel) { //特判,如果根是哨兵,即树是空的
node->parent = NULL; //新插入的结点变成了根
node->left = sentinel; //新结点的左子结点是哨兵
node->right = sentinel; //新结点的右子结点也是哨兵
ngx_rbt_black(node); //新根染黑
*root = node; //确认新结点为新根
return; //插入结束
}
//树初始化时给了insert指针一个函数地址
//查看前面的宏ngx_rbtree_init(tree, s, i)
//发现只是把指定结点染黑,同时赋为根和哨兵,给insert指针指定一个函数
//ngx_rbtree.c中有两个参数表符合的可选函数:插入值、插入计时器值
//稍后来看两种插入分别如何实现又有什么区别
// insert 就对应 .h的那一个函数指针(61,54 行就能看懂了)
// 函数指针里面的参数有 根节点、插入结点、哨兵结点的指针
tree->insert(*root, node, sentinel); //插入操作(通过这个函数指针的调用就能找到我们的插入点了)
/* 若红黑树不为空,则按照二叉查找树的插入操作进行
* 该操作由函数指针提供(跳转到插入函数)
*/
/* re-balance tree */
//如果新结点不是根结点而且其父结点是红的,循环
while (node != *root && ngx_rbt_is_red(node->parent)) {
//最坏的情况,就是 情况:3,4,5全部走一遍
// 之前博客的情况3
if (node->parent == node->parent->parent->left) { //如果父结点是左子结点,temp 获得父结点的右兄弟
temp = node->parent->parent->right;
if (ngx_rbt_is_red(temp)) { //如果父结点的右兄弟是红的
ngx_rbt_black(node->parent); //父结点染黑
ngx_rbt_black(temp); //父结点的右兄弟染黑
ngx_rbt_red(node->parent->parent); //父结点的父结点染红
node = node->parent->parent; //父结点的父结点成为当前结点,继续去维护红黑树性质
//之前博客的情况4
} else { //如果父结点的右兄弟是黑的
if (node == node->parent->right) { //如果新结点是右子结点
node = node->parent; //父结点成为新node
ngx_rbtree_left_rotate(root, sentinel, node); //node左旋 --->跳转到子函数
}
//之前博客的情况5
ngx_rbt_black(node->parent); //node的父结点染黑
ngx_rbt_red(node->parent->parent); //node的父结点的父结点染红
ngx_rbtree_right_rotate(root, sentinel, node->parent->parent);//node的父结点的父结点右旋 ---->跳转到子函数
}
}
//与上面的情况对称
else { //如果父结点是右子结点,获得父结点的左兄弟
temp = node->parent->parent->left;
if (ngx_rbt_is_red(temp)) {//如果父结点的左兄弟是红的
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) { //如果新结点是左子结点
node = node->parent; //父结点成为当前结点
ngx_rbtree_right_rotate(root, sentinel, node); //当前结点右旋
}
ngx_rbt_black(node->parent); //当前结 点染黑
ngx_rbt_red(node->parent->parent); //当前结点父结点的父结点染红
ngx_rbtree_left_rotate(root, sentinel, node->parent->parent); //当前结点的父结点的父结点左旋
}
}
}
ngx_rbt_black(*root); //根结点染黑,因为根节点必须为黑色
}
//然后是对应ngx_rbtree_insert_pt指针的基础的结点插入函数:
//上文提到nginx提供一种机制自定义插入函数解决key值冲突,不过nginx提供几个默认的供我们使用:
//root是容器指针
//node是待插入节点指针
//sentinel是哨兵节点
//向红黑树插入节点,每个节点key都是唯一的
void
ngx_rbtree_insert_value(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node,
ngx_rbtree_node_t *sentinel)
{//根节点temp、插入结点node、哨兵结点的指针sentinel
ngx_rbtree_node_t **p;
for ( ;; ) {//无条件循环或者说死循环,等同于while(1)但节省了一个字符,为了找到插入点
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);
}
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; //获取当前右节点 此时temp就是当前节点的右节点了
node->right = temp->left; ///node的右节点设置为他原来右节点的左节点
if (temp->left != sentinel) { //如果右子结点的左结点不为哨兵
temp->left->parent = node; //右子结点的左子结点挂在左旋结点上
}
temp->parent = node->parent; //右节点将会变成原来node的父节点。
if (node == *root) { //是不是根节点的判断
*root = temp;
} else if (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; //根节点赋为左子结点
} else if (node == node->parent->right) { //如果右旋结点为右子结点
node->parent->right = temp; //左子结点挂父结点右边
} else { //否则左子结点挂父结点左边
node->parent->left = temp;
}
temp->right = node; //现在node变回他原来左节点的子节点了
node->parent = temp; //所以他的parent变成temp
}
另一种情况就是我们的镜像了,就不再演示
流程图(画的比教粗糙):
以上就是我们 NGINX有关红黑树插入的全部内容了。上面GIF有点模糊所以想要看完整的视频可以点击我的百度网盘进行观看
本博客篇幅已经够长了如果博主再讲解红黑树的删除只怕打架承受不住,所以想要了解删除的,期待博主的下一篇博客吧。