红黑树是一种二叉平衡搜索树(BST)的表现形式
平衡二叉树和普通二叉树的区
- 二叉树不会进行调整,只会通过递归根节点,判断新插入的节点放左子树还是右子树,若插入的节点一直是某一个方向的插入,则会出现二叉树退化成链表的现象,二叉树基于二分查找的优势就不存在了。
- 平衡二叉树每次进行增删都会对当前树结构进行调整,以保持树状结构相对完整。AVL树保持平衡的原则是左右子树的高度差不超过1,否则对最小不平衡树进行旋转(左旋&右旋)调整,已达到各子树的高度一致,同时保持了查找的效率没有产生退化。
红黑树和AVL的区别
- 红黑树的特点:
- 节点分为红色和黑色
- 根节点为黑色
- 叶子节点都为黑色
- 如果一个节点是红色,那么他的子节点都是黑色(红色节点不相邻)
- 对每个节点,到其叶子节点的所有路径上都包含相同数量的黑色节点(平衡的指标)
- AVL树在调整的时候必须遍历得到子树的高度;而红黑树可以通过附近节点的颜色选择通过改色或者旋转的方式进行调整,某些情况下只需要进行改色的方式即可实现平衡,通过旋转调整时也需要调整部分节点的颜色。
- AVL树的时间复杂度相对红黑树要更稳定
关于旋转
- 旋转的本质个人认为其实就是由于新插入节点导致所在的树结构不平衡,此时需要对不平衡的部分进行调整产生的需求。对树而言,调整的方向应该是由上直下的,对于当前节点(新插入节点)而言就需要通过调整自己所在的位置来影响到上层的节点分布,进而来影响树的平衡特征。
左旋
- 条件:当前节点Y在其父节点X的右子树上(X作为根节点)
- 做法要点:1.断开X与Y的联系(修改X的右子树指向);2.将Y的父节点变为X的父节点,Y变为X的父节点(新节点上浮,父节点下沉); 3.根据二叉搜索树左小右大的特性,将Y的左孩子变为X的右孩子,保持二叉特征
- 右旋就是左旋的镜像实现(left->right & right -> left)
- 代码实现
void rotate_left(rbtree* T, rb_node *x) // 左旋
{
rb_node *y = x->right; // 当前节点y
x->right = y->left; // 修改父节点x的右子树指向,断开与Y的关系
if(y->left != T->nil)
y->left->parent = x; // 非叶子节点则修改其父节点指向
// 修改根节点父节点的指向关系,实现y上浮
y->parent = x->parent;
if(x->parent == T->nil)
T->root = y;
else if( x == x->parent->left)
x->parent->left = y;
else
x->parent->right = y;
// x变为y的左子树,y变为x的父节点
y->left = x;
x->parent = y;
}
插入节点
- 插入节点的方式:按照二叉树的查找方式,找到放置的节点位置(都是叶子节点)
void insert_rbtree_node(rbtree *T, rb_node *z)
{
// 节点的key这里使用int
rb_node *x = T->root;
rb_node *y = T->nil;
while(x != T->nil)
{
y = x; // y 在这里是 x的父节点,因为递归结束后x是一个叶子节点,在树中采用了一个通用的nil节点作为叶子节点,所以此处需要通过一个y节点记录插入位置的父节点位置,来实现插入
if( z.key < x.key)
x = x.left;
else if( z.key > x.key )
x = x.right;
else
return; // 这里要求key唯一
}
// 递归结束,使用y作为插入的基础
z.parent = y;
if(y == T->nil)
T.root = z;
else if(x == y.left)
y.left = z;
else
y.right = z;
z.left = T.nil;
z.right = T.nil;
z.color = RED;
// 根据插入后的结果判断是否需要进行修正,调整平衡
rbtree_fixup(T, z);
}
修正节点
- 在插入节点时,默认的节点颜色是红色。原因是如果插入的颜色使用黑色,则必然破坏红黑树的特征--任意节点到叶子节点所有路径上的黑色节点数量一致,则必然要发生调整;若默认颜色使用红色,则只要插入位置的父节点不是红色就不需要进行修正,一定程度上提高了插入的效率。
- 需要修正的几种情况(此处举例的前提条件是父节点为左,叔父节点为右)
- 父节点为红色,叔父节点为红色
- 父节点为红色,叔父节点为黑色,且当前节点在父节点的左子树
- 父节点为红色,叔父节点为黑色,且当前节点在父节点的右子树
- 上述几种情况的提出,其大前提是实现修正功能重点,即当前已存在的树是一个满足要求的红黑树
- 情况1:父节点为红色,叔父节点为红色。当前情况下可以推导出其祖父节点必为黑色,从祖父节点到当前节点,每一层的颜色分配为黑-红-红,此时不满足红色节点不连续的特征,理想状态是红-黑-红,由此得到的做法就是将父节点这一层变为黑色,祖父节点变为红色,即可满足红色节点不连续且黑色节点没有变多。由于祖父节点变成红色,此时不排除祖父节点的变化产生了新插入红色节点时同样的情况,即祖父节点与曾祖父节点可能不满足红色节点不连续的条件,所以此时需要将祖父节点作为新的当前节点进行递归处理,最差情况下就是将树的根节点T->root改为了红色,此时循环结束,直接将根节点改为黑色即可,不会影响整体树的平衡。
- 情况2:父节点为红色,叔父节点为黑色。由此推导出该叔父节点为叶子节点,叶子节点必须是黑色,所以就不能像情况1中直接修改颜色,此时就需要考虑旋转。若当前节点位于父节点的左子树位置,则当前节点-父节点-祖父节点按照红-红-黑分布在一条直线上,这种分布情况对应的旋转方式是以祖父节点为根节点进行一次单旋转(右旋),通过旋转这三个节点会变成两层的分布形式,如果这两层的颜色是黑-红(父-子),则不会影响树的平衡。那么可以得到,将当前节点的父节点改为黑色,祖父节点改为红色,再进行一次旋转就可以重新实现整棵树的平衡,且无需递归上层节点,因为此时父节点是黑色,叔父节点也是黑色。
- 情况3:情况3和情况2的区别就在于当前节点是父节点的右子树还是左子树。在左子树(即当前节点-父节点-祖父节点处在一条直线的情况)的情况只需要一次旋转+2次换色即可,对于情况3,我们可以将其退化成情况2的处理。此时当前节点-父节点-祖父节点处于一条折线,结合隐藏的叶子节点,我们可以先对父节点-当前节点-当前节点的右叶子节点进行一次单旋转,以父节点为根节点,旋转后就形成了祖父节点-当前节点-父节点在直线上的情况,此时将父节点当做新的当前节点,就将情况3退化为了情况2.所以情况3的处理方式,就是执行情况2的处理办法前,多执行一次以父节点为根节点的左旋,以及将父节点变为新的当前节点。
- 代码实现
void rbtree_fixup(rbtree *T, rb_node* z)
{
while(z.parent.color == RED) // 父节点为黑色不需要处理
{
if(z.parent == z.parent.parent.left)
{
rb_node *uncle = z.parent.parent.right; // 叔父节点
if(uncle.color == RED) // 情况1
{
z.parent.color = BLACK;
uncle.color = BLACK;
z.parent.parent.color = RED;
z = z.parent.parent;
}
else
{
if( z == z.parent.right) // 情况3
{
rotate_left(T, z.parent);
z = z.parent;
}
// 情况2
z.parent.color = BLACK;
z.parent.parnet.color = RED;
rotate_right(T, z.parent.parent);
}
}
else
{
// 镜像过程 left to right & right to left
}
}
T.root.color = BLACK;
}
删除节点
红黑树几种删除的场景:
- 当前节点的兄弟节点是红色的
- 当前节点的兄弟节点是黑色的,而且兄弟节点的两个孩子都是黑色的
- 当前节点的兄弟节点是黑色的,而且兄弟节点的左孩子是红色的,右孩子是黑色的
- 当前节点的兄弟节点是黑色的,而且兄弟节点的右孩子是红色的