某些教程不区分普通红黑树和左倾红黑树的区别,直接将左倾红黑树拿来教学,而且称其为红黑树,由于左倾红黑树与普通的红黑树相比,实现起来较为简单,容易教学。在这里,咱们区分开左倾红黑树和普通红黑树。node
红黑树是一种近似平衡的二叉查找树,从2-3
树或2-3-4
树衍生而来。经过对二叉树节点进行染色,染色为红或黑节点,来模仿2-3
树或2-3-4
树的3节点和4节点,从而让树的高度减少。2-3-4
树对照实现的红黑树是普通的红黑树,而2-3
树对照实现的红黑树是一种变种,称为左倾红黑树,其更容易实现。算法
使用平衡树数据结构,能够提升查找元素的速度,咱们在本章介绍2-3
树,再用二叉树形式来实现2-3
树,也就是左倾红黑树。segmentfault
2-3
树是一棵严格自平衡的多路查找树,由1986年图灵奖得主,美国理论计算机科学家John Edward Hopcroft
在1970年发明,又称3阶的B树
(注:B
为Balance
平衡的意思)数组
它不是一棵二叉树,是一棵三叉树。具备如下特征:数据结构
由于2-3
树的第二个特征,它是一棵完美平衡的树,很是完美,除了叶子节点,其余的节点都没有空儿子,因此树的高度很是的小。并发
如图:数据结构和算法
若是一个内部节点拥有一个数据元素、两个子节点,则此节点为2节点。若是一个内部节点拥有两个数据元素、三个子节点,则此节点为3节点。函数
能够说,全部平衡树的核心都在于插入和删除逻辑,咱们主要分析这两个操做。性能
在插入元素时,须要先找到插入的位置,使用二分查找从上自下查找树节点。学习
找到插入位置时,将元素插入该位置,而后进行调整,使得知足2-3
树的特征。主要有三种状况:
如图(来自维基百科):
核心在于插入3节点后,该节点变为临时4节点,而后进行分裂恢复树的特征。最坏状况为插入节点后,每一次分裂后都致使上一层变为临时4节点,直到树根节点,这样须要不断向上分裂。
临时4节点的分裂,细分有六种状况,如图:
与其余二叉查找树由上而下生长不一样,2-3
树是从下至上的生长。
2-3
树的实现将会放在B树
章节,咱们将会在此章节实现其二叉树形式的左倾红黑树结构。
删除操做就复杂得多了,请耐心阅读理解。
2-3
树的特征注定它是一棵很是完美平衡的三叉树,其全部子树也都是完美平衡,因此2-3
树的某节点的儿子,要么都是空儿子,要么都不是空儿子。好比2-3
树的某个节点A
有两个儿子B
和C
,儿子B
和C
要么都没有孩子,要么孩子都是满的,否则2-3
树全部叶子节点到根节点的长度一致这个特征就被破坏了。
基于上面的现实,咱们来分析删除的不一样状况,删除中间节点和叶子节点。
状况1:删除中间节点
删除的是非叶子节点,该节点必定是有两棵或者三棵子树的,那么从子树中找到其最小后继节点,该节点是叶子节点,用该节点替换被删除的非叶子节点,而后再删除这个叶子节点,进入状况2。
如何找到最小后继节点,当有两棵子树时,那么从右子树一直往左下方找,若是有三棵子树,被删除节点在左边,那么从中子树一直往左下方找,不然从右子树一直往左下方找。
状况2:删除叶子节点
删除的是叶子节点,这时若是叶子节点是3节点,那么直接变为2节点便可,不影响平衡。可是,若是叶子节点是2节点,那么删除后,其父节点将会缺失一个儿子,破坏了满孩子的2-3
树特征,须要进行调整后才能删除。
针对状况2,删除一个2节点的叶子节点,会致使父节点缺失一个儿子,破坏了2-3
树的特征,咱们能够进行调整变换,主要有两种调整:
看图说话:
若是被删除的叶子节点有兄弟是3节点,那么从兄弟那里借一个值填补被删除的叶子节点,而后兄弟和父亲从新分布调整位置。下面是从新分布的具体例子:
能够看到,删除100
,从兄弟那里借来一个值80
,而后从新调整父亲,兄弟们的位置。
若是兄弟们都是2节点呢,那么就合并节点:将父亲和兄弟节点合并,若是父亲是2节点,那么父亲就留空了,不然父亲就从3节点变成2节点,下面是合并的两个具体例子:
能够看到,删除80
,而兄弟节点60
和父亲节点90
都是个2节点,因此父亲下来和兄弟合并,而后父亲变为空节点。
能够看到,删除70
,而兄弟节点都为2节点,父亲节点为3节点,那么父亲下来和其中一个兄弟合并,而后父亲从3节点变为2节点。
可是,若是合并后,父亲节点变空了,也就是说有中间节点留空要怎么办,那么能够继续递归处理,如图:
中间节点是空的,那么能够继续从兄弟那里借节点或者和父亲合并,直到根节点,若是到达了根节点呢,如图:
递归到了根节点后,若是存在空的根节点,咱们能够直接把该空节点删除便可,这时树的高度减小一层。
2-3
树的实现将会放在B树
章节,咱们将会实现其二叉树形式的左倾红黑树结构。
左倾红黑树能够由2-3
树的二叉树形式来实现。
其定义为:
因为红连接都在左边,因此这种红黑树又称左倾红黑树。左倾红黑树与2-3
树一一对应,只要将左连接画平,如图:
首先,咱们要定义树的结构LLRBTree
,以及表示左倾红黑树的节点LLRBTNode
:
// 定义颜色 const ( RED = true BLACK = false ) // 左倾红黑树 type LLRBTree struct { Root *LLRBTNode // 树根节点 } // 左倾红黑树节点 type LLRBTNode struct { Value int64 // 值 Times int64 // 值出现的次数 Left *LLRBTNode // 左子树 Right *LLRBTNode // 右子树 Color bool // 父亲指向该节点的连接颜色 } // 新建一棵空树 func NewLLRBTree() *LLRBTree { return &LLRBTree{} } // 节点的颜色 func IsRed(node *LLRBTNode) bool { if node == nil { return false } return node.Color == RED }
在节点LLRBTNode
中,咱们存储的元素字段为Value
,因为可能有重复的元素插入,因此多了一个Times
字段,表示该元素出现几回。
固然,红黑树中的红黑颜色使用Color
定义,表示父亲指向该节点的连接颜色。为了方便,咱们还构造了一个辅助函数IsRed()
。
在元素添加和实现的过程当中,须要作调整操做,有两种旋转操做,对某节点的右连接进行左旋转,或者左连接进行右旋转。
如图是对节点h
的右连接进行左旋转:
代码实现以下:
// 左旋转 func RotateLeft(h *LLRBTNode) *LLRBTNode { if h == nil { return nil } // 看图理解 x := h.Right h.Right = x.Left x.Left = h x.Color = h.Color h.Color = RED return x }
如图是对节点h
的左连接进行右旋转:
代码实现以下:
// 右旋转 func RotateRight(h *LLRBTNode) *LLRBTNode { if h == nil { return nil } // 看图理解 x := h.Left h.Left = x.Right x.Right = h x.Color = h.Color h.Color = RED return x }
因为左倾红黑树不容许一个节点有两个红连接,因此须要作颜色转换,如图:
代码以下:
// 颜色转换 func ColorChange(h *LLRBTNode) { if h == nil { return } h.Color = !h.Color h.Left.Color = !h.Left.Color h.Right.Color = !h.Right.Color }
旋转和颜色转换做为局部调整,并不影响全局。
每次添加元素节点时,都将该节点Color
字段,也就是父亲指向它的连接设置为RED
红色。
接着判断其父亲是否有两个红连接(如连续的两个左红连接或者左右红色连接),或者有右红色连接,进行颜色变换或旋转操做。
主要有如下这几种状况。
插入元素到2节点,直接让节点变为3节点,不过当右插入时须要左旋使得红色连接在左边,如图:
插入元素到3节点,须要作旋转和颜色转换操做,如图:
也就是说,在一个已是红色左连接的节点,插入一个新节点的状态变化以下:
根据上述的演示图以及旋转,颜色转换等操做,添加元素的代码为:
// 左倾红黑树添加元素 func (tree *LLRBTree) Add(value int64) { // 跟节点开始添加元素,由于可能调整,因此须要将返回的节点赋值回根节点 tree.Root = tree.Root.Add(value) // 根节点的连接永远都是黑色的 tree.Root.Color = BLACK } // 往节点添加元素 func (node *LLRBTNode) Add(value int64) *LLRBTNode { // 插入的节点为空,将其连接颜色设置为红色,并返回 if node == nil { return &LLRBTNode{ Value: value, Color: RED, } } // 插入的元素重复 if value == node.Value { node.Times = node.Times + 1 } else if value > node.Value { // 插入的元素比节点值大,往右子树插入 node.Right = node.Right.Add(value) } else { // 插入的元素比节点值小,往左子树插入 node.Left = node.Left.Add(value) } // 辅助变量 nowNode := node // 右连接为红色,那么进行左旋,确保树是左倾的 // 这里作完操做后就能够结束了,由于插入操做,新插入的右红连接左旋后,nowNode节点不会出现连续两个红左连接,由于它只有一个左红连接 if IsRed(nowNode.Right) && !IsRed(nowNode.Left) { nowNode = RotateLeft(nowNode) } else { // 连续两个左连接为红色,那么进行右旋 if IsRed(nowNode.Left) && IsRed(nowNode.Left.Left) { nowNode = RotateRight(nowNode) } // 旋转后,可能左右连接都为红色,须要变色 if IsRed(nowNode.Left) && IsRed(nowNode.Right) { ColorChange(nowNode) } } return nowNode }
可参考论文:Left-leaning Red-Black Trees。
左倾红黑树的最坏树高度为2log(n)
,其中n
为树的节点数量。为何呢,咱们先把左倾红黑树看成2-3
树,也就是说最坏状况下沿着2-3
树左边的节点都是3节点,其余节点都是2节点,这时树高近似log(n)
,再从2-3
树转成左倾红黑树,当3节点不画平时,能够知道树高变成原来2-3
树树高的两倍。虽然如此,构造一棵最坏的左倾红黑树很难。
AVL
树的最坏树高度为1.44log(n)
。因为左倾红黑树是近似平衡的二叉树,没有AVL
树的严格平衡,树的高度会更高一点,所以查找操做效率比AVL
树低,但时间复杂度只在于常数项的差异,去掉常数项,时间复杂度仍然是log(n)
。
咱们的代码实现中,左倾红黑树的插入,须要逐层判断是否须要旋转和变色,复杂度为log(n)
,当旋转变色后致使上层存在连续的红左连接或者红色左右连接,那么须要继续旋转和变色,可能有屡次这种调整操做,如图在箭头处添加新节点,出现了右红连接,要一直向上变色到根节点(实际上穿投到根节点的状况极少发生):
咱们能够优化代码,使得在某一层旋转变色后,若是其父层没有连续的左红连接或者不须要变色,那么能够直接退出,不须要逐层判断是否须要旋转和变色。
对于AVL
树来讲,插入最多旋转两次,但其须要逐层更新树高度,复杂度也是为log(n)
。
按照插入效率来讲,不少教程都说左倾红黑树会比AVL
树好一点,由于其不要求严格的平衡,会插入得更快点,但根据咱们实际上的递归代码,二者都须要逐层向上判断是否须要调整,只不过AVL
树多了更新树高度的操做,此操做影响了一点点效率,但我以为两种树的插入效率都差很少。
在此,咱们再也不纠结两种平衡树哪一种更好,由于代码实现中,两种平衡树都须要自底向上的递归操做,效率差异不大。。
删除操做就复杂得多了。对照一下2-3
树。
在这里,为了使得删除叶子节点时能够直接删除,叶子节点必须变为红节点。(在2-3
树中,也就是2节点要变成3节点,咱们知道要不和父亲合并再递归向上,要不向兄弟借值而后从新分布)
咱们创造两种操做,若是删除的节点在左子树中,可能须要进行红色左移,若是删除的节点在右子树中,可能须要进行红色右移。
咱们介绍红色左移的步骤:
要在树h
的的左子树中删除元素,这时树h
根节点是红节点,其儿子b,d
节点都为黑色节点,且两个黑色节点都是2节点,都没有左红孩子,那么直接对h
树根节点变色便可(至关于2-3
树:把父亲的一个值拉下来合并),如图:
若是存在右儿子d
是3节点,有左红孩子e
,那么须要先对h
树根节点变色后,对右儿子d
右旋,再对h
树根节点左旋,最后再一次对h
树根节点变色(至关于2-3
树:向3节点兄弟借值,而后从新分布),如图:
红色左移能够总结为下图(被删除的节点在左子树,且进入的树根h必定为红节点):
代码以下:
// 红色左移 // 节点 h 是红节点,其左儿子和左儿子的左儿子都为黑节点,左移后使得其左儿子或左儿子的左儿子有一个是红色节点 func MoveRedLeft(h *LLRBTNode) *LLRBTNode { // 应该确保 isRed(h) && !isRed(h.left) && !isRed(h.left.left) ColorChange(h) // 右儿子有左红连接 if IsRed(h.Right.Left) { // 对右儿子右旋 h.Right = RotateRight(h.Right) // 再左旋 h = RotateLeft(h) ColorChange(h) } return h }
为何要红色左移,是要保证调整后,子树根节点h
的左儿子或左儿子的左儿子有一个是红色节点,这样从h
的左子树递归删除元素才能够继续下去。
红色右移的步骤相似,如图(被删除的节点在右子树,且进入的树根h必定为红节点):
代码以下:
// 红色右移 // 节点 h 是红节点,其右儿子和右儿子的左儿子都为黑节点,右移后使得其右儿子或右儿子的右儿子有一个是红色节点 func MoveRedRight(h *LLRBTNode) *LLRBTNode { // 应该确保 isRed(h) && !isRed(h.right) && !isRed(h.right.left); ColorChange(h) // 左儿子有左红连接 if IsRed(h.Left.Left) { // 右旋 h = RotateRight(h) // 变色 ColorChange(h) } return h }
为何要红色右移,一样是为了保证树根节点h
的右儿子或右儿子的右儿子有一个是红色节点,往右子树递归删除元素能够继续下去。
介绍完两种操做后,咱们要明确一下究竟是如何删除元素的。
咱们知道2-3
树的删除是从叶子节点开始,自底向上的向兄弟节点借值,或和父亲合并,而后一直递归到根节点。左倾红黑树参考了这种作法,但更巧妙,左倾红黑树要保证一路上每次递归进入删除操做的子树树根必定是一个3节点,因此须要适当的红色左移或右移(相似于2-3
树借值和合并),这样一直递归到叶子节点,叶子节点也会是一个3节点,而后就能够直接删除叶子节点,最后再自底向上的恢复左倾红黑树的特征。
下面是左倾红黑树从树h
删除元素的示例图,往树h
左子树和右子树删除元素分别有四种状况,后两种状况须要使用到红色左移或右移,状态演变以后,树h
才能够从左或右子树进入下一次递归:
能够对照着大图,继续阅读下面的左倾红黑树删除元素代码:
// 左倾红黑树删除元素 func (tree *LLRBTree) Delete(value int64) { // 当找不到值时直接返回 if tree.Find(value) == nil { return } if !IsRed(tree.Root.Left) && !IsRed(tree.Root.Right) { // 左右子树都是黑节点,那么先将根节点变为红节点,方便后面的红色左移或右移 tree.Root.Color = RED } tree.Root = tree.Root.Delete(value) // 最后,若是根节点非空,永远都要为黑节点,赋值黑色 if tree.Root != nil { tree.Root.Color = BLACK } }
首先tree.Find(value)
找到能够删除的值时才能进行删除。
当根节点的左右子树都为黑节点时,那么先将根节点变为红节点,方便后面的红色左移或右移。
删除完节点:tree.Root = tree.Root.Delete(value)
后,须要将根节点染回黑色,由于左倾红黑树的特征之一是根节点永远都是黑色。
核心的从子树中删除元素代码以下:
// 对该节点所在的子树删除元素 func (node *LLRBTNode) Delete(value int64) *LLRBTNode { // 辅助变量 nowNode := node // 删除的元素比子树根节点小,须要从左子树删除 if value < nowNode.Value { // 由于从左子树删除,因此要判断是否须要红色左移 if !IsRed(nowNode.Left) && !IsRed(nowNode.Left.Left) { // 左儿子和左儿子的左儿子都不是红色节点,那么无法递归下去,先红色左移 nowNode = MoveRedLeft(nowNode) } // 如今能够从左子树中删除了 nowNode.Left = nowNode.Left.Delete(value) } else { // 删除的元素等于或大于树根节点 // 左节点为红色,那么须要右旋,方便后面能够红色右移 if IsRed(nowNode.Left) { nowNode = RotateRight(nowNode) } // 值相等,且没有右孩子节点,那么该节点必定是要被删除的叶子节点,直接删除 // 为何呢,反证,它没有右儿子,但有左儿子,由于左倾红黑树的特征,那么左儿子必定是红色,可是前面的语句已经把红色左儿子右旋到右边,不该该出现右儿子为空。 if value == nowNode.Value && nowNode.Right == nil { return nil } // 由于从右子树删除,因此要判断是否须要红色右移 if !IsRed(nowNode.Right) && !IsRed(nowNode.Right.Left) { // 右儿子和右儿子的左儿子都不是红色节点,那么无法递归下去,先红色右移 nowNode = MoveRedRight(nowNode) } // 删除的节点找到了,它是中间节点,须要用最小后驱节点来替换它,而后删除最小后驱节点 if value == nowNode.Value { minNode := nowNode.Right.FindMinValue() nowNode.Value = minNode.Value nowNode.Times = minNode.Times // 删除其最小后驱节点 nowNode.Right = nowNode.Right.DeleteMin() } else { // 删除的元素比子树根节点大,须要从右子树删除 nowNode.Right = nowNode.Right.Delete(value) } } // 最后,删除叶子节点后,须要恢复左倾红黑树特征 return nowNode.FixUp() }
这段核心代码十分复杂,会用到红色左移和右移,当删除的元素小于根节点时,咱们明白要在左子树中删除,如:
// 删除的元素比子树根节点小,须要从左子树删除 if value < nowNode.Value { // 由于从左子树删除,因此要判断是否须要红色左移 if !IsRed(nowNode.Left) && !IsRed(nowNode.Left.Left) { // 左儿子和左儿子的左儿子都不是红色节点,那么无法递归下去,先红色左移 nowNode = MoveRedLeft(nowNode) } // 如今能够从左子树中删除了 nowNode.Left = nowNode.Left.Delete(value) }
递归删除左子树前:nowNode.Left = nowNode.Left.Delete(value)
,要确保删除的左子树根节点是红色节点,或左子树根节点的左儿子是红色节点,才可以继续递归下去,因此使用了!IsRed(nowNode.Left) && !IsRed(nowNode.Left.Left)
来判断是否须要红色左移。
若是删除的值不小于根节点,那么进入如下逻辑(可仔细阅读注释):
// 删除的元素等于或大于树根节点 // 左节点为红色,那么须要右旋,方便后面能够红色右移 if IsRed(nowNode.Left) { nowNode = RotateRight(nowNode) } // 值相等,且没有右孩子节点,那么该节点必定是要被删除的叶子节点,直接删除 // 为何呢,反证,它没有右儿子,但有左儿子,由于左倾红黑树的特征,那么左儿子必定是红色,可是前面的语句已经把红色左儿子右旋到右边,不该该出现右儿子为空。 if value == nowNode.Value && nowNode.Right == nil { return nil } // 由于从右子树删除,因此要判断是否须要红色右移 if !IsRed(nowNode.Right) && !IsRed(nowNode.Right.Left) { // 右儿子和右儿子的左儿子都不是红色节点,那么无法递归下去,先红色右移 nowNode = MoveRedRight(nowNode) } // 删除的节点找到了,它是中间节点,须要用最小后驱节点来替换它,而后删除最小后驱节点 if value == nowNode.Value { minNode := nowNode.Right.FindMinValue() nowNode.Value = minNode.Value nowNode.Times = minNode.Times // 删除其最小后驱节点 nowNode.Right = nowNode.Right.DeleteMin() } else { // 删除的元素比子树根节点大,须要从右子树删除 nowNode.Right = nowNode.Right.Delete(value) }
首先,须要先判断该节点的左子树根节点是否为红色节点IsRed(nowNode.Left)
,若是是的话须要右旋:nowNode = RotateRight(nowNode)
,将红节点右旋是为了后面能够递归进入右子树。
而后,判断删除的值是否等于当前根节点的值,且其没有右节点:value == nowNode.Value && nowNode.Right == nil
,若是是,那么该节点就是要被删除的叶子节点,直接删除便可。
接着,判断是否须要红色右移:!IsRed(nowNode.Right) && !IsRed(nowNode.Right.Left)
,若是该节点右儿子和右儿子的左儿子都不是红色节点,那么无法递归进入右子树,须要红色右移,必须确保其右子树或右子树的左儿子有一个是红色节点。
再接着,须要判断是否找到了要删除的节点:value == nowNode.Value
,找到时表示要删除的节点处于内部节点,须要用最小后驱节点来替换它,而后删除最小后驱节点。
找到最小后驱节点:minNode := nowNode.Right.FindMinValue()
后,将最小后驱节点与要删除的内部节点替换,而后删除最小后驱节点:nowNode.Right = nowNode.Right.DeleteMin()
,删除最小节点代码以下:
// 对该节点所在的子树删除最小元素 func (node *LLRBTNode) DeleteMin() *LLRBTNode { // 辅助变量 nowNode := node // 没有左子树,那么删除它本身 if nowNode.Left == nil { return nil } // 判断是否须要红色左移,由于最小元素在左子树中 if !IsRed(nowNode.Left) && !IsRed(nowNode.Left.Left) { nowNode = MoveRedLeft(nowNode) } // 递归从左子树删除 nowNode.Left = nowNode.Left.DeleteMin() // 修复左倾红黑树特征 return nowNode.FixUp() }
由于最小节点在最左的叶子节点,因此只须要适当的红色左移,而后一直左子树递归便可。递归完后须要修复左倾红黑树特征nowNode.FixUp()
,代码以下:
// 修复左倾红黑树特征 func (node *LLRBTNode) FixUp() *LLRBTNode { // 辅助变量 nowNode := node // 红连接在右边,左旋恢复,让红连接只出如今左边 if IsRed(nowNode.Right) { nowNode = RotateLeft(nowNode) } // 连续两个左连接为红色,那么进行右旋 if IsRed(nowNode.Left) && IsRed(nowNode.Left.Left) { nowNode = RotateRight(nowNode) } // 旋转后,可能左右连接都为红色,须要变色 if IsRed(nowNode.Left) && IsRed(nowNode.Right) { ColorChange(nowNode) } return nowNode }
若是不是删除内部节点,依然是从右子树继续递归:
// 删除的元素比子树根节点大,须要从右子树删除 nowNode.Right = nowNode.Right.Delete(value)
固然,递归完成后还要进行一次FixUp()
,恢复左倾红黑树的特征。
删除操做很难理解,能够多多思考,红色左移和右移不断地递归都是为了确保删除叶子节点时,其是一个3节点。
PS:若是不理解自顶向下的红色左移和右移递归思路,能够更换另一种方法,使用原先2-3树
删除元素操做步骤来实现,一开始从叶子节点删除,而后自底向上的向兄弟借值或与父亲合并,这是更容易理解的,咱们不在这里进行展现了,能够借鉴普通红黑树章节的删除实现(它使用了自底向上的调整)。
完整代码见最下面。
左倾红黑树删除元素须要自顶向下的递归,可能不断地红色左移和右移,也就是有不少的旋转,当删除叶子节点后,还须要逐层恢复左倾红黑树的特征。时间复杂度仍然是和树高有关:log(n)
。
查找最小值,最大值,或者某个值,代码以下:
// 找出最小值的节点 func (tree *LLRBTree) FindMinValue() *LLRBTNode { if tree.Root == nil { // 若是是空树,返回空 return nil } return tree.Root.FindMinValue() } func (node *LLRBTNode) FindMinValue() *LLRBTNode { // 左子树为空,表面已是最左的节点了,该值就是最小值 if node.Left == nil { return node } // 一直左子树递归 return node.Left.FindMinValue() } // 找出最大值的节点 func (tree *LLRBTree) FindMaxValue() *LLRBTNode { if tree.Root == nil { // 若是是空树,返回空 return nil } return tree.Root.FindMaxValue() } func (node *LLRBTNode) FindMaxValue() *LLRBTNode { // 右子树为空,表面已是最右的节点了,该值就是最大值 if node.Right == nil { return node } // 一直右子树递归 return node.Right.FindMaxValue() } // 查找指定节点 func (tree *LLRBTree) Find(value int64) *LLRBTNode { if tree.Root == nil { // 若是是空树,返回空 return nil } return tree.Root.Find(value) } func (node *LLRBTNode) Find(value int64) *LLRBTNode { if value == node.Value { // 若是该节点刚刚等于该值,那么返回该节点 return node } else if value < node.Value { // 若是查找的值小于节点值,从节点的左子树开始找 if node.Left == nil { // 左子树为空,表示找不到该值了,返回nil return nil } return node.Left.Find(value) } else { // 若是查找的值大于节点值,从节点的右子树开始找 if node.Right == nil { // 右子树为空,表示找不到该值了,返回nil return nil } return node.Right.Find(value) } } // 中序遍历 func (tree *LLRBTree) MidOrder() { tree.Root.MidOrder() } func (node *LLRBTNode) MidOrder() { if node == nil { return } // 先打印左子树 node.Left.MidOrder() // 按照次数打印根节点 for i := 0; i <= int(node.Times); i++ { fmt.Println(node.Value) } // 打印右子树 node.Right.MidOrder() }
查找操做逻辑与通用的二叉查找树同样,并没有区别。
如何确保咱们的代码实现的就是一棵左倾红黑树呢,能够进行验证:
// 验证是否是棵左倾红黑树 func (tree *LLRBTree) IsLLRBTree() bool { if tree == nil || tree.Root == nil { return true } // 判断树是不是一棵二分查找树 if !tree.Root.IsBST() { return false } // 判断树是否遵循2-3树,也就是红连接只能在左边,不能连续有两个红连接 if !tree.Root.Is23() { return false } // 判断树是否平衡,也就是任意一个节点到叶子节点,通过的黑色连接数量相同 // 先计算根节点到最左边叶子节点的黑连接数量 blackNum := 0 x := tree.Root for x != nil { if !IsRed(x) { // 是黑色连接 blackNum = blackNum + 1 } x = x.Left } if !tree.Root.IsBalanced(blackNum) { return false } return true } // 节点所在的子树是不是一棵二分查找树 func (node *LLRBTNode) IsBST() bool { if node == nil { return true } // 左子树非空,那么根节点必须大于左儿子节点 if node.Left != nil { if node.Value > node.Left.Value { } else { fmt.Printf("father:%#v,lchild:%#v,rchild:%#v\n", node, node.Left, node.Right) return false } } // 右子树非空,那么根节点必须小于右儿子节点 if node.Right != nil { if node.Value < node.Right.Value { } else { fmt.Printf("father:%#v,lchild:%#v,rchild:%#v\n", node, node.Left, node.Right) return false } } // 左子树也要判断是不是平衡查找树 if !node.Left.IsBST() { return false } // 右子树也要判断是不是平衡查找树 if !node.Right.IsBST() { return false } return true } // 节点所在的子树是否遵循2-3树 func (node *LLRBTNode) Is23() bool { if node == nil { return true } // 不容许右倾红连接 if IsRed(node.Right) { fmt.Printf("father:%#v,rchild:%#v\n", node, node.Right) return false } // 不容许连续两个左红连接 if IsRed(node) && IsRed(node.Left) { fmt.Printf("father:%#v,lchild:%#v\n", node, node.Left) return false } // 左子树也要判断是否遵循2-3树 if !node.Left.Is23() { return false } // 右子树也要判断是不是遵循2-3树 if !node.Right.Is23() { return false } return true } // 节点所在的子树是否平衡,是否有 blackNum 个黑连接 func (node *LLRBTNode) IsBalanced(blackNum int) bool { if node == nil { return blackNum == 0 } if !IsRed(node) { blackNum = blackNum - 1 } if !node.Left.IsBalanced(blackNum) { fmt.Println("node.Left to leaf black link is not ", blackNum) return false } if !node.Right.IsBalanced(blackNum) { fmt.Println("node.Right to leaf black link is not ", blackNum) return false } return true }
运行请看完整代码。
package main import "fmt" // 左倾红黑树实现 // Left-leaning red-black tree // 定义颜色 const ( RED = true BLACK = false ) // 左倾红黑树 type LLRBTree struct { Root *LLRBTNode // 树根节点 } // 新建一棵空树 func NewLLRBTree() *LLRBTree { return &LLRBTree{} } // 左倾红黑树节点 type LLRBTNode struct { Value int64 // 值 Times int64 // 值出现的次数 Left *LLRBTNode // 左子树 Right *LLRBTNode // 右子树 Color bool // 父亲指向该节点的连接颜色 } // 节点的颜色 func IsRed(node *LLRBTNode) bool { if node == nil { return false } return node.Color == RED } // 左旋转 func RotateLeft(h *LLRBTNode) *LLRBTNode { if h == nil { return nil } // 看图理解 x := h.Right h.Right = x.Left x.Left = h x.Color = h.Color h.Color = RED return x } // 右旋转 func RotateRight(h *LLRBTNode) *LLRBTNode { if h == nil { return nil } // 看图理解 x := h.Left h.Left = x.Right x.Right = h x.Color = h.Color h.Color = RED return x } // 红色左移 // 节点 h 是红节点,其左儿子和左儿子的左儿子都为黑节点,左移后使得其左儿子或左儿子的左儿子有一个是红色节点 func MoveRedLeft(h *LLRBTNode) *LLRBTNode { // 应该确保 isRed(h) && !isRed(h.left) && !isRed(h.left.left) ColorChange(h) // 右儿子有左红连接 if IsRed(h.Right.Left) { // 对右儿子右旋 h.Right = RotateRight(h.Right) // 再左旋 h = RotateLeft(h) ColorChange(h) } return h } // 红色右移 // 节点 h 是红节点,其右儿子和右儿子的左儿子都为黑节点,右移后使得其右儿子或右儿子的右儿子有一个是红色节点 func MoveRedRight(h *LLRBTNode) *LLRBTNode { // 应该确保 isRed(h) && !isRed(h.right) && !isRed(h.right.left); ColorChange(h) // 左儿子有左红连接 if IsRed(h.Left.Left) { // 右旋 h = RotateRight(h) // 变色 ColorChange(h) } return h } // 颜色变换 func ColorChange(h *LLRBTNode) { if h == nil { return } h.Color = !h.Color h.Left.Color = !h.Left.Color h.Right.Color = !h.Right.Color } // 左倾红黑树添加元素 func (tree *LLRBTree) Add(value int64) { // 跟节点开始添加元素,由于可能调整,因此须要将返回的节点赋值回根节点 tree.Root = tree.Root.Add(value) // 根节点的连接永远都是黑色的 tree.Root.Color = BLACK } // 往节点添加元素 func (node *LLRBTNode) Add(value int64) *LLRBTNode { // 插入的节点为空,将其连接颜色设置为红色,并返回 if node == nil { return &LLRBTNode{ Value: value, Color: RED, } } // 插入的元素重复 if value == node.Value { node.Times = node.Times + 1 } else if value > node.Value { // 插入的元素比节点值大,往右子树插入 node.Right = node.Right.Add(value) } else { // 插入的元素比节点值小,往左子树插入 node.Left = node.Left.Add(value) } // 辅助变量 nowNode := node // 右连接为红色,那么进行左旋,确保树是左倾的 // 这里作完操做后就能够结束了,由于插入操做,新插入的右红连接左旋后,nowNode节点不会出现连续两个红左连接,由于它只有一个左红连接 if IsRed(nowNode.Right) && !IsRed(nowNode.Left) { nowNode = RotateLeft(nowNode) } else { // 连续两个左连接为红色,那么进行右旋 if IsRed(nowNode.Left) && IsRed(nowNode.Left.Left) { nowNode = RotateRight(nowNode) } // 旋转后,可能左右连接都为红色,须要变色 if IsRed(nowNode.Left) && IsRed(nowNode.Right) { ColorChange(nowNode) } } return nowNode } // 找出最小值的节点 func (tree *LLRBTree) FindMinValue() *LLRBTNode { if tree.Root == nil { // 若是是空树,返回空 return nil } return tree.Root.FindMinValue() } func (node *LLRBTNode) FindMinValue() *LLRBTNode { // 左子树为空,表面已是最左的节点了,该值就是最小值 if node.Left == nil { return node } // 一直左子树递归 return node.Left.FindMinValue() } // 找出最大值的节点 func (tree *LLRBTree) FindMaxValue() *LLRBTNode { if tree.Root == nil { // 若是是空树,返回空 return nil } return tree.Root.FindMaxValue() } func (node *LLRBTNode) FindMaxValue() *LLRBTNode { // 右子树为空,表面已是最右的节点了,该值就是最大值 if node.Right == nil { return node } // 一直右子树递归 return node.Right.FindMaxValue() } // 查找指定节点 func (tree *LLRBTree) Find(value int64) *LLRBTNode { if tree.Root == nil { // 若是是空树,返回空 return nil } return tree.Root.Find(value) } func (node *LLRBTNode) Find(value int64) *LLRBTNode { if value == node.Value { // 若是该节点刚刚等于该值,那么返回该节点 return node } else if value < node.Value { // 若是查找的值小于节点值,从节点的左子树开始找 if node.Left == nil { // 左子树为空,表示找不到该值了,返回nil return nil } return node.Left.Find(value) } else { // 若是查找的值大于节点值,从节点的右子树开始找 if node.Right == nil { // 右子树为空,表示找不到该值了,返回nil return nil } return node.Right.Find(value) } } // 中序遍历 func (tree *LLRBTree) MidOrder() { tree.Root.MidOrder() } func (node *LLRBTNode) MidOrder() { if node == nil { return } // 先打印左子树 node.Left.MidOrder() // 按照次数打印根节点 for i := 0; i <= int(node.Times); i++ { fmt.Println(node.Value) } // 打印右子树 node.Right.MidOrder() } // 修复左倾红黑树特征 func (node *LLRBTNode) FixUp() *LLRBTNode { // 辅助变量 nowNode := node // 红连接在右边,左旋恢复,让红连接只出如今左边 if IsRed(nowNode.Right) { nowNode = RotateLeft(nowNode) } // 连续两个左连接为红色,那么进行右旋 if IsRed(nowNode.Left) && IsRed(nowNode.Left.Left) { nowNode = RotateRight(nowNode) } // 旋转后,可能左右连接都为红色,须要变色 if IsRed(nowNode.Left) && IsRed(nowNode.Right) { ColorChange(nowNode) } return nowNode } // 对该节点所在的子树删除最小元素 func (node *LLRBTNode) DeleteMin() *LLRBTNode { // 辅助变量 nowNode := node // 没有左子树,那么删除它本身 if nowNode.Left == nil { return nil } // 判断是否须要红色左移,由于最小元素在左子树中 if !IsRed(nowNode.Left) && !IsRed(nowNode.Left.Left) { nowNode = MoveRedLeft(nowNode) } // 递归从左子树删除 nowNode.Left = nowNode.Left.DeleteMin() // 修复左倾红黑树特征 return nowNode.FixUp() } // 左倾红黑树删除元素 func (tree *LLRBTree) Delete(value int64) { // 当找不到值时直接返回 if tree.Find(value) == nil { return } if !IsRed(tree.Root.Left) && !IsRed(tree.Root.Right) { // 左右子树都是黑节点,那么先将根节点变为红节点,方便后面的红色左移或右移 tree.Root.Color = RED } tree.Root = tree.Root.Delete(value) // 最后,若是根节点非空,永远都要为黑节点,赋值黑色 if tree.Root != nil { tree.Root.Color = BLACK } } // 对该节点所在的子树删除元素 func (node *LLRBTNode) Delete(value int64) *LLRBTNode { // 辅助变量 nowNode := node // 删除的元素比子树根节点小,须要从左子树删除 if value < nowNode.Value { // 由于从左子树删除,因此要判断是否须要红色左移 if !IsRed(nowNode.Left) && !IsRed(nowNode.Left.Left) { // 左儿子和左儿子的左儿子都不是红色节点,那么无法递归下去,先红色左移 nowNode = MoveRedLeft(nowNode) } // 如今能够从左子树中删除了 nowNode.Left = nowNode.Left.Delete(value) } else { // 删除的元素等于或大于树根节点 // 左节点为红色,那么须要右旋,方便后面能够红色右移 if IsRed(nowNode.Left) { nowNode = RotateRight(nowNode) } // 值相等,且没有右孩子节点,那么该节点必定是要被删除的叶子节点,直接删除 // 为何呢,反证,它没有右儿子,但有左儿子,由于左倾红黑树的特征,那么左儿子必定是红色,可是前面的语句已经把红色左儿子右旋到右边,不该该出现右儿子为空。 if value == nowNode.Value && nowNode.Right == nil { return nil } // 由于从右子树删除,因此要判断是否须要红色右移 if !IsRed(nowNode.Right) && !IsRed(nowNode.Right.Left) { // 右儿子和右儿子的左儿子都不是红色节点,那么无法递归下去,先红色右移 nowNode = MoveRedRight(nowNode) } // 删除的节点找到了,它是中间节点,须要用最小后驱节点来替换它,而后删除最小后驱节点 if value == nowNode.Value { minNode := nowNode.Right.FindMinValue() nowNode.Value = minNode.Value nowNode.Times = minNode.Times // 删除其最小后驱节点 nowNode.Right = nowNode.Right.DeleteMin() } else { // 删除的元素比子树根节点大,须要从右子树删除 nowNode.Right = nowNode.Right.Delete(value) } } // 最后,删除叶子节点后,须要恢复左倾红黑树特征 return nowNode.FixUp() } // 验证是否是棵左倾红黑树 func (tree *LLRBTree) IsLLRBTree() bool { if tree == nil || tree.Root == nil { return true } // 判断树是不是一棵二分查找树 if !tree.Root.IsBST() { return false } // 判断树是否遵循2-3树,也就是红连接只能在左边,不能连续有两个红连接 if !tree.Root.Is23() { return false } // 判断树是否平衡,也就是任意一个节点到叶子节点,通过的黑色连接数量相同 // 先计算根节点到最左边叶子节点的黑连接数量 blackNum := 0 x := tree.Root for x != nil { if !IsRed(x) { // 是黑色连接 blackNum = blackNum + 1 } x = x.Left } if !tree.Root.IsBalanced(blackNum) { return false } return true } // 节点所在的子树是不是一棵二分查找树 func (node *LLRBTNode) IsBST() bool { if node == nil { return true } // 左子树非空,那么根节点必须大于左儿子节点 if node.Left != nil { if node.Value > node.Left.Value { } else { fmt.Printf("father:%#v,lchild:%#v,rchild:%#v\n", node, node.Left, node.Right) return false } } // 右子树非空,那么根节点必须小于右儿子节点 if node.Right != nil { if node.Value < node.Right.Value { } else { fmt.Printf("father:%#v,lchild:%#v,rchild:%#v\n", node, node.Left, node.Right) return false } } // 左子树也要判断是不是平衡查找树 if !node.Left.IsBST() { return false } // 右子树也要判断是不是平衡查找树 if !node.Right.IsBST() { return false } return true } // 节点所在的子树是否遵循2-3树 func (node *LLRBTNode) Is23() bool { if node == nil { return true } // 不容许右倾红连接 if IsRed(node.Right) { fmt.Printf("father:%#v,rchild:%#v\n", node, node.Right) return false } // 不容许连续两个左红连接 if IsRed(node) && IsRed(node.Left) { fmt.Printf("father:%#v,lchild:%#v\n", node, node.Left) return false } // 左子树也要判断是否遵循2-3树 if !node.Left.Is23() { return false } // 右子树也要判断是不是遵循2-3树 if !node.Right.Is23() { return false } return true } // 节点所在的子树是否平衡,是否有 blackNum 个黑连接 func (node *LLRBTNode) IsBalanced(blackNum int) bool { if node == nil { return blackNum == 0 } if !IsRed(node) { blackNum = blackNum - 1 } if !node.Left.IsBalanced(blackNum) { fmt.Println("node.Left to leaf black link is not ", blackNum) return false } if !node.Right.IsBalanced(blackNum) { fmt.Println("node.Right to leaf black link is not ", blackNum) return false } return true } func main() { tree := NewLLRBTree() values := []int64{2, 3, 7, 10, 10, 10, 10, 23, 9, 102, 109, 111, 112, 113} for _, v := range values { tree.Add(v) } // 找到最大值或最小值的节点 fmt.Println("find min value:", tree.FindMinValue()) fmt.Println("find max value:", tree.FindMaxValue()) // 查找不存在的99 node := tree.Find(99) if node != nil { fmt.Println("find it 99!") } else { fmt.Println("not find it 99!") } // 查找存在的9 node = tree.Find(9) if node != nil { fmt.Println("find it 9!") } else { fmt.Println("not find it 9!") } tree.MidOrder() // 删除存在的9后,再查找9 tree.Delete(9) tree.Delete(10) tree.Delete(2) tree.Delete(3) tree.Add(4) tree.Add(3) tree.Add(10) tree.Delete(111) node = tree.Find(9) if node != nil { fmt.Println("find it 9!") } else { fmt.Println("not find it 9!") } if tree.IsLLRBTree() { fmt.Println("is a llrb tree") } else { fmt.Println("is not llrb tree") } }
运行:
find min value: &{2 0 <nil> <nil> false} find max value: &{113 0 0xc0000941e0 <nil> false} not find it 99! find it 9! 2 3 7 9 10 10 10 10 23 102 109 111 112 113 not find it 9! is a llrb tree
PS:咱们的程序是递归程序,若是改写为非递归形式,效率和性能会更好,在此就不实现了,理解左倾红黑树添加和删除的整体思路便可。
红黑树能够用来做为字典 Map 的基础数据结构,能够存储键值对,而后经过一个键,能够快速找到键对应的值,相比哈希表查找,不须要占用额外的空间。咱们以上的代码实现只有value
,没有key:value
,能够简单改造实现字典。
Java 语言基础类库中的 HashMap,TreeSet,TreeMap 都有使用到,C++ 语言的 STL 标准模板库中,map 和 set 类也有使用到。不少中间件也有使用到,好比 Nginx,但 Golang 语言标准库并无它。
最后,上述应用场景使用的红黑树都是普通红黑树,并非本文所介绍的左倾红黑树。
左倾红黑树做为红黑树的一个变种,只是被设计为更容易理解而已,变种只能是变种,工程上使用得更多的仍是普通红黑树,因此咱们仍然须要学习普通的红黑树,请看下一章节。
我是陈星星,欢迎阅读我亲自写的 数据结构和算法(Golang实现),文章首发于 阅读更友好的GitBook。