平衡树
二叉搜索树
或者是一棵空树,或者是具有下列性质的二叉树:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉排序树。
但是,一个不具备平衡性的查找树可能退化为单链表,时间复杂度退化为O(N)。
二叉搜索树不具有自平衡的概念。
平衡二叉树
它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
自平衡的二叉树平衡二叉树保证了在最差的情况下,二叉树依然能够保持绝对的平衡,即左右两个子树的高度差的绝对值不超过1。但是这又会带来一个问题,那就是平衡二叉树的定义过于严格,导致每次插入或者删除一个元素之后,都要去维护二叉树整体的平衡,这样产生额外的代价又太大了。二叉搜索树可能退化成链表,而平衡二叉树维护平衡的代价开销又太大了。
红黑树
红黑树的本质其实也是对概念模型:2-3-4树的一种实现。2-3-4树是阶数为4的B树,全称为BalanceTree,平衡树。这种结构主要用于查找,最终要的特性是平衡,能够在最坏的情况下也保持O(LogN)的时间复杂度实现查找1。
**红黑树是一种含有红黑结点并能自平衡的二叉查找树。**它必须除了满足二叉搜索树的性质外,还要满足下面的性质:
性质1:每个节点要么是黑色,要么是红色。
性质2:根节点是黑色。
性质3:每个叶子节点(NIL)是黑色。
性质4:每个红色结点的两个子结点一定都是黑色。
性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点
性质的详细讲解,参照大佬的文章2。
使用场景
红黑树的使用主要是基于两个特点:
1. 查找效率O(logn),适合用于key->value的查找
2. 中序遍历是顺序的。
红黑树经典应用:
1. map,利用性质1,将key作为索引存入红黑树,通过key可以查找到value。
2. nginx中用于定时器等,利用特点2,将定时任务到期的timetamp存入红黑树,通过中序遍历,查找到红黑树中最小的节点,急最近要到期的定时任务。
3. cfs(Completely Fair Scheduler,完全调用调度),利用特点2,将进程调度的时间存入红黑树,查找最小的节点,即调度时间最短的进程,进行调度,可以做到公平调度。
4. 内存管理,利用特点1,可以快速查找到对应的内存块。
内存管理的红黑树的key是内存首地址?
a. 首地址+长度
b. 首地址+尾地址
红黑树实现
定义节点和红黑树
typedef int KEY_TYPE;
#define RED 0
#define BLACK 1
int key_compare(KEY_TYPE a,KEY_TYPE b){
}//遗留此函数 就可做模板
typedef struct _rbtree_node{
unsigned char color;
struct _rbtree_node *left;
struct _rbtree_node *right;
struct _rbtree_node *parent;
KEY_TYPE key;
void *value; //万能指针
}rbtree_node;
typedef struct _rbtree{
rbtree_node *root;
rbtree_node *nil;//叶子节点为黑,所以定义一个专用叶子节点
}rbtree;
左旋和右旋
左旋和右旋用于插入新节点后,继续符合红黑树的条件。
需要动三根线,六个指针。
void _left_rotate(rbtree *T,rbtree_node *x){
rbtree_node *y = x->right; //
//1
x->right = y->left;
if(y->left != T->nil){
y->left->parent = x;
}
//2
y->parent = x->parent;
if (x->parent == T->nil){ //x是根节点
T->root = y;
}
else if (x == x->parent->left){ //x是左子树
x->parent->left = y;
}else{ //x是右子树
x->parent->right = y;
}
//3
y->left = x;
x->parent = y;
}
可以直接将x换为y,left换成right。
void _right_rotate(rbtree *T,rbtree_node *y){
rbtree_node *x = y->left; //
//1
y->left = x->right;
if(x->right != T->nil){
x->right->parent = y ;
}
//2
x->parent = y->parent;
if (y->parent == T->nil){ //x根节点
T->root = x;
}
else if (y == y->parent->right){
y->parent->right = x;
}else{
y->parent->right = x;
}
//3
x->right = y;
y->parent = x;
}
插入
- 新插入的节点是红色的,所以只有当他的父节点是红色的才需要修正。有以下两种:
(1)父节点是祖父节点的左子树
(2)父节点是祖父节点的右子树
上面两个case又分为三个小case:
(1)叔父节点是红色;
**将父节点和叔父节点设置为黑色,祖父节点设置为红色,这样叔父节点往下就满足红黑树定义。**接着将祖父节点作为当前节点,继续递归进行修正。
(2)叔父节点是黑色,当前节点是父节点的右子树,将父亲节带你作为当前节点,以他为轴,进行左旋
(3)叔父节点是黑色,当前节点是父亲接待你的左子树,将父节点设置为黑色,祖父节点设置为红色,并以祖父节点为轴进行右旋
对于父亲节点是祖父节点右子树的情况,也可以分为三个case,上面的代码中left换位right就行,right换为left就可以。
左右子树高度最大相差n/2-1。
void rbtree_fix_up(rbtree *T,rbtree_node *z){
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);
}
}
}
}
void rbtree_insert(rbtree *T, rbtree_node *z){
rbtree_node *x = T->root;
rbtree_node *y = T->nil;
while(x != T->nil){
y = x;//y一直是x的父节点
if(z->key < x->key){
x = x->left;
}else if(z->key > x->key){
x=x->right;
}else{
return ;
}
}
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;//默认是红色
rbtree_fix_up(T,z);
}
删除节点
可能出现的组合:
红黑树中删除一个节点,遇到的各种情形就是其子节点的状态和颜色的组合,字节点共有三种:无子节点、有一个子节点、有两个子节点,颜色有红色和黑色,总共有六种组合。
组合1:被删节点无子节点,且被删节点为红色。
直接将节点删除,不破坏任何红黑树性质。
组合2:被删节点无子节点,且被删节点为黑色
组合3:被删节点有一个子节点,且被删节点为红色
这项不可能
组合4:被删节点有一个子节点,且被删节点为黑色
这种组合下,被删结点node的另一个子结点value必然为红色,此时直接将node删掉,用value代替node的位置,并将value着黑即可。
组合5或组合6
当删除节点node有两个子节点,我们先找到这个被删除节点的后继节点successor(前驱节点也可以),然后successor替换node的值,不用改变颜色,此时转换为删除node后记节点successor。
应用
案例一、服务器端高并发IO的keep alilve方案,满足一下几个需求
1. 每个IO都是自己的时间戳
2. 每个IO收到自己的beat后,重置自己的定时器
3. 若IO定时没有收到beat,则执行IO的回调函数,并重置定时器
4. 若再次没有收到beat,销毁IO,注销定时器。
参考以下实现3 。
针对服务器端高并发IO的keepalive方案,可以采用以下思路:
维护一个IO列表,其中每个IO都有一个唯一的ID和一个时间戳,表示上一次收到心跳包的时间。
对于每个IO,设置一个定时器,用于定时发送心跳包和检测是否收到心跳包。定时器的时间间隔可以根据具体情况进行调整。
当服务器接收到客户端的心跳包时,根据心跳包中携带的IO ID,找到对应的IO,并更新其时间戳。
每当一个IO收到心跳包后,重置该IO对应的定时器,以延长其生命周期。
如果一个IO的定时器超时,即表示该IO长时间没有收到心跳包,此时可以执行一个回调函数,例如发送一个警告邮件或关闭该IO连接。
如果一个IO在第二次定时器超时后仍未收到心跳包,则销毁该IO,并注销其定时器。
这样的方案可以满足上述需求,同时还可以灵活地根据实际情况进行调整和优化。需要注意的是,该方案需要考虑并发性和线程安全性,以保证多个线程之间的数据访问不会冲突。
案例二、设计一个线程或者进程的运行体R与运行体调度器S的结构体
1. 运行体R:包含运行状态{新建,准备,挂起{IO等待读,IO等待写,
睡眠,延时}, 退出},运行体回调函数,回调参数
2. 调度器S:包含栈指针,栈大小,当前运行体
3. 调度器S:包含执行集合{就绪,延时,睡眠,等待}
using namespace std;
#include<stack>
#include<iostream>
typedef void(*Callback)(int);
typedef struct _PCB{
Callback callback; //回调函数
int paramter;
int pid; // 进程或线程ID
int state; // 进程或线程状态,例如:新建,准备,挂起{IO等待读,IO等待写,睡眠,延时}, 退出}
int priority; // 进程或线程优先级
int pc; // 程序计数器,指向下一条指令
void *stack_pointer; // 运行体的栈指针
// 其他需要的信息,例如进程或线程的上下文信息
} PCB;
//回调函数
void myCallback(int paramter){
cout<<"myCallback:"<<paramter<<endl;
}
// 运行体调度器结构体
typedef struct _Scheduler{
PCB *ready_queue; // 就绪队列,用于存储所有处于就绪状态的进程或线程,当前运行体
stack<int> *process;//进程栈指针
int num_processes; // 当前活动进程或线程数
int time_slice; // 时间片,表示每个进程或线程能够执行的时间
int state;//执行集合{就绪,延时,睡眠,等待
// 其他需要的信息,例如调度算法类型,进程或线程的状态等
} Scheduler;