红黑树
定义和性质
R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种自平衡的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
性质:
性质4也可说成:
(4)从根节点到叶子节点,不会出现两个连续的红色节点。
应用
用途
运用红黑树主要用其两个性质:
A.通过键值对方式存储信息
运用:
1、多个客户端与服务器建立链接时,此时链接为socket实质就是一个int型数字。但是每个客户端都有自己的id,此时socket与客户端id有个映射关系。若有大量客户端链接时,我们就可采用红黑树的key-value(socket,id)方式来存储这个信息。以便后期查找socket与id之间的相互查找;
2、内存中使用:
整块内存还没有没配时,mallo分配内存时将其假如红黑树中,后期free时就从红黑树找对应的块将其释放;
B.使用其中序遍历的特点(按顺序存储)
1、进程的调度:
有些进程处于等待状态(在某个时刻会运行),存入红黑树中。当在某个时刻准备运行时,利用红黑树的中序遍历,将其小于此时间段的进程拿取出来,准备执行。
红黑树的操作
创建代码
typedef int KEY_TYPE;
typedef struct _rbtree_node {
//此为红黑树的性质
unsigned char color;
struct _rbtree_node *right;
struct _rbtree_node *left;
struct _rbtree_node *parent;
KEY_TYPE key;
void *value;
} rbtree_node;
typedef struct _rbtree {
rbtree_node *root;
rbtree_node *nil; //NULL 所有的叶子结点都指向他
} rbtree;
左旋
左旋和右旋都是为了调节二叉树平衡而存在的。
以x为节点进行左旋
z
x /
/ \ --(左旋)--> x
y z /
y
对x进行左旋,意味着,将“x的右孩子”设为“x的父亲节点”;即,将 x变成了一个左节点(x成了为z的左孩子)!。 因此,左旋中的“左”,意味着“被旋转的节点将变成一个左节点”。
对x进行左旋,意味着"将x变成一个左节点"。
代码实现:
//左旋,T为TREE,x为待旋转的节点
//注意6根指针:x节点与父结点的两根指针,x与右节点的两根,右节点的左子节点的两根指针
void rbtree_left_rotate(rbtree *T, rbtree_node *x) {
rbtree_node *y = x->right; // x --> y , y --> x, right --> left, left --> right
//假设的前提:x的右孩子为y
x->right = y->left; // 将 “y的左孩子” 设为 “x的右孩子”,即 将β设为x的右孩子
if (y->left != T->nil) { // 将 “x” 设为 “y的左孩子的父亲”,即 将β的父亲设为x
y->left->parent = x;
}
y->parent = x->parent; //将 “x的父亲” 设为 “y的父亲”
if (x->parent == T->nil) { //若x父结点为空结点,将y设为根节点
T->root = y;
} else if (x == x->parent->left) { //若x为其父结点的左孩子,则将Y设为“x父结点的左孩子”
x->parent->left = y;
} else { //(若x为其父结点的右孩子)则将y设为“x父结点的右孩子””
x->parent->right = y;
}
y->left = x; //将x设为y的左孩子
x->parent = y; //将x的父结点设为y
}
详情案例:
右旋
左旋和右旋都是为了调节二叉树平衡而存在的。
(以x为节点进行右旋)
y
x \
/ \ --(右旋)--> x
y z \
z
对x进行右旋,意味着,将“x的左孩子”设为“x的父亲节点”;即,将 x变成了一个右节点(x成了为y的右孩子)! 因此,右旋中的“右”,意味着“被旋转的节点将变成一个右节点”。
对x进行左旋,意味着"将x变成一个左节点"。
代码实现
//右旋
//小技巧:将rbtree_left_rotate中的左改成右,x改成y,改完后左旋就变成了右旋
void rbtree_right_rotate(rbtree *T, rbtree_node *y) {
rbtree_node *x = y->left; // 前提:这里假设y的左孩子为x。下面开始正式操作
y->left = x->right; // 将 “x的右孩子” 设为 “y的左孩子”,即 将β设为y的左孩子
if (x->right != T->nil) {
x->right->parent = y; // 将 “y” 设为 “x的右孩子的父亲”,即 将β的父亲设为y
}
x->parent = y->parent; // 将 “y的父亲” 设为 “x的父亲”
if (y->parent == T->nil) {
T->root = x; // 情况1:如果 “y的父亲” 是空节点,则将x设为根节点
} else if (y == y->parent->right) {
y->parent->right = x; // 情况2:如果 y是它父节点的右孩子,则将x设为“y的父节点的左孩子”
} else {
y->parent->left = x; // 情况3:(y是它父节点的左孩子) 将x设为“y的父节点的左孩子”
}
x->right = y; // 将 “y” 设为 “x的右孩子”
y->parent = x; // 将 “y的父节点” 设为 “x”
}
详情案例:
仔细观察上面"左旋"和"右旋"的示意图。我们能清晰的发现,它们是对称的。无论是左旋还是右旋,被旋转的树,在旋转前是二叉查找树,并且旋转之后仍然是一颗二叉查找树。
添加
注意:
1、添加前的树已经满足了红黑树的性质;
2、添加的节点为红色,这样更容易满足红黑树性质;
3、节点添加后,会有可能其父层、祖父层甚至更往上调整,所以整个是一个迭代的过程,且迭代一次后要将记录z指向下一次迭代的节点,一般是最高层。
为了满足红黑树的5条性质,一般插入结点时,将结点设置为红色,这样不会改变黑色的高度,这样更容易满足性质。
若当前结点为红色,待插入的位置其父结点也为红色,就要进行调整,recolor。
需要调整分为三种情况:
- (Case 1)叔叔是红色
1.1 现象说明
当前节点(即,被插入节点)的父节点是红色,且当前节点的祖父节点的另一个子节点(叔叔节点)也是红色。
1.2 处理策略
(01) 将“父节点”设为黑色。
(02) 将“叔叔节点”设为黑色。
(03) 将“祖父节点”设为“红色”。
(04) 将“祖父节点”设为“当前节点”(红色节点);即,之后继续对“当前节点”进行操作。
2. (Case 2)叔叔是黑色,且当前节点是右孩子
2.1 现象说明
当前节点(即,被插入节点)的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的右孩子
2.2 处理策略
(01) 将“父节点”作为“新的当前节点”。
(02) 以“新的当前节点”为支点进行左旋。
3. (Case 3)叔叔是黑色,且当前节点是左孩子
3.1 现象说明
当前节点(即,被插入节点)的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的左孩子
3.2 处理策略
(01) 将“父节点”设为“黑色”。
(02) 将“祖父节点”设为“红色”。
(03) 以“祖父节点”为支点进行右旋。
//在插入之前就已经是一颗红黑树了,插入是要红色容易满足性质,
//黑色会改变此路径下黑色节点的高度,就需要不断调整
//什么情况下需要调整?插入红节点会违背性质4,也就是当前节点是红色,同时父结点也是红色的,此时需要调整
void rbtree_insert(rbtree *T, rbtree_node *z) {
//红黑树只能在叶子节点插入
rbtree_node *y = T->nil;
rbtree_node *x = T->root;
while (x != T->nil) { //查找待插入的位置
y = x;
if (z->key < x->key) {
x = x->left;
} else if (z->key > x->key) {
x = x->right;
} else { //Exist
return ;
}
}//找到了待插入的叶子节点处,为y
z->parent = y; //
if (y == T->nil) { //将z结点插入其中
T->root = z;//设为根节点
} else if (z->key < y->key) {
y->left = z;//设为Y的左子树
} else {
y->right = z;//设为y的右子树
}
//插入的点性质修改,因为两个节点之间的有两个指针联系,都要记得修改
z->left = T->nil;
z->left = T->nil;
z->right = T->nil;
z->color = RED; //新结点置成红色
rbtree_insert_fixup(T, z); //调整结点
}
//相邻的节点不可能为一黑一红 , //调整过程中,z始终是为红色
void rbtree_insert_fixup(rbtree *T, rbtree_node *z) {
//在调整过程中,还需要看其上层是否满足性质,直到根节点,所以要while循环来迭代
while (z->parent->color == RED) { //z ---> RED 违背了性质4,根节点到叶子节点路径不可有连续的红节点
if (z->parent == z->parent->parent->left) { //若parent为祖父节点的左子树,便于区分左旋右旋 //父红,且为左子树
rbtree_node *y = z->parent->parent->right; //获取z的叔父结点
if (y->color == RED) {//情况1:若叔父结点为红色,(若z的父节点和叔父结点都为红色,则祖父结点一定为黑色,红黑树性质决定的) //此时叔为红色节点,父红,违背了性质4,要将父、叔改为黑,祖父改为红
z->parent->color = BLACK; //将父结点/叔父结点置黑色
y->color = BLACK;
z->parent->parent->color = RED; //祖父结点置红
z = z->parent->parent; //z --> RED //注意:此为迭代的关键://迭代的关键,下次迭代判断是否满足红黑树性质从其祖父节点开始
//所以z一直是红色的
} else { //情况2: 若叔父结点为黑色,叔父的黑高比z,违背了性质5
if (z == z->parent->right) { //若z为父的右子树,将其父节点进行左转
z = z->parent; //
rbtree_left_rotate(T, z); //左旋,
}
//z为父的左子树时,将父结点转黑,祖父节点转红,对祖父节点进行右转。
z->parent->color = BLACK;
z->parent->parent->color = RED;
rbtree_right_rotate(T, z->parent->parent);
}
}else { //父结点为祖父节点的右子树,父为红节点
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; //z --> RED 重点:因为是继续迭代的,z要指向修改后的,下次要迭代的节点,也就是祖父节点
} else { //若叔父节点为黑色,父结点为祖父的右子树,父为红节点
if (z == z->parent->left) {
z = z->parent;
rbtree_right_rotate(T, z);
}
z->parent->color = BLACK;
z->parent->parent->color = RED;
rbtree_left_rotate(T, z->parent->parent);
}
}
}
T->root->color = BLACK;
}
删除
二叉排序树
性质
二叉查找树(BST)具备什么特性呢?
1.左子树上所有结点的值均小于或等于它的根结点的值。
2.右子树上所有结点的值均大于或等于它的根结点的值。
3.左、右子树也分别为二叉排序树。
优点
利用二分查找的思想,使得查找所需的最大次数等于二叉查找树的高度。插入结点时,也是一层一层的比较大小,找到新结点适合插入的位置。
缺点
当插入的值几乎都比根节点小/大时,此时的二叉树就类似与链表的结构,这样效率就大打折扣。
为了解决 :二叉查找树多次插入结点可能导致不平衡的问题 ,就创建了自平衡的二叉查找树,也就是红黑树。
实现代码
创建二叉树:
typedef int KEY_VALUE;
struct bstree_node {
KEY_VALUE data;
struct bstree_node *left;
struct bstree_node *right;
};
struct bstree {
struct bstree_node *root;
};
struct bstree_node *bstree_create_node(KEY_VALUE key) {
struct bstree_node *node = (struct bstree_node*)malloc(sizeof(struct bstree_node));
if (node == NULL) {
assert(0);
}
node->data = key;
node->left = node->right = NULL;
return node;
}
二叉树的插入:
int bstree_insert(struct bstree *T, int key) {
assert(T != NULL);
if (T->root == NULL) {//若为空树,直接赋值
T->root = bstree_create_node(key);
return 0;
}
struct bstree_node *node = T->root;
struct bstree_node *tmp = T->root;
//查找该插入的位置
while (node != NULL) { //直到Node为底层了
tmp = node; //先遍历确定待插入的位置
if (key < node->data) {
node = node->left;
} else {
node = node->right;
}
}
if (key < tmp->data) {
tmp->left = bstree_create_node(key);
} else {
tmp->right = bstree_create_node(key);
}
return 0;
}
二叉树的中序遍历:
int bstree_traversal(struct bstree_node *node) {
//先序,中序,后序遍历,这个是代表根节点的顺序(每个节点会遇到三次)
//先序:跟节点都会遇到3次,遇到第一次打印;
//中序:根节点遇到第二次打印;
//后序:根节点遇到第三次打印;
if (node == NULL) return 0;
//printf("%4d ", node->data); //先序
bstree_traversal(node->left);
printf("%4d ", node->data); //中序
bstree_traversal(node->right);
//printf("%4d ", node->data); //后序
}