1. 什么是红黑树?
1.1 概述
红黑树实际上是一种自平衡二叉查找树。
二叉树是什么?二叉树是每个节点最多有两个子树的树结构,每个节点都可以用于存储数据,可以由任 1 个节点访问它的左右
子树或父节点。二叉查找树是什么?二叉查找树或者是一棵空树,是具有下列性质的二叉树。
- 每个节点都有一个作为查找依据的关键码(key),所有节点的关键码互不相同。
- 左子树(如果存在)上所有节点的关键码都小于根节点的关键码。
- 右子树(如果存在)上所有节点的关键码都大于根节点的关键码。
- 左子树和右子树也是二叉查找树。
这样,一棵二叉查找树的所有元素节点都是有序的。在二叉树的形态比较平衡的情况下,它的检索效率很高,有些类似与二分
法检索有序数组的效率。一般情况下,查询复杂度是与目标节点到根节点的距离(即深度)有关的。然而,不断地添加、删除 节点,可能造成二叉查找树形态非常不平衡,在极端情形下它会变成单链表,检索效率也就会变得低下。自平衡二叉查找树是什么?在不断地向二叉查找树中添加、删除节点时,二叉查找树自身通过形态的变换,始终保持着一定程
度上的平衡,即为自平衡二叉查找树。红黑树是一种自平衡性较好的二叉查找树。ngx_rbtree_t 红黑树容器中的元素都是有序的,它支持快速的检索、插入、删除操作,也支持范围查询、遍历等操作。
1.2 红黑树特性
红黑树是指每个节点都带有颜色属性的二叉查找树,其中颜色为红色或黑色。除了二叉查找树的一般要求以外,对于红黑树
还有如下的特性:- 特性1:节点是红色或黑色。
- 特性2:根节点是黑色。
- 特性3:所有叶子节点都是黑色(叶子是 NIL 节点,也叫 “哨兵”)。
- 特性4:每个红色节点的两个子节点都是黑色(每个叶子节点到根节点的所有路径上不能有两个连续的红色节点)。
- 特性5:从任一节点到每个叶子节点的所有简单路径都包含相同数目的黑色节点。
这些约束加强了红黑树的关键性质:从根节点到叶子节点的最长可能路径长度不大于最短可能路径的两倍,这样这个树大致
上就是平衡了。示例:依次往空的 ngx_rbtree_t 红黑树容器中添加元素 1、6、8、11、13、15、17、22、25、27 后,会形成如下的红黑树:
2. ngx_rbtree_t 红黑树的实现
2.1 相关结构体
2.1.1 ngx_rbtree_node_t:红黑树的节点结构体
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; /* 节点的颜色,0 表示黑色,1 表示红色 */ u_char color; /* 仅 1 字节的节点数据。由于表示的空间太小,一般很少使用 */ u_char data;};
ngx_rbtree_node_t 是红黑树实现中必须用到的数据结构,一般把它放到结构体中的第 1 个成员中,这样方便把自定义的结
构体强制成 ngx_rbtree_node_t 类型。 ngx_rbtree_node_t 结构体中的 key 成员是每个红黑树节点的关键字,它必须是整型。红黑树的排序主要依据 key 成员 (自定义 ngx_rbtree_insert_pt 方法后,节点的其他成员也可以在 key 排序的基础上影响红黑树的形态)。2.1.2 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; /* 指向 NIL 哨兵节点 */ ngx_rbtree_node_t *sentinel; /* 表示红黑树添加元素的函数指针,它决定在添加新节点时的行为究竟是替换还是新增 */ ngx_rbtree_insert_pt insert;};
红黑树是一个通用的数据结构,它的节点(或者称为容器的元素)可以是包含基本红黑树节点的任意结构体。对于不同的结
构体,很多场合下是允许不同的节点拥有相同的关键字的。例如,不同的字符串可能会散列出相同的关键字,这时它们在红 黑树中的关键字是相同的,然而它们又是不同的节点,这样在添加时就不可以覆盖原有同名关键字的节点,而是作为新插入 的节点存在。因此,将添加元素的方法抽象出 ngx_rbtree_insert_pt 函数指针可以很好地实现这一思想。2.2 提供的接口
2.2.1 ngx_rbtree_init:初始化一棵红黑树
/* 设置该节点颜色为红色 */#define ngx_rbt_red(node) ((node)->color = 1)/* 设置该节点颜色为黑色 */#define ngx_rbt_black(node) ((node)->color = 0)/* a sentinel must be black *//* 初始化一个哨兵节点,哨兵节点(即叶子节点)一定是黑色的 */#define ngx_rbtree_sentinel_init(node) ngx_rbt_black(node)/* * 参数含义: * - tree:是红黑树容器的指针 * - s:是哨兵节点的指针 * - i:是ngx_rbtree_insert_pt类型的节点添加方法 * * 执行意义: * 初始化红黑树,包括初始化根节点、哨兵节点、ngx_rbtree_insert_pt节点添加方法 */#define ngx_rbtree_init(tree, s, i) \ ngx_rbtree_sentinel_init(s); \ (tree)->root = s; \ (tree)->sentinel = s; \ (tree)->insert = i
2.2.2 ngx_rbtree_insert: 向红黑树中添加节点
/* * 参数含义: * - tree:是红黑树容器的指针 * - node:是需要添加到红黑树的节点指针 * * 执行意义: * 向红黑树中添加节点,该方法会通过旋转红黑树保持树的平衡. */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; 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)) { 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) { node = node->parent; ngx_rbtree_left_rotate(root, sentinel, node); } 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)) { 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);}
2.2.3 ngx_rbtree_insert_value: 插入方法一
/* * 参数含义: * - temp:是红黑树容器的指针 * - node:是待添加元素的ngx_rbtree_node_t成员的指针 * - sentinel:是这棵红黑树初始化时哨兵节点的指针 * * 执行意义: * 向红黑树添加数据节点,每个数据节点的关键字都是唯一的,不存在同一个 * 关键字有多个节点的问题. */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 ( ;; ) { /* 首先比较 key 关键字,红黑树中以 key 作为第一索引关键字 */ p = (node->key < temp->key) ? &temp->left : &temp->right; /* 如果当前节点是哨兵节点,则跳出循环准备插入节点 */ if (*p == sentinel) { break; } /* 若不是哨兵,则继续遍历下一个节点 */ temp = *p; } /* 将 node 插入到该位置 */ *p = node; node->parent = temp; /* 左右子节点都是哨兵节点 */ node->left = sentinel; node->right = sentinel; /* 将节点颜色置为红色。注意,红黑树的ngx_rbtree_insert方法会在 * 可能的旋转操作后重置该节点的颜色 */ ngx_rbt_red(node);}
2.2.4 ngx_rbtree_min: 找到当前节点及其子树中最小节点
/* * 参数含义: * - node:是红黑树中ngx_rbtree_node_t类型的节点指针 * - sentinel:是这棵红黑树的哨兵节点 * * 执行意义: * 找到当前节点及其子树中最小节点(按照key关键字) */static ngx_inline ngx_rbtree_node_t *ngx_rbtree_min(ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel){ while (node->left != sentinel) { node = node->left; } return node;}