从2-4树到红黑树
对于一般的二叉搜索树来说,查找、插入、删除的平均时间复杂度都是$O(logN)$,但最坏情况是$O(N)$。而平衡树可以保证这三种方法的复杂度都是$O(logN)$,平衡树通过重构使得树的高度是平衡的,不会退化成单链。红黑树就是平衡树的一种。
本文将先介绍2-3-4树,通过2-3-4树引入到红黑树。
2-3-4树
2-3-4树也叫2-4树,是一种自平衡的数据结构。之所以这么叫是因为2-4树的节点类型有以下三种
- 2-node:存储一个key,有两个儿子节点
- 3-node:存储两个key,有三个儿子节点
- 4-node:存储三个key,有四个儿子节点
2-4树是4阶B树,查找、插入、删除的时间复杂度都是$O(logN)$。2-4树有一个重要的性质就是每一个叶子节点的深度都是相同的。
对于2-node来说,左儿子表示的范围是$(-\infty, v)$,右儿子表示的范围是$(v, +\infty)$,其他类型的节点也是同理。
为什么是2-3-4
为什么不最大化儿子节点的数量来让树的高度最小?假设所有节点都是d-node的话,查询的时间复杂度会更低吗?
假设$d = N^{1/2}$,意味着树的高度是2,那么我们在每一层通过二分查找查询正确节点的时间复杂度是$O(logN^{1/2})$,而$2logN^{1/2} = O(logN)$,并没有得到优化。2-3-4树会保证树的高度在$O(logN)$。
插入
对于2-node和3-node,都是直接插入即可。
对于4-node的插入则要麻烦一些。因为4-node已经没法直接存入值了。首先需要将中间值提取出来,存储到父节点中,剩下的左值和右值以及4个儿子节点分裂成两个2-node挂在父节点下,然后再根据上面的情况插入新值即可。
假如该4-node的父节点也是4-node节点怎么办?继续向上分裂即可。
删除
2-3-4树删除节点分比较多种情况。
如果k是在叶子结点,并且该叶子节点至少存储了两个key,则直接删除即可
如果k是在当前节点内,则执行以下三种操作之一
- 如果左儿子至少有两个key,则用值最大的key替换掉k,并删除左儿子中最大的key
- 如果右儿子至少有两个key,则用值最小的key替换掉k,并删除右儿子中最小的key
- 如果左右儿子都只有一个key,则将左儿子、右儿子和k合并成4-node,并从原节点删除k,新的4-node节点为原节点的左儿子,然后从4-node节点中删除k即可
如果k不在当前节点内,则根据正确的路径找到向下找。在这个过程中,需要保证经过的节点都至少包含两个key,因此过程中可能需要执行以下两种操作之一
- 如果即将去到的节点仅有一个key,并且兄弟节点至少有两个key,则从当前节点移动一个key到目标路径节点,从兄弟节点移动一个key到当前节点。
- 如果即将去到的节点仅有一个key,并且兄弟节点也只有一个key,则和当前节点的一个key进行合并成一个4-node,并且这个值在中间。
Red Black Tree
红黑树和2-4树是等价的,任何一颗2-4树都可以转换成红黑树。红黑树是二叉搜索树,只有2-node节点。每个节点要么是黑色,要么是红色,通过颜色可以表示2-4树中的3-node和4-node。
红黑树的高度最多会是2-4树的两倍,例如下面这个case,整体的高度还是$O(logN)$
性质
- 从root到leaf的路径上不会有连续的红色节点,即父节点和儿子节点不会都是红色(red property)
- 从root到所有leaf的路径上,黑色节点的数量是相同的(black property)
- root节点永远都是黑色(root property)
- 每一个NIL节点可以看作是黑色节点,black property需要计算上黑色NIL节点的数量
插入
插入操作是将值k插入到红黑树中,并且维持红黑树的性质。如果是一颗空树的话,则生成一个黑节点,存储k即可。若是一颗非空树,则需要执行以下操作:
- 找到合适的叶子结点位置
- 将k插入到该叶子结点的位置,并且为红色节点
- 必要时候通过旋转、变色操作来维护红黑树性质。
现在来分析一下插入到非空树的情况,假设我们要插入的节点为k,父节点为p,祖父节点为g。
- Case1:p是黑节点
如果k的父节点p是黑节点,那么直接插入即可,什么也不需要调整,性质都不会被破坏。从2-4树来看,就是2-node变成3-node的过程。
- Case2:p是红节点
如果k的父节点p是红节点,则出现了double-red
的情况,破坏了red property,此时p的父节点g(k的祖父节点)为黑节点,为了解除double-red的情况,我们需要考虑g的另一个儿子节点s(即p的兄弟节点)的颜色。
- Case2a:s是黑节点或nil
如果s是黑节点或者nil,则我们需要将k、p和g这三个节点重组。首先按照k、p、g三个节点的值进行从小到大的排序,假设排序结果是a、b、c,则将a和c设置为b的儿子节点,涂为红色,b为黑节点,同时考虑p的子树以及s是在合适的位置。
- Case2b:s是红节点
如果s是红节点,则将p和s的颜色改变成黑色,g节点的颜色改成红色(如果g节点是root则保持黑色不变)。由于g节点变成了红色,g和g的父节点可能出现double-red的情况,则根据case2a的方法调整即可。
删除
删除操作是从红黑树中找到要删除的节点x,将节点移除的过程。删除操作和插入操作一样可能会破坏红黑树的性质,删除后通过旋转和变色操作修复破坏的性质。
二叉搜索树的删除,通常不是直接删除节点本身,而是找到前驱或者后继节点,将值替换到原本要删除的节点,然后将问题转换成删除前驱或者后继节点。且前驱和后继节点只多只有一个子节点。
当前是红色节点
是红色叶子节点则直接删除即可,不影响任何性质
有一个子节点(不可能的情况)
- 有一个红色子节点,出现
double-red
情况 - 有一个黑色子节点,破坏了black property性质
- 有一个红色子节点,出现
当前是黑色节点
是黑色叶子节点,这是最复杂的情况,下面会详细讨论
- Case1:兄弟节点是红色的
- Case2:兄弟节点是黑色的且有两个黑色节点儿子
- Case3:兄弟节点是黑色的且兄弟节点左儿子是红色,右儿子是黑色的
- Case4:兄弟节点是黑色的且兄弟节点右儿子是红色的
有一个黑色子节点(不可能的情况)
- 破坏了black property性质
有一个红色子节点
- 将这个红色子节点替换自己的位置,并且变色成黑色即可
- Case1:当前是黑色叶子节点,且兄弟节点是红色的
首先将父节点和兄弟节点的颜色变换一下,然后父节点相对于兄弟节点进行旋转操作。当前仍然没有完全恢复红黑树的性质,但是转换成了Case2或Case3的情况,将根据Case2或者Case3的操作继续恢复。
- Case2: 当前是黑色叶子节点,兄弟节点是黑色且有两个黑色儿子
首先将sibling变色成红色,然后将current移动到parent节点,这时候会有两种情况,parent是红色的或者是黑色的。如果parent是红色的,则变色成黑色即可结束。否则根据情况继续修复。
- Case3:兄弟节点是黑色的且兄弟节点左儿子是红色,右儿子是黑色的
将兄弟节点的左儿子变色成黑色,兄弟节点变色成红色,对兄弟节点进行右旋,并用case4的方法继续修复。
- Case4:兄弟节点是黑色的且兄弟节点右儿子是红色的
将sibling变色成parent的颜色,将parent变成黑色,将sibling的右儿子变成黑色,对parent进行左旋,将current移至root并且变色成黑色。
代码
Red Black Tree implemented by Go
type Color byte
const (
Red Color = iota
Black
)
type Direction byte
const (
DirRoot Direction = iota
DirLeft
DirRight
)
type RBNode[T constraints.Ordered] struct {
Val T
parent *RBNode[T]
left *RBNode[T]
right *RBNode[T]
color Color
}
func newRBNode[T constraints.Ordered](v T) *RBNode[T] {
return &RBNode[T]{
Val: v,
parent: nil,
left: nil,
right: nil,
color: Red,
}
}
func (n *RBNode[T]) isLeaf() bool {
return (n.left == nil && n.right == nil)
}
func (n *RBNode[T]) isRoot() bool {
return n.parent == nil
}
func (n *RBNode[T]) isRed() bool {
return n.color == Red
}
func (n *RBNode[T]) isBlack() bool {
return n.color == Black
}
func (n *RBNode[T]) direction() Direction {
if n.parent != nil {
if n == n.parent.left {
return DirLeft
} else {
return DirRight
}
} else {
return DirRoot
}
}
func (n *RBNode[T]) sibling() *RBNode[T] {
if n.direction() == DirLeft {
return n.parent.right
} else {
return n.parent.left
}
}
func (n *RBNode[T]) hasSibling() bool {
return (!n.isRoot() && n.sibling() != nil)
}
func (n *RBNode[T]) uncle() *RBNode[T] {
return n.parent.sibling()
}
func (n *RBNode[T]) hasUncle() bool {
return (!n.isRoot() && n.parent.hasSibling())
}
func (n *RBNode[T]) grandParent() *RBNode[T] {
return n.parent.parent
}
func (n *RBNode[T]) hasGrandParent() bool {
return (!n.isRoot() && !n.parent.isRoot())
}
func (n *RBNode[T]) release() {
n.parent = nil
if n.left != nil {
n.left.release()
}
if n.right != nil {
n.right.release()
}
}
type RBTree[T constraints.Ordered] struct {
root *RBNode[T]
cnt uint
}
func NewRBTree[T constraints.Ordered]() *RBTree[T] {
return &RBTree[T]{
root: nil,
cnt: 0,
}
}
func (t *RBTree[T]) Size() uint {
return t.cnt
}
func (t *RBTree[T]) Empty() bool {
return t.cnt == 0
}
func (t *RBTree[T]) Clear() {
if t.root != nil {
t.root.release()
t.root = nil
}
t.cnt = 0
}
func search[T constraints.Ordered](n *RBNode[T], v T) (*RBNode[T], bool) {
if n == nil {
return nil, false
}
if n.Val == v {
return n, true
}
if n.Val < v {
return search(n.right, v)
}
return search(n.left, v)
}
func (t *RBTree[T]) Get(v T) (*RBNode[T], bool) {
return search(t.root, v)
}
func (t *RBTree[T]) Has(v T) bool {
_, ok := search(t.root, v)
return ok
}
func (t *RBTree[T]) Insert(v T) {
if t.root == nil {
t.root = newRBNode(v)
t.root.color = Black
t.cnt++
} else {
t.insert(t.root, v)
}
}
func (n *RBNode[T]) maintainRelationship() {
if n.left != nil {
n.left.parent = n
}
if n.right != nil {
n.right.parent = n
}
}
func (t *RBTree[T]) leftRotate(n *RBNode[T]) {
if n == nil || n.right == nil {
return
}
parent := n.parent
dir := n.direction()
successor := n.right
n.right = successor.left
successor.left = n
n.maintainRelationship()
successor.maintainRelationship()
switch dir {
case DirRoot:
t.root = successor
break
case DirLeft:
parent.left = successor
break
case DirRight:
parent.right = successor
break
}
successor.parent = parent
}
func (t *RBTree[T]) rightRotate(n *RBNode[T]) {
if n == nil || n.left == nil {
return
}
parent := n.parent
dir := n.direction()
successor := n.left
n.left = successor.right
successor.right = n
n.maintainRelationship()
successor.maintainRelationship()
switch dir {
case DirRoot:
t.root = successor
break
case DirLeft:
parent.left = successor
break
case DirRight:
parent.right = successor
break
}
successor.parent = parent
}
func (t *RBTree[T]) insert(n *RBNode[T], v T) {
if n.Val == v {
return
}
if v < n.Val {
if n.left == nil {
n.left = newRBNode(v)
n.left.parent = n
t.insertFixUp(n.left)
t.cnt++
} else {
t.insert(n.left, v)
}
} else {
if n.right == nil {
n.right = newRBNode(v)
n.right.parent = n
t.insertFixUp(n.right)
t.cnt++
} else {
t.insert(n.right, v)
}
}
}
func (t *RBTree[T]) insertFixUp(n *RBNode[T]) {
if n.isRoot() {
n.color = Black
return
}
if n.parent.isBlack() {
// case 1: parent is black
return
}
if n.parent.isRoot() {
n.parent.color = Black
return
}
if !n.hasUncle() || n.uncle().isBlack() {
// case 2a: uncle is nil or black
if n.direction() != n.parent.direction() {
parent := n.parent
if n.direction() == DirLeft {
t.rightRotate(n.parent)
} else {
t.leftRotate(n.parent)
}
n = parent
}
if n.parent.direction() == DirLeft {
t.rightRotate(n.grandParent())
} else {
t.leftRotate(n.grandParent())
}
n.parent.color = Black
n.sibling().color = Red
return
}
if n.hasUncle() && n.uncle().isRed() {
// case 2b: uncle is red
n.parent.color = Black
n.uncle().color = Black
n.grandParent().color = Red
t.insertFixUp(n.grandParent())
return
}
}
func (t *RBTree[T]) Delete(v T) {
if t.root == nil {
return
} else {
t.delete(t.root, v)
}
}
func (t *RBTree[T]) delete(n *RBNode[T], v T) {
if n == nil {
return
}
if n.Val != v {
if v < n.Val {
left := n.left
t.delete(left, v)
} else {
right := n.right
t.delete(right, v)
}
n.maintainRelationship()
return
}
// only root
if t.Size() == 1 {
t.Clear()
return
}
if n.left != nil && n.right != nil {
successor := n.right
parent := n
// find successor of current
for successor.left != nil {
parent = successor
successor = parent.left
}
// swap current and successor
n.Val, successor.Val = successor.Val, n.Val
n, successor = successor, n
parent.maintainRelationship()
}
if n.isLeaf() {
// black leaf node
if n.isBlack() {
t.deleteFixUp(n)
}
// delete
if n.direction() == DirLeft {
n.parent.left = nil
} else {
n.parent.right = nil
}
} else {
// current node has a signle child
// replace current with its child
parent := n.parent
var replacement *RBNode[T]
if n.left != nil {
replacement = n.left
} else {
replacement = n.right
}
switch n.direction() {
case DirRoot:
t.root = replacement
break
case DirLeft:
parent.left = replacement
break
case DirRight:
parent.right = replacement
break
}
if !n.isRoot() {
replacement.parent = parent
}
if n.isBlack() {
if replacement.isRed() {
replacement.color = Black
} else {
t.deleteFixUp(replacement)
}
}
n = nil
}
t.cnt--
}
func (t *RBTree[T]) deleteFixUp(n *RBNode[T]) {
if n.isRoot() {
n.color = Black
return
}
dir := n.direction()
sibling := n.sibling()
if sibling.isRed() {
// case 1: current is black and sibling is red
// step 1: recolor parent and sibling
// step 2: rotate parent
parent := n.parent
parent.color = Red
sibling.color = Black
if dir == DirLeft {
t.leftRotate(parent)
} else {
t.rightRotate(parent)
}
sibling = n.sibling()
}
var closeNephew, distantNephew *RBNode[T]
var isCloseNephewBlack, isDistantNephewBlack bool
if dir == DirLeft {
closeNephew = sibling.left
distantNephew = sibling.right
} else {
closeNephew = sibling.right
distantNephew = sibling.left
}
if closeNephew == nil || closeNephew.isBlack() {
isCloseNephewBlack = true
}
if distantNephew == nil || distantNephew.isBlack() {
isDistantNephewBlack = true
}
if isCloseNephewBlack && isDistantNephewBlack {
// case 2: sibling is black and has two black childs
if n.parent.isRed() {
n.parent.color = Black
sibling.color = Red
} else {
sibling.color = Red
t.deleteFixUp(n.parent)
}
return
} else {
if closeNephew != nil && closeNephew.isRed() {
// case 3: sibling is black and close nephew is red
closeNephew.color = Black
sibling.color = Red
if dir == DirLeft {
t.rightRotate(sibling)
} else {
t.leftRotate(sibling)
}
sibling = n.sibling()
if dir == DirLeft {
closeNephew = sibling.left
distantNephew = sibling.right
} else {
closeNephew = sibling.right
distantNephew = sibling.left
}
}
// case 4: sibling is black and distant nephew is red
sibling.color = n.parent.color
n.parent.color = Black
if distantNephew != nil {
distantNephew.color = Black
}
if dir == DirLeft {
t.leftRotate(n.parent)
} else {
t.rightRotate(n.parent)
}
t.root.color = Black
return
}
}
func (t *RBTree[T]) InOrder() []T {
res := make([]T, 0)
t.inOrder(t.root, &res)
return res
}
func (t *RBTree[T]) inOrder(n *RBNode[T], res *[]T) {
if n == nil {
return
}
if n.left != nil {
t.inOrder(n.left, res)
}
*res = append(*res, n.Val)
if n.right != nil {
t.inOrder(n.right, res)
}
}
Benchmark
goos: linux
goarch: amd64
pkg: github.com/elitedj/rbtree
cpu: AMD EPYC 7K62 48-Core Processor
BenchmarkInsert 4255707 324.2 ns/op
BenchmarkDelete 10957584 113.8 ns/op