红黑树-“旋转”实现自平衡概念理解(动图演示)
红黑树(Red-Black Tree),又称“R-B树”,属于“二叉查找树”的一种,但它比较特殊,能够实现树结点的“自平衡”特性,但这种平衡只是近似的,不是绝对平衡。而这一特性使得红黑树能够保证在最坏情况下,基本动态集合的操作时间复杂度为O(lgn)。树中每个结点包含5个属性:key、color、left、right 和 p。
根据《算法导论》中,红黑树的描述定义为:
- 每个结点或是红色的,或是黑色的;
- 根结点是黑色的;
- 每个叶结点(NIL / NULL)是黑色的;
- 如果一个结点是红色的,则它的两个子结点都是黑色的;
- 对每个结点,从该结点到其后代叶结点的简单路径上,均包含相同数目的黑色结点。(概念:根据性质5,所得到的黑色结点数,又称为“黑高(black-height)",记作 bh(x),注意不包含当前结点,“NIL / NULL”的黑高为0)。
以该图的红黑树为例,将“H”结点视为当前结点,而此时的从“H”结点开始的路径1、2、3中所包含的黑色结点数目一致,任意结点都符合性质5。
红黑树主要通过“旋转” + 红黑色约束来实现“自平衡”,首先阅读一下来自《算法导论》中关于“左旋”的伪代码描述,理解后“右旋”的原理也大致大致:
LEFT-ROTATE的伪代码中,假设x.right != T.nil,且根结点的父结点为T.nil
y = x.right //set y
x.right = y.left //turn y's left subtree into x's right subtree
if y.left != T.nil
y.left.p = x
y.p = x.p //link x's parent to y
if x.p == T.nil
T.root = y
else if x == x.p.left
x.p.left = y
else x.p.right = y
y.left = x
x.p = y
“左旋演示”
在以下红黑树插入新结点“0008”为例,演示整个“左旋”的过程细节:
上述“左旋”的详细执行步骤:
- 插入“0008”结点;
- 0008 >= 0002,看右子树;
- 0008 >= 0004, 看右子树;
- 0008 >= 0006, 看右子树;
- 0008 >= 0007, 看右子树;
- 发现空树或叶子结点,插入元素;
- 当前结点为0008,结点0008和父结点0007都是红色,叔叔结点0005也是红色(从当前结点0008的祖父结点0006推算得到),于是将叔叔结点0005和父结点0007的颜色改为黑色,而祖父结点改为红色;
- 当前结点为0006,结点0006和父结点0004都是红色,当前结点是右子结点,父结点是右子结点,需要进行旋转;
- 进行左旋,结点0004成为根节点且由红变黑色,结点0002变成结点0004的左子树结点且由黑变红色,而结点0004的原左子树结点0003成为结点0002的右子树结点;
- 左旋完成,实现了二叉树自平衡。
“右旋”演示
在以下红黑树插入新结点“0001”为例,演示整个“右旋”的过程细节:
上述“右旋”的详细执行步骤:
- 插入新结点“0001”;
- 0001 < 0007,看左子树;
- 0001 < 0005,看左子树;
- 0001 < 0003,看左子树;
- 0001 < 0002,看左子树;
- 发现空树或叶子结点,插入元素;
- 当前结点0001和父结点0002都是红色,叔叔结点0004也是红色(从当前结点0001的祖父结点0003推算得到),于是将叔叔结点0004和父结点0002的颜色改为黑色,而祖父结点0003改为红色;
- 当前结点0003和父结点0005都是红色的,当前结点0003是左子结点,父结点0005是左子结点,需要进行旋转;
- 进行右旋,结点0005成为根节点且由红变黑色,结点0007变成结点0005的右子树结点且由黑变红色,而结点0005的原左子树结点0006成为结点0007的左子树结点;
- 右旋完成,实现了二叉树自平衡。
说明:红黑树的概念远不止上述的内容,有兴趣的小伙伴可以自行深入学习,本文主要是为了辅助理解“旋转”这一抽象过程。
参考自:
《算法导论》
动图在线演示网址:https://www.cs.usfca.edu/~galles/visualization/RedBlack.html