注意:红黑树的使用的时候只有这两种情况:
- 作key,value --> 通过key查找value
- 利用中序遍历,是顺序的
一、红黑树用在哪里?
1.STL中的map???
map是一种数据结构,不能说成是红黑树的用途,只能说是红黑树的封装
2.nginx???
3.定时器???
4.进程调度的cfs
5.内存管理
红黑树在海量数据查询才有优势
二、红黑树的性质
- 每个结点是红的或者黑的–>红、黑代表的是逻辑颜色,也可用0,1或黑、白表示
- 根结点是黑的
- 每个叶子结点是黑的
- 如果一个结点是红的,则它的两个儿子都是黑的–>两个红节点不能相邻
- 对每个结点,从该结点到其子孙结点的所有路径上的包含相同数目的黑结点–>红黑树的平衡性,不是平衡所有结点,而是黑高
只有2是红黑树,1、3黑高不同,4相邻结点为红色
这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长(左右子树相差最大为2n-1倍)。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例 O(logn),这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,和 AVL 树通过约束左右子树高度不同,红黑树是通过它的四条性质来实现 “平衡状态”,在插入结点或者删除结点时,可能会造成某个结点违反了上述的某条性质,那么红黑树的做法就是通过 “重新着色” 和 “旋转” 两种方式使之重新符合性质。
为什么叫红黑树,为什么两种颜色?
红黑代表的是两种逻辑状态,目的是辨认黑高,每一个分支拥有一样的黑高,实现一种平衡
三、红黑树的实现
3.1结点的实现
typedef int KEY_TYPE; //KEY_TYPE没有写死,留一个接口方便日后修改
//无论是double还是数据结构作key都可以
typedef struct _rbtree_node {
int color;
struct _rbtree_node* left; //记得加struct
struct _rbtree_node* right;
struct _rbtree_node* parent; //parent主要用于旋转、调整
KEY_TYPE key;
void* data; //void* 表示可以接受任何类型的数据
} rbtree_node, *prbtree_node;
#include <iodstream>
using namespace std;
using KEY_TYPE = int; //等价于typedef int KEY_TYPE;
enum {red = 0, black = 1};
struct Node{
int color;
Node* left;
Node* right;
Node* parent;
KEY_TYPE key;
void* data;
}
3.2红黑树的定义
typedef struct _rbtree {
prbtree_node root;
prbtree_node nil; //定义一个通用的叶子结点,方便判断是不是叶子结点
} rbtree, *prbtree;
3.3红黑树的旋转
注意:
- 旋转不是红黑树特有的,只要是二叉树都有
- 左旋、右旋和颜色没关系,是为了作结点而进行的旋转
旋转的目的:把旋转做成原语(最基本的,最独立的)操作目的是为了更好的实现红黑树,即左旋、右旋是实现红黑树最基础的原语操作
无论左旋还是右旋,只需动3个方向,六根指针动完就🆗
3.3.1左旋
- y为x的右子树
- x的右子树指向y的左子树b
- 如果y的左子树b不是叶子,则y的左子树的parent指向x
- y->parent指向x的parent
- 如果x->key比x->parent->key小,x是x->parent的左子树,则x->parent->left指向y
- 否则x->key比x->parent->key大,x是x->parent的右子树,则x->parent->right指向y
- x->parent指向y
- y的左子树指向x
//旋转:三个方向,6根指针
void _left_rotate(prbtree T,prbtree_node x) {
prbtree_node y = x->right;
x->right = y->left;
if(y->left != T->nil) {
y->left->parent = x; //parent = x而不是x->right
}
y->parent = x->parent;
if(x->parent == T->root) { //所谓根节点是指有个根节点指针指向它而且它的parent也指向根节点,
//所以此处判断的也是x->parent而不是x
T->root = y;
}
else if(x == x->parent->left) { //这里判断是x而不是x->parent
x->parent->left = y;
}
else {
x->parent->right = y;
}
y->left = x;
x->parent = y;
}
3.3.1右旋
把左旋的代码复制过来,然后left改成right,x改成y,再检查一遍逻辑
//旋转:三个方向,6根指针
void _right_rotate(prbtree T,prbtree_node y) {
prbtree_node x = y->left;
y->left = x->right;
if(x->right != T->nil) {
x->right->parent = y; //parent = x而不是x->right
}
x->parent = y->parent;
if(y->parent == T->root) { //所谓根节点是指有个根节点指针指向它而且它的parent也指向根节点,
//所以此处判断的也是x->parent而不是x
T->root = x;
}
else if(y == y->parent->right) { //这里判断是x而不是x->parent
y->parent->right = x;
}
else {
y->parent->left = x;
}
x->right = y;
y->parent = x;
}
3.4插入
- 定义一个循环变量x,初始化指向根结点
- 当x不是叶子结点则进行循环
- 如果插入结点的key比当前结点key小,则当循环变量往左走
- 如果插入结点的key比当前结点key大,则当循环变量往右走
- 如果出现相等情况,应根据应用场景而定:
- 如果是定时器,丢掉相当丢掉了一个任务 可以增加1微秒,对实际情况影响不打又保证key的唯一性
- 如果是学生信息以学号或时间为key,则直接返回,防止重复插入
- 把新节点插入,并赋初值
3.4.1二叉树的插入
二叉树将结点插入到最底层的非叶子结点
int key_compare(KEY_TYPE* key1,KEY_TYPE* key2) {
//大于返回正数,等于返回0,小于返回负数
//只要把key_compare函数留出来就可以把红黑树做成模板
}
void rbtree_insert(prbtree T, prbtree_node z) {
prbtree x = T->root; //x是循环变量相当属于for循环的i
prbtree y = T->nil;
while(x != T->nil) {
y = x; //y一直是x的父节点,慢一步
if(z->key < x->key) { //有的对象(人)需要多个维度衡量,key比较写成函数,
//调用key_compare
x = x->left;
} else if(z->key > x->key) {
x = x->right;
} else {
return;
}
}
//退出循环后x指向的是叶子结点,所以要找到x当前位置得需要x结点的父节点
//所以需要定义一个父节点,用来找到当前位置
z->parent = y;
if(y == T.nil) { //插入的第一个结点
T->root = z;
} else if(z->key < y->key) {
y->left = z;
} else {
y->right = z;
}
z->left = T->nil;
z->right = T->nil;
}
3.4.2红黑树的插入
二叉树的方法已把结点插入到树中,那么这个树是红色还是黑色呢???要看父节点
注意:在插入之前,原本的树就是红黑树,满足红黑树的性质
为什么新插入的结点需要初始化为红色的???
- 首先黑高不好判断
- 因为原树满足红黑树性质,加入黑结点后黑高肯定不同,每次都要旋转
- 为什么是红色?红色好判断,不影响黑高
- 初始化为红色只有父结点为红色才需要调整(性质4)
- 通过二叉树方法插入新节点
- 给新节点赋初值为红色
- 如果父节点为红色需要进行循环递归调整
- 如果叔父节点也是红色,采用“重新着色”方法进行调整
- 令父结点和叔父结点为黑色,祖父结点为红色
- 贸然将祖父节点该成红色可能导致与祖父节点的父结点颜色冲突,所以将表示当前结点的指针指向祖父结点,从第3步进行循环递归
- 否则叔父结点为黑色,因为原来树就是红黑树,所以叔父结点只能为叶子结点才能满足黑高
- 判断父结点是祖父结点的左支还是右支
- 如果父结点是祖父结点的左支
- 判断新结点是父结点的左支还右支
- 如果是右支
- 需要将指向 新结点的指针 指向 新结点的指针的父结点,并以新结点的父结点进行“左旋”
- 旋转后的结果:父结点在新结点左支,新结点在祖父结点的左支,即与“新结点是父结点的左支“相同,指向当前节点的指针也相同
- 令父结点为黑,令祖父节点为红
- 以祖父结点进行”右旋“
- 如果父结点是祖父结点的右支
- 将上面”如果父结点是祖父结点的左支“代码复制,将left 该成right,将right改成left
- 即,以下内容
- 判断新结点是父结点的左支还右支
- 如果是左支
- 需要将指向 新结点的指针 指向 新结点的指针的父结点,并以新结点的父结点进行“右旋”
- 旋转后的结果:父结点在新结点右支,新结点在祖父结点的右支,即与“新结点是父结点的右支“相同,指向当前节点的指针也相同
- 令父结点为黑,令祖父节点为红
- 以祖父结点进行”左旋“
- 如果叔父节点也是红色,采用“重新着色”方法进行调整
- 新插入的节点是根结点或者调整的时候可能将根节点变成红色,所以最后把根节点致黑
#define RED 0 //谁是0谁是1无所谓,只要不同就行
#define BLACK 1
int key_compare(KEY_TYPE* key1,KEY_TYPE* key2) {
//大于返回正数,等于返回0,小于返回负数
//只要把key_compare函数留出来就可以把红黑树做成模板
}
//——————————————————以上内容最好写在上面,外部接口————————————————
void rbtree_insert_fixup(prbtree T, prbtree_node z) { //对红黑树进行调整
//if(z->parent->color == RED) { //贸然改变祖父结点颜色为红色,可能会导致与祖父的
//父结点冲突,需要用循环递归的思想
while(z->parent->color == RED) {
if(z->parent == z->parent->parent->left) {
rbtree_node *y = z->parent->parent->right; //定义叔父结点
if(y->color == RED) {
z->parent->color = BLACK;
y->color = BLACK;
z->parent->parent->color = RED;
//祖父结点一下现在都满足红黑树性质,
//将当前结点改为祖父结点,递归解决可能存在的祖父结点与其父结点的冲突
z = z->parent->parent;
} else { //祖父结点若为黑色只能是叶子结点才能使原树满足红黑树性质
if(z == z->parent->right) {
z = z->parent;
_left_rotate(T, z); //旋转后变成一条直线
}
z->parent->color = BLACK;
z->parent->parent->color = RED;
_right_rotate(T, z->parent->parent );
}
} else { //插入结点z的父节点是祖父结点的右支
//直接将左支代码复制,然后left改成right,right改成left
rbtree_node *y = z->parent->parent->left; //定义叔父结点
if(y->color == RED) {
z->parent->color = BLACK;
y->color = BLACK;
z->parent->parent->color = RED;
//祖父结点一下现在都满足红黑树性质,
//将当前结点改为祖父结点,递归解决可能存在的祖父结点与其父结点的冲突
z = z->parent->parent;
} else { //祖父结点若为黑色只能是叶子结点才能使原树满足红黑树性质
if(z == z->parent->left) {
z = z->parent;
_right_rotate(T, z); //旋转后变成一条直线
}
z->parent->color = BLACK;
z->parent->parent->color = RED;
_left_rotate(T, z->parent->parent );
}
}
}
T->root->color = BLACK; //递归可能会将根结点变成红色
}
void rbtree_insert(prbtree T, prbtree_node z) {
prbtree x = T->root; //x是循环变量相当属于for循环的i
prbtree y = T->nil;
while(x != T->nil) {
y = x; //y一直是x的父节点,慢一步
if(z->key < x->key) { //有的对象(人)需要多个维度衡量,key比较写成函数,
//调用key_compare
x = x->left;
} else if(z->key > x->key) {
x = x->right;
} else {
return;
}
}
//退出循环后x指向的是叶子结点,所以要找到x当前位置得需要x结点的父节点
//所以需要定义一个父节点,用来找到当前位置
z->parent = y;
if(y == T.nil) { //插入的第一个结点
T->root = z;
} else if(z->key < y->key) {
y->left = z;
} else {
y->right = z;
}
z->left = T->nil;
z->right = T->nil;
z->color = RED; //新节点初始化为红色
}
思考题
案例一、服务器端高并发IO的keep alive方案,满足一下几个需求
- 每个IO都是自己的时间戳
- 每个IO收到自己的beat后,重置自己的定时器
- 若IO定时没有收到beat,则执行IO的回调函数,并重置定时器
- 若再次没有收到beat,销毁IO,注销定时器
案例二、设计一个线程或者进程的运行体R与运行体调度器s的结构体
- 运行体R:包含运行状态{新建、准备、挂起{IO等待、IO等待写、睡眠、延时}、退出},运行体回调函数,回调参数
- 调度器s:包含栈指针,栈大小,当前运行体
- 调度器s:包含执行集合{就绪、延时、睡眠、等待}