红黑树
红黑树的简介
红黑树是一种自平衡的二叉查找树,是一种高效的查找树。它是由 Rudolf Bayer 于1972年发明,在当时被称为对称二叉 B 树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的红黑树。
红黑树具有良好的效率,它可在 O(logN) 时间内完成查找、增加、删除等操作。因此,红黑树在业界应用很广泛,比如 Java 中的 TreeMap,JDK 1.8 中的 HashMap、C++ STL 中的 map 均是基于红黑树结构实现的。
考虑到红黑树是一种被广泛应用的数据结构,所以我们很有必要去弄懂它。
红黑树的性质
- 每个节点是红的或者黑的
- 根节点是黑的
- 每个叶子节点是黑的
- 如果一个结点是红的,则它的两个儿子都是黑的。【这一点说明如果红黑树中的一个结点是红色,那么它的父节点一定是黑色,因为如果父节点是红色,则对于父节点来说,不满足它的两个儿子都是黑色的性质;也说明了从根节点到叶子结点的所有路径上不能有2个连续的红色结点。】
- 对每个结点,从该结点到其子孙结点的所有路径上的包含相同数目的黑结点。
红黑树的旋转
红黑树在性质被破坏时会旋转。旋转包括左旋和右旋。
红黑树的插入
红黑树在插入结点之前,它已经是一颗红黑树。若插入结点z为红色结点。违背了性质4。插入红黑树。z的父节点也是红色,z的祖父结点是黑色,z的叔父结点不确定。
-
叔父结点是红色的【把父节点和叔父结点变为黑色,把祖父结点变为红色。】
-
若叔父结点是黑色的。z在左子树。树的左边比较重,通过右旋变成右边的样子。
为什么要旋转?插入结点的这一端比较重,结点数量比较多。
如何判断哪边重?两颗红色结点相邻,并且当前结点z是左孩子。
1 把z的父节点改为黑色,祖父结点改为红色,然后进行旋转。
图1 调整出来的现象: z是黑色, 调整变为红色(89、145由红变黑,140由黑变红)变色:1. 把z的父节点和叔父结点变为黑色;2. 把z的祖父结点变为红色。
-
如果z的叔父结点是黑色结点,当前结点z是右孩子。
这种状态不能一步到位。需要转成中间状态,如图2的右边所示。
图2 转成中间状态
对应的代码是:
if (z == z->parent->right) //需要进行2次调整
{
z = z->parent;
rbtree_left_rotate(T, z);
}
图3 由中间状态转成最终状态
z->parent->color = BLACK;
z->parent->parent->color = RED;
//以z的祖父结点进行旋转
rbtree_right_rotate(T, z->parent->parent);
插入总共以上三种状态。
红黑树的删除更加复杂,先放下。红黑树很经典,如果为了开发,了解性能、时间复杂度即可。自己实现一个红黑树在工作中的可能性极低。
总代码:
#include <iostream>
using namespace std;
#define RED 0
#define BLACK 1
typedef int KEY_TYPE; /*就是说KEY_TYPE现在是一种和int类型等价的类型。*/
typedef struct _rbtree_node
{
KEY_TYPE key;
void* value;
struct _rbtree_node* right;
struct _rbtree_node* left;
struct _rbtree_node* parent;
unsigned char color;
}rbtree_node;
typedef struct _rbtree {
struct _rbtree_node* root;/*根节点*/
struct _rbtree_node* nil;/*叶子节点*/
}rbtree;
//左旋、右旋应该带哪些参数? 红黑树【因为需要判断它的左子树、右子树是不是叶子节点,还需要判断父亲节点是不是根节点】
/*
* 左旋 三根指针方向修改 每个方向有两根
* 6根指针,3对
*/
void rbtree_left_rotate(rbtree* T, rbtree_node* x)
{
rbtree_node* y = x->right; //x的右子树
x->right = y->left;//两个都要变
if (y->left != T->nil) //非叶子节点才有parent
{
y->left->parent = x;
}
y->parent = x->parent;
//x可能是根节点,判断是左子树还是右子树
if (x->parent == T->nil)
{
T->root = y;
}//如果是左子树
else if (x == x->parent->left)
{
x->parent->left = y;
}
else
{
x->parent->right = y;
}
y->left = x;
x->parent = y;
}
void rbtree_right_rotate(rbtree* T, rbtree_node* y)
{
rbtree_node* x = y->right; //x的右子树
y->left = x->right;//两个都要变
if (x->right != T->nil) //非叶子节点才有parent
{
x->right->parent = y;
}
x->parent = y->parent;
//x可能是根节点,判断是左子树还是右子树
if (y->parent == T->nil)
{
T->root = x;
}//如果是左子树
else if (y == y->parent->right)
{
y->parent->right = x;
}
else
{
x->parent->left = x;
}
x->left = y;
y->parent = x;
}
/*
* 插入需要调整的函数
*
*/
void rbtree_insert_fixup(rbtree* T, rbtree_node* z)
{
//z = RED
while (z->parent->color == RED)
{
//如果z的父亲是z的祖父的左子树
if (z->parent == z->parent->parent->left)
{
rbtree_node* y = z->parent->parent->right; //叔父结点:祖父结点的右边
if (y->color == RED)
{
z->parent->color = BLACK; //z的父节点变为黑色
y->color = BLACK; //z的叔父结点变为黑色
z->parent->parent->color = RED; //z的祖父结点变为红色
//回溯【不太明白】
z = z->parent->parent; //z==RED 才会保证z.parent==RED需要修改
}
else //如果叔叔结点是黑色的 y == BLACK
{
if (z == z->parent->right) //需要进行2次调整
{
z = z->parent;
rbtree_left_rotate(T, z);
}
z->parent->color = BLACK;
z->parent->parent->color = RED;
//以z的祖父结点进行旋转
rbtree_right_rotate(T, z->parent->parent);
}
}
else
{
}
}
}
/*
* 插入
* @param1: tree
* @param2 需要插入的节点: node
*/
void rbtree_insert(rbtree *T, rbtree_node *z)
{
rbtree_node* x = T->root; //得到根节点
rbtree_node* y = T->nil;//首先等于空节点
//一个遍历的过程 循环遍历终止的条件是什么?
//需要两个遍历的指针,当x指向下一个,y始终指向x之前的位置
while (x!=T->nil) //等于叶子节点就跳出循环
{
y = x;
if (z->key < x->key) //如果要插入节点比当前节点小,插入左子树,大,插入右子树
{
x = x->left;
}
else if (z->key > x->key)
{
x = x->right;
}
else {//等于就是已经存在了 暂时不退出
return;
}
}
//如果红黑树一个节点也没有。x、y都指向空节点
if (y == T->nil)
{
T->root = z;
}
else
{
//插入节点z
if (y->key > z->key)
{
y->left = z;
}
else
{
y->right = z;
}
}
z->parent = y;
z->left = T->nil;
z->right = T->nil;
z->color = RED;
rbtree_insert_fixup(T, z);
}
int main()
{
cout << "yes" << endl;
return 0;
}
拓展:红黑树的应用
1. Linux进程调度CFS
CFS调度算法在Linux内核中使用了红黑树。CFS算法通过使用红黑树来维护进程优先级和时间片的分配,使得每个进程都能公平地获得CPU时间片。CFS算法将所有可运行的进程按照优先级放置在红黑树上,每个进程的优先级被转化为一个虚拟运行时间,运行时间越短的进程优先级越高,运行时间越长的进程优先级越低。当一个进程运行时,它的虚拟运行时间会不断增加,表示它已经使用了一定的CPU时间片。进程调度器会选择虚拟运行时间最小的进程运行,如果有多个虚拟运行时间相同的进程,进程调度器会选择其中最先插入红黑树的进程运行。这样就能保证每个进程都能公平地获得CPU时间片。
2. Nginx Timer事件管理
Nginx的Timer事件管理使用了红黑树。在Nginx中,Timer事件是通过红黑树这种数据结构进行管理的。每个红黑树的节点代表一个定时器事件,节点的值是定时器的超时时间戳。红黑树的最左边的节点代表距离超时时间最近的事件,最右边的节点代表距离超时时间最远的事件。添加、删除、查找定时器事件实际上就是对应于红黑树的插入、删除和查找节点的操作。
3. Epoll事件块的管理
epoll内部使用红黑树管理事件块。epoll是Linux中的一种I/O多路复用技术,用于高效地处理大量文件描述符。在内核实现中,epoll使用了红黑树这种高效数据结构来管理事件块(文件描述符)。具体来说,每个事件块可以对应多个文件描述符,这些文件描述符通过红黑树进行管理,使得查找、添加、删除事件块的操作更加高效。
4. map和unordered_map
map在内部使用红黑树作为底层实现。C++标准库中的map容器是基于红黑树实现的,红黑树是一种自平衡的二叉搜索树,能够保证查找、插入和删除操作的时间复杂度为O(log n)。而unordered_map则使用哈希表作为底层实现,是一种基于哈希表的数据结构,它能够以O(1)的时间复杂度进行查找、插入和删除操作,但是需要解决哈希冲突的问题。
总结:感觉红黑树是一个平衡的二叉树,不会出现某一端重的情况,如果出现了也会进行调整变成平衡的二叉树,它的查找、插入、删除操作的事件复杂度为O(logn)。
参考:https://zhuanlan.zhihu.com/p/91960960