从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树

参考:自平衡二叉查找树 ,红黑树, 算法:理解红黑树 (英文pdf:红黑树)

目录

  • 自平衡二叉树介绍
  • avl树
  • 2-3树
  • LLRBT(Left-leaning red-black tree左倾红黑树 (代码见git)
  • 2-3-4树和红黑树
  • avl和红黑树的比较

自平衡二叉查找树

 

诞生的目的:

它是为了解决二叉查找树的查找时间复杂度最差是O(n)的问题而发明的数据结构。

完全二叉树的公式: n = 2h - 1 

 

BST的查找运行时间和BST的高度有关。一个树的高度指的是从树的根开始所能到达的最长的路径长度。

如果按照从小到大的顺序输入一组key值,得到的将是一棵只有右子树的树。见下图。

如这个例子:

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第1张图片

有6个节点,它的时间复杂度是O(n)。无论是新增,变更还是查找,删除,都需要诸葛对比key值。

假如要查找节点200,那么节点会比较5次,相当于遍历所有节点了。

这太浪费时间了,因此降低树的高度,就可以减少时间复杂度。

我们知道二叉搜索树的搜索节点的最小时间复杂度是 O(log­2n)。因此找到一个高度和节点数量的最佳比例。让它的时间复杂度维持在O(log­2n)。

 

期望是:

如果树中节点的数量为 n,则一棵满足O(log2n) 渐进运行时间的 BST 树的高度应接近于比 log2n 小的最大整数。

 

但实际问题是:

如何保证 BST 的拓扑结构始终保持树高度与节点数量的最佳比例?

因为 BST 的拓扑结构与节点的插入顺序息息相关,一种方式是通过数据的乱序来保证。所以必须在插入节点前就得到数据。

但是如果无法掌控数据的来源,怎么做?一种方案是新的节点插入不会打乱BST树的平衡。这种始终维持树的平衡状态的数据结构称为:自平衡二叉查找树。self-balancing binary search tree.

 

一棵平衡树

指的是树能够保持其高度与广度能够保持预先定义的比例。有许多种不同的自平衡 BST 数据结构,例如 AVL 树、红黑树(Red-Black Tree)、2-3 树、2-3-4 树、伸展树(Splay Tree)、B 树等等。

 


 

AVL树 

1962年, 数学家发明的第一种自平衡二叉查找树,以其名字命名。它的平衡条件是,对每个节点n:

节点n的左子树的高度与右子树的高度差最多是1。即可以没有高度差,也可以高度差距1.

树的高度可递归性定义为:

  • 如果节点n没有子节点,则它的高度为 0;
  • 如果节点n只有一个子节点,则n的高度为该子节点的高度加 1;
  • 如果节点n有两个子节点,则n的高度为两个子节点中高度较高的加 1;
  • 如果节点没有子节点,无子节点侧的高度是-1。

 例子:

下面有4个BST树,节点中的数代表节点的值,左右两侧的数代表左右子树的高度。a, b是AVL树,c,d不是,因为c/d不满足AVL的平衡条件。

 从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第2张图片

 

 

 当创建一棵 AVL 树时,难点在于如何保证 AVL 的平衡性质要求,而不用关注对树的具体操作。也就是说,无论是向树添加节点还是删除节点,最重要的事情就是保持树的平衡。

AVL 树通过 "旋转操作(rotations)" 来保持树的平衡。旋转操作可以重塑树的拓扑结构来恢复树的平衡,更重要的是,重塑后的树依然符合二叉查找树的性质要求。

当向一棵 AVL 树中插入一个新的节点时,需要经过两阶段的过程。

  1. 插入新节点的操作将使用与向 BST 树中插入新节点时使用的相同的查找算法。新的节点将做为一个叶子节点被添加到树中合适的位置,以满足 BST 的性质要求。在添加完节点后,将导致树的结构可能已经违背 AVL 树的性质要求。
  2. 在第二个阶段中,将遍历访问路径,来检查每个节点左右子树高度。如果存在某节点的左右子树的高度差大于 1 时,则需要使用旋转操作来处理。

例子:

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第3张图片

 

 

 

有时除了像上图中描述的简单的旋转操作之外,可能还需要进行多次旋转操作。最重要的就是要意识到插入操作和删除操作都会破坏 AVL 树的平衡,而旋转操作就是解决这些问题的法宝。

通过确保所有节点的左右子树的差小于等于 1,AVL 树保证了插入、删除和查找操作将始终保持 O(log2n) 的渐进运行时间,而与插入或删除节点的顺序无关。

 


 

2-3树

参考:浅谈算法和数据结构: 八 平衡查找树之2-3树

 

2-3树是多叉树,它同样是一个平衡查找树。

二叉树中,每个节点最多只储存一个数据项的同时,最多也只有左右两条链接。而2-3树则不同:

定义

2-3树是一个多叉树。一个节点可以保存1个或2个数据项。可以有0-3个子节点。

  1. 有一个数据项的节点必须有2个子节点。
  2. 有二个数据项的节点必须有3个子节点。
  3. 每个节点的数据项按照,数据项的key,从左到右保持从小到大的顺序。
  4. 两个key之间的子树的key的值,大于父节点左key,小于父节点的右key.
  5. 作为平衡树,所有从leaf到root的path的高度相同,因此所有的叶子节点都是位于同一层。
  6. ⚠️2-3树的节点分裂是:自底向上的(不能预分裂),而且2-3树节点分裂必须用到新数据项。
  7. 由1,2可知,除了叶节点不允许出现空节点。

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第4张图片

 

 

时间复杂度: 

  • 在最坏的情况下,也就是所有的节点都是2-node节点,查找效率为lgN
  • 在最好的情况下,所有的节点都是3-node节点,查找效率为log3N约等于0.631lgN

原理:

2-3树在插入key值的过程中会不断构建并分解3-key节点来保持树的平衡,因此在2-3树就可以避免二叉树的不平衡导致的效率低下的问题。

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第5张图片

 

2-3树是自平衡的树,例如插入一组从小到大的key值的元素:

  1. 首先2-3树已经构建一个根节点
  2. 然后插入值为1的key。(⚠️2-3树允许一个节点储存两个key值)
  3. 插入2。成为了临时的3key节点,这是2-3树不允许的,需要分裂,并把1上传,形成二叉树结构。
  4. 插入3。⚠️这时的2-3树是左右平衡的。
  5. 再插入4。形成了3-key节点,需要分解它,并导致树的不平衡。(⚠️定义所有叶节点都在同一层)
  6. 重构,把节点3和节点1合并。成为一个2-3树。
  7. 再插入5。然后插入6,形成一个3key节点。分解它,上传5节点。根节点形成3-key节点,树虽然平衡,但不符合2-3树的定义:每个节点最多有2个数据key。
  8. 分解root节点:最后的树是左右平衡的。

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第6张图片 

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第7张图片

 

 

由此可知,2-3树可以避免二叉搜索树的不平衡导致的效率低下的问题。 

 

总结-插入方法:

  1. 如果2-3树已存在当前插入的key,则插入失败,正确的插入一定是在叶子节点内插入。
  2. 如果等待插入的节点内只有一个节点,则直接插入。
  3. 如果等待插入的节点内有2个节点,插入后,需要对节点分裂。形成一个二叉树结构,然后将父节点再向上传递。
  4. 重复2和3的步骤,直到满足2-3树的定义。

 

删除方法:

比较复杂,未细看。参考:https://blog.csdn.net/u012152619/article/details/84332165

 

首先找到所在要删除的关键字(假设是K)所在的节点。

如果这个节点不是叶节点,就要找到中序排列时K后面的关键字所在的节点,这个节点一定是叶节点(因为它在右子树中是最小的)。然后交换这两个节点,那么所删除的关键字最终还是在一个叶节点中。

如果这个节点是叶节点,分叶节点和非叶节点,再根据key的数量分为4种:

  • 1key的叶节点
  • 2key的叶节点
  • 1key的非叶节点
  • 2key的非叶节点

下面从简单到复杂情况分析:

1. 如果删除的节点是2key的叶节点,只需删除目标key即可。2key叶节点变为1key叶节点,仍是2-3树。

    10               10
  /     \      =>  /   \
 4,6     18       4,x  18

 

 

2. 如果删除的节点属于2key非叶节点,则中序遍历找到待删除节点的后继节点,然后将后继节点和待删除节点位置交换。 此时问题转化为删除节点为叶子节点了。这时有2种情况:

  • 待删除节点位于一个2-key叶节点内。删除方法见方法1。
       10, 20                             12,20
    /     |      \        =>           /     |     \       
 4     12, 15    25                   4    x ,15    25  
#直接删除10即可。

 

  • 待删除节点是一个独立的叶子节点。  删除方法见。
    10,20                     12,20
   /   |   \        =>       /    |    \      
 4,6  12   25             4, 6    X    25
#需要再平衡,删除方法见。

 

 

3. 如果删除节点是1key的非叶节点, 删除方法和2一样。

     10                  18
   /     \      =>    /    \
 4,6     18          4,6     x   
# 需要再平衡,删除方法见。

 

       10                    12
   /        \      =>    /      \
 4,6      12, 18        4,6     x,18
#直接删除10后,仍是2-3树。  

 

 

4.如果删除节点是1key叶节点,将它删除后,需再平衡。分几种情况:

看待删除节点的兄弟节点和父节点是,1key还算2key节点:

A: 当父节是1key,兄弟节点是两key节点。3个节点可以拆分成一个二叉树,满足2-3树结构。 

     10                  18                 6
   /     \      =>     /     \      =>   /     \
 4,6     18          4,6     x          4      18

 

B: 当父节是1-key,兄弟节点是1-key节点。合并这2个节点,并将合并后的节点看成当前节点x。然后重复4的判断,即x的兄弟节点和父节点的情况,然后做A,B,C之一的处理,直到满足2-3树结构。例子:

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第8张图片

 

C: 当父节点是2-key节点,兄弟有2个,兄弟有1-keyor2-key的可能,总共有4种组合,再加上删除节点的位置有左,中,右三种。穷举起来:总共3*4=12种。

虽然有12种,但处理方式只有2种:

  • 父节点下移和子节点合并
  • 父节点下移形成单独的子节点,然后2key的子节点上移一个key到父节点合并。

具体参考https://blog.csdn.net/u012152619/article/details/84332165

 

 


 

 

从2-3树到左倾红黑树

先放上定义:

1. No node has two red links connected to it。没有任何一个节点同时和两条红链接相连(类似传统红黑树的定义4:红性质)

2. Every path from root to nil(null) link has the same number of black links。(相当于传统红黑树的定义5黑性质)

3. Red links lean left。红链接为左链接。

⚠️定义1的反例:

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第9张图片同时也符合传统红黑树的定义:

  1. 节点的颜色是红色或者黑色;
  2. 根节点是黑色的;(根性质
  3. NIL(null) 节点的颜色是黑色;
  4. 如果节点的颜色是红色,则其子节点均为黑色;(红性质
  5. 从任一节点到其后代任一叶子节点的nil节点的路径上的黑色节点的数量相同;(黑性质

 

 

如何将2-3树这种保持完美平衡的特性移植到二叉搜索树上呢?

要知道,二叉树不允许一个节点储存2个key值的。解决的办法就是,如果我们将2-3中的链接表示为二叉树中的黑色链接的话,那么我们就将拥有2个key值的节点表示为用红色链接连接起来的两个节点。如下图:

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第10张图片

 变为:

用红线和左右2个节点表示一个2-key节点。

红线链接的左节点,链接左子节点和中间子节点。红线链接的右节点,链接一个右子节点。

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第11张图片

 

上图就是一棵红黑树 

把2-3树的2-key节点用两个节点和一个红色的左链接表示了出来。

为了表示在原本的2-3树中的关系,我们将红色的左链接画成了平级关系,而不是父子关系。

这种用红线的左链接来表示2-key节点的红黑树,我们称之为左倾红黑树

⚠️左倾红黑树是2008年发明的一种红黑树,比传统的红黑树更简单,代码更少。

 

因为无法用代码来区分链接的颜色,所以将链接的颜色转换为被指向的节点的颜色。所以上面的红黑树可以这样画:

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第12张图片

 

如果改为父子关系就是:

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第13张图片

 

 

红黑树实际就是一种特殊的二叉搜索树,所以二叉搜索树的很多方法,例如max、min、search、keys,遍历等方法,基本可以搬过来在红黑树中使用。但是,为了在插入和删除的同时维持红黑树的平衡,我们在插入和删除操作上需要做一定的修改。

 

 

回想2-3树的插入。有两种情况:叶节点包含1个数据和包含2个数据。对应红黑树。看本例子:

 

 

 

 

 

 

 

 

左倾红黑树就是一种特别的2-3树;而二叉搜索树只要能转化为2-3树,就代表它是一个平衡树。

 

红黑树的插入方法

红黑树的插入是通过旋转来降低/升高树的高度。本质上它的插入的方式是按照2-3树(或者2-3-4树)插入节点的方式来完成再平衡的。

LLRBT之所以使用左倾(left-leaning)则是为了将2-key节点(3树)限制为一种,以减少实现上的复杂程度。

本质

左倾红黑树本质上就是2-3树新插入节点后,形成了3key节点,通过分裂并向上合并的操作,调整树的高度,以保持树的平衡。只不过红黑树使用了左倾(左旋,右旋,上传红色)的方法来达目的。

插入方法

红黑树在最初插入时使用的方法和二叉树一样,递归查找到树的底部,然后将新节点插入到树底部。在递归的回归时对整棵树进行旋转调整。

规则:

  1. 每个节点默认插入是红色的。
  2. 判断是否有红色节点在右边,如果有,则将该红色节点的父节点左旋(以符合定义3)
  3. 判断左侧是否有连续的红节点,如果有,则将连续红色节点起始位置的节点的父节点右旋转。(定义的第一条:一个节点不能同时有2条红链接。)
  4. 判断是否存在左右子节点均是红色的情况,如果有,则将左右子节点的红色上传。(以符合定义3)

 

开始插入:

  • 开始只有一个根节点key值为0。
  • 插入红节点1。此时对应2-3树的一个节点包含2key值的情况。
  • 然后进行左旋转,然后改变颜色,以符合左倾的规则。

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第14张图片

 

  •  接下来插入红节点2。此时对应2-3树的3key节点,所以中间节点1是上传为父节点。
  • 而在红黑树中,因为红节点只能在左侧,所以此时要进行变色处理:
    • 把红色上传,父节点变为红色,左右儿子变为黑色。
    • 因为父节点也是root,所以节点1变为黑色。
  • 整个插入完成后,得到的红黑树和2-3树的结构一致。

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第15张图片

 

  • 插入红节点3.
  • 根据左倾规则,红节点只能在左边,所以执行左旋。完成后和2-3树插入的结果是一致的。(可见上面2-3树的图)

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第16张图片

  •  接着插入4.
  •  形成了左右儿子都是红色的状况,不符合左倾规则。
  •  变色,然后上传。此时3节在右侧,仍然不符合左倾规则,还要左旋转处理。
  •  左旋转:节点1及其左子树成为节点3的左子树。把原先3的左子树,放到节点1的右子树上。
  •  变色,让节点1为红色,3为黑色。
  •  此时的形态和2-3树是一致的。(即把红黑树的红节点和其父节点看做一个节点)

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第17张图片

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第18张图片

 所以,红黑树可以和2-3树一样有效的保存树的平衡,同时又是一个二叉树的结构。

  •  接下来插入-1。
  •  然后插入-2。此时出现左侧连续2红色的情况。要处理它。(以符合定义1)
  •  首先,使用右旋转。对-1的父节点0右旋转。
  •  然后,改变节点-1的颜色,变为黑色;节点0的颜色变为红色。(以符合定义2)
  •  之后,因为节点-2和0是红色的,所以上传颜色到-1。(符合定义3)
  •  然后,又发现1和-1是链接的,对1的父节点右旋转。(以符合定义1)
    • 此时注意还要改变1和3的颜色。被右旋的节点3变为红色。1变为黑色。(以符合定义2)
  • 同样上传红色给1,但此时1是根节点,所以1还是黑色。(以符合定义3)

 从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第19张图片

 

 

 从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第20张图片

 

 

 

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第21张图片

 

 

代码参考:https://github.com/leyafo/practice-algorithm/blob/master/DataStruct/rb_tree.c

改使用ruby代码并重构:

首先建立一个Node类,用于储存节点实例。

然后建立一个RedBlackTree类,添加插入/删除等方法,对树的实例对象进行操作。

class Node
  attr_accessor :item, :color, :left, :right
  attr_reader :key

  def initialize(key, item = "any data")
    @key = key
    @item = item                         #  用于在节点内储存需要的数据,可以是任意格式
    @color = :red                        #  默认插入的节点的颜色为红色
    @left = nil
    @right = nil
  end
end

定义一个红黑树类,以生成左倾红黑树实例:

  1. 红黑树实例包括@root, 用于储存根节点(Node实例),默认为nil
  2. @lenght用于储存树的节点总数。
  3. 实例方法insert(key, item),用于树的插入节点。
    • 因为根据左倾rdt性质,@root由于调整操作会发生改变,所以需要给@root变量重新赋值。
    • 并注意始终保持根节点的color为black。
  4. private私有方法,为insert()方法服务。
class RedBlackTree
  def initialize
    @root = nil                       #初始化一个树,默认根节点nil。
    @length = 0                       #记录树的节点总数
  end
   
  def insert(key, item)               # 插入操作
    @root = _insert(@root, key, item) #左倾红黑树,新插入节点后,经过调整,根节点可能发生改变
    @root.color = :black             # 根据定义,根节点为黑色。如果调整时把根节点的颜色变为红色,需要改回来
  end

  private
  #私有方法。。。
end

 

 私有方法

 _insert(), fix_up(),is_red?(), rotate_left(), rotate_right(), shift_color()

 (明确这不是tree实例能够直接调用的方法)

 

1.  _insert(node, key, item)的参数:

  •  node节点:  用来和新插入节点的key进行比较。
  •  新插入节点:key和item是其属性。

下面是插入节点3的整个递归过程:

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第22张图片

解释:_insert()是一个递归函数。

# 因为每个节点和其左右子树都是一个二叉搜索树,所以可以使用递归的方法。
# 首先对插入的节点的key和root节点的key进行比较,然后向下传递参数(儿子node和新插入节点的key,item)。
# 传递参数并逐层的对节点key值进行比较,直到找到要插入的位置。 #
完成插入节点Node.new(),会对树进行平衡(即对左倾红黑树调整,以符合最佳时间复杂度)。
# 然后回归到上一个节点,继续再fix_up平衡,直到回归到最初tree.insert()方法内。
def _insert(node, key, item) if node == nil @length += 1 return Node.new(key, item) end if key < node.key node.left = _insert(node.left, key, item) elsif key> node.key node.right = _insert(node.right, key, item) elsif key == node.key puts "key can't same。" return end return fix_up(node) end

 2. fix_up()用来确定左倾红黑树的三种调整方式:左旋,右旋,上传颜色。并返回调整后的父节点。

    def fix_up(n)
      n = rotate_left(n) if is_red?(n.right) && (n.left == nil || n.left.color == :black)
      n = rotate_right(n) if is_red?(n.left) && is_red?(n.left.left)
      n = flip_color(n) if is_red?(n.left) && is_red?(n.right)
      return n
    end

 3. 用于判断是否是红节点来选择不同调整方式

def is_red?(n)
  n != nil && n.color == :red
end

 4. rotate_right()左旋转调整:

def rotate_left(n)
   #左旋:
   #1. 得到新插入节点new_node
   #2. new_node的原左儿子放到node的右儿子位置。(回归中产生的情况。)
   #3. 最后,把node放到new_node的左儿子位置。
  new_node = n.right
  n.right = new_node.left 
  new_node.left = n
  #改色
  new_node.color = n.color
  n.color = :red
   
  return new_code
end

#node的父节点的right需要改成new_node。通过回归就做到了。node.right = _insert()

 

问题:为什么在左旋时,方法rotate_left(n)不改变节点1的右儿子,应该从2改为3啊?

答:递归函数的回归对象是调整后的节点,它会被上层父节点所指向。代码中node.right指向了_insert()返回的对象。

node.right = _insert(node.right, key, item) 
#node.right 指向 _insert()方法返回的node节点3。

_insert方法有2个return。一个是返回Node.new创造的新节点,另一个是返回fix_up()后的节点。同样以上面_insert()过程图为例子,在rotate_left左旋后,fix_up返回的新插入节点3被根节点1的right所指(right指向节点3),因此节点1的右儿子也就改变了。这是递归算法中的回归。(递与归)

 

4. rotate_right(n)右旋转调整:

    def rotate_right(n)                    # 右旋一个节点
      n_l = n.left
      # 如果n_l有右儿子的话,需要重新调整位置。
      n.left = n_l.right
      n_l.right = n
      #调整后,改颜色
      n_l.color = n_l.right.color
      n_l.right.color = :red
      return n_l
    end

 

以下图举例分析代码:

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第23张图片 

第一回归的是新插入的节点2。

第二次回归使用fix_up(-1),经过判断无需调整,所以返回的还是节点-1

第三次回归使用fix_up(0), 经过判断需要调整,

此时右旋后n是节点-1,但还有进行判断发现节点-1的两个儿子都是红,所以上调颜色。调用pop_color(-1)

    def pop_color(n)                      
      n.color = :red                    
      n.left.color = :black               
      n.right.color = :black
      return n
    end

,然后返回的是调整位置和颜色为红的节点-1。

第4次回归使用fix_up(1), 经过判断无需调整,返回1。

第5次回归使用fix_up(3), 经过判断需调整(节点3的左儿子1和节点1的左儿子-1都是红色的),进行右旋转rotate_right(),后 n是1,又经过判断节点1有双红儿子,需要pop_color()。

最后,返回到tree.insert()函数中, @root根节点指向红色的节点1,改变颜色@root.color = :black。

总结:

以上就是左倾红黑树插入的原理和代码及其讲解。

完整代码:https://github.com/chentianwei411/left_lean_red_black_tree/blob/master/llrbt.rb

 

2-3树改成2-3-4树:

只需要改变一下上色的代码的位置。⚠️新插入的节点是黑色的。

    def _insert(node, key, item)
      if node == nil
        @length += 1
        return Node.new(key, item)
      end
    
      node = flip_color(node) if is_red?(node.left) && is_red?(node.right)

      if key < node.key
        node.left = _insert(node.left, key, item)
      elsif key > node.key
        node.right = _insert(node.right, key, item)
      end

      node = rotate_left(node) if is_red?(node.right) 
      node = rotate_right(node) if is_red?(node.left) && is_red?(node.left.left)
      return node
    end

 

 

遍历方法:

遍历方法就是二叉搜索树的原代码无需改动:(中序遍历)

  def inorder_tree_walk(node)
    if node.left != nil
      inorder_tree_walk(node.left)
    end
    p node.key
    if node.right != nil
      inorder_tree_walk(node.right)
    end
  end

 

查找一个节点:就是二叉搜索树的方法,传入一个key值后从root开始比较,直到底层nil。

class RedBlackTree
  #。。。略
  def include?(node = self.root, number)
    if node.key > number && node.left != nil
      include?(node.left, number)
    elsif node.key < number && node.right != nil
      include?(node.right, number)
    elsif node.key == number
      return true
    else
      return false
    end
  end

 

 

左倾红黑树的删除方法 :

我的思路:

删除一个节点,可以理解为删除的这个节点是刚刚新增的,所以回复到刚刚添加这个节点的状态然后删除即可。

因为左倾红黑树本质上可以是2-3树,先思考2-3树的删除是怎么回事。见上面 2-3树的删除。

参考:https://www.cs.princeton.edu/~rs/talks/LLRB/RedBlack.pdf 

删除策略:

从2-3树的删除方式可知道:

  • 如果要删除的节点不是2-node节点。即这个节点是2-key或3-key。直接删除。
  • 如果需要,引进4-nodes节点(3-key节点)
  • 从底部删除key节点。
  • 在上升途中,排除掉3-key节点。

从简单的删除最大值和最小值开始:

 

删除最大值:

文章参考:https://zhuanlan.zhihu.com/p/55711442

原理:

2-3树从root开始沿着右子树搜索最大值。

  • 如果搜索结果是是2-key,3-key节点,2-3树即可直接删除最大值,而为保持左倾红黑树结够,L需要右旋后,再删除最大值。
  • 如果搜索结果是单key节点,直接移除会破坏的平衡,因此需要从父亲节点或者兄弟节点借用节点。

常规思路是,搜索到结果,然后判断,如果是单key就向上借节点。但如果父节点也是单key节点还要向上借。

因此,从效率上考虑:

  1. 可以从root开始就沿着搜索路径转化tree,把右子树的节点都转化为可以借出节点的2-key节点。
  2. 找到最大值并删除,
  3. 再从下往上恢复左倾红黑树的结构。

例子:下面两个图都是2-3树删除最大值的方式:最大值是单key所以向父节点借节点再删除,依然保持了2-3树结构。

 

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第24张图片

 

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第25张图片

左倾红黑树的删除最大值方法,思路就是2-3树的方法。但需要为保持红黑树结构,做右旋,变色等操作。

具体方法:

从根节点开始向下寻找最大key所在节点位置,在这个过程中如果发现当前节点的右节点是单节点,就需要借节点给这个单节点。

首先,判断当前节点n的left是否是red(即当前节点是否是2-3树中的2-key节点)。如果是的话,就右旋,将左子节点的红色转移到右边。为将key借给右子节点做作准备。

然后,判断n.right和n.right.left是否都是黑色,如果是的话,从2-3树来看,n的right不是2-key节点。是单节点。那么就要从n那里借出key给它了。目的是为了下面别的节点向它借的时候,没有多余的节点可借。

此时有2种处理办法:

  1. 简单的:  n的right节点h的left的left是也黑色的。那么直接改色下color即可。返回h。
  2. 复杂的:n的right节点h的left的left是红色的。那么先改色⬇️color,然后右旋,然后再⬆️color。返回h。

 

ruby分析代码:(全部代码见git)

  def delete_max
    # 只有root节点的情况:
    if @root.right == nil && @root.left == nil
      @root = nil
      return puts "delete root node, then tree is nil"
    end
    @root = _delete_max(@root)
    @root.color = :black
  end

  private
    def _delete_max(node)
      # 相当于把双key节点的较大值准备借出。
      if is_red?(node.left)
        node = rotate_right(node)
      end
      # 节点的right是nil,则判断为最大节点,返回nil
      if node.right == nil
        # 树节点总数减1.
        if @length > 0
          @length -= 1
        end
        return nil
      end

      # 从2-3树来看,当前节点的右节点不是双key节点的话,就需要借用了。
      if !is_red?(node.right) && !is_red?(node.right.left)
        node = move_red_right(node)
      end
      #继续移动到下一层:
      node.right = _delete_max(node.right)
      #删除完后需要,从下向上修复左倾红黑树结构。
      return fix_up(node)

    end

    def move_red_right(node)
      #借用有2种处理:即node.left.left是否是红的。
      flip_color(node)
      if is_red?(node.left.left)
        node = rotate_right(node)
        flip_color(node)
      end
      return node
    end

 

 

删除最小值

类似删除最大值,但有些许不同。因为左倾红黑树,左儿子,一般是红色,无需再左旋转了。

直接删除的情况:

  • 从2-3树角度看:最小值n是双key节点就可以直接删除:
  • 从红黑树角度看:n是red即可直接删除。

从兄弟借节点(即最小值不是双key节点的情况下):

  • 从红黑树角度看:如果n.left和n.left.left都是black, 即都是单key节点。就需要借用。
  • 又分两种情况:看n.right.left是不是红色(从2-3树看,n的右节点是不是2-key节点)

方法和删除最大值类似:

首先,从root开始递归,判断当前节点的left和left.left的情况,看是否需要借用。

  • 是的话:调用move_red_left。
  • 不是的话, 继续递归:node.left = _delete_min(node.left)

 

具体代码见git

小结:不论是删除最大值还是删除最小值,删除的都是叶节点。思路是把左倾红黑树想象成2-3树,为了保持树的平衡,只能删除即是2-key叶节点中的数据项。如果不是双key节点就要从父亲兄弟节点去借用节点,形成双key节点,删除后,再恢复成左倾红黑树的结构。

 

删除任意节点

最后终于来到删除任意节点的方法:delete()

参考:https://www.cs.princeton.edu/~rs/talks/LLRB/RedBlack.pdf

delete方法,将适用于delete_min和delete_max。

 

当前节点node或者它的一个儿子是:red.

  1. 从左子树开始搜索:使用move_red_left
  2. 从右子树开始搜索:使用move_red_right
  3. 删除的节点必须位于底部,即是叶子节点。(为了树的平衡)
  4. 在递归的回归中,fix_up, 重新恢复成左倾红黑树。

⚠️第三点。回想2-3树是如何删除非叶节点的。左倾红黑树本质是2-3树,因此方法同样适用。

代码见git。

 

❌了测试删除不能通过?

错误1:

llrbt.rb:103:in `delete': undefined method `key=' for # (NoMethodError)
Did you mean? key

使用byebug xx.rb跟踪:

   102:       if node.key == k
   103:         next_node = min(node.right)
=> 104:         node.key = next_node.key
   105:         # node.item = next_node.item
   106:         node.right = delete_min(node.right)
   107:       else
   108:       #没有找到要删除节点,则移动到下一层:
(byebug) step
undefined method `key=' for #
Did you mean?  key

Node对象为什么不能使用key=方法,看class Node类的代码发现attr_reader :key

所以报告这个❌。

 

错误2:

删除0/1/2的话,遍历树,发现5以后的节点都被删除了。

但是删除最小值是正确的。

然后byebug xx.rb一下,看看过程。哪里出现了问题?

答案:发现遍历正常结束后,返回的是节点3,但返回值并没有赋值给tree2的root节点。所以出现❌。

(我的笔记)

 

tree2 = RedBlackTree.new()
tree2.insert(0, 'a')
[1,2,3,4,5,6,7,8,9].map { |e| tree2.insert(e, 'aa')  }
# 3.times do |x|
#   puts "第#{x + 1} 次:"
#   tree2.delete_min
#   tree2.inorder_tree_walk(tree2.root)
# end
tree2.inorder_tree_walk(tree2.root)
tree2.delete(0)
puts "..."
# tree2.delete_min
tree2.inorder_tree_walk(tree2.root)

 下面是代码有❌,改正方法见git:

  1. 增加一个私有方法_delete方法,把下面代码放入_delete, ⚠️代码内的delete改成_delete
  2. delete(k)方法改为: @root = _delete(@root, k),接收回归的对象给根节点。
  def delete(node = self.root, k)
    #删除节点在左子树。left
    if node.key > k
      if !is_red?(node.left) && !is_red?(node.left.left)
        node = move_red_left(node)
      end
      node.left = delete(node.left, k)
    # 删除节点在右子树。 right
    else
      # 类似删除最大值:
      if is_red?(node.left)
        node = rotate_right(node)
      end
      # 在树底部,找到要删除的节点。删除它,返回nil。
      if node.right == nil && node.key == k
        return nil
      end
      # 当前节点的右节点不是双key节点的话,就需要借用了。
      if !is_red?(node.right) && !is_red?(node.right.left)
        node = move_red_right(node)
      end
      # 在树中间层找到要删除的节点。需要转化:
      # 思考?
      # 结论:2-3树删除中间层的节点的方法同样适用于这里:
      # 找到要删除节点x的中序列遍历的后续节点next_node,它一定在底层,交换两者。然后删除x。
      # 考虑红黑树的代码:
      # 1.node右子树的最小值即它的后续节点next_node。
      # 2.把next_node的key和item(即储存的内容)复制给节点x。那么节点x就被替换掉了。相当于x被删除了。
      # 3.最后删除在底部的重复的后续节点next_node。
      if node.key == k
        next_node = min(node.right)
        node.key = next_node.key
        # node.item = next_node.item
        node.right = delete_min(node.right)
      else
      #没有找到要删除节点,则移动到下一层:
        node.right = delete(node.right, k)
      end
    end

    return fix_up(node)
  end

  def min(node)
    if node.left != nil
      min(node.left)
    else
      return node
    end
  end

 

 

 


 

红黑树

简介:

红黑树是2-3-4树的一种抽象表示,在1978年 Guibas 和 Sedgewick 发明最初的红黑树。2008年Sedgewick 对其进行了改进,并将此命名为 LLRBT(Left-leaning red–black tree 左倾红黑树)。

LLRBT 相比1978年的红黑树要简单很多,实现的代码量也少很多。Sedgewick的一篇 PPT 对此有非常详细的介绍。

现在大部分工程中的红黑树都是基于1978发明的算法。

 

1,2-3-4树就是红黑树的抽象表示。

2, 红黑树使用internal红链接来代表3节点(2key节点)和4节点(3key节点)

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第26张图片

 

 

 

算法导论对R-B Tree的介绍:
红黑树,一种二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。

通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。 

因此最大的时间复杂度是2倍的log2(N+1)。

红黑树还添加了一种特殊类型的节点,称为 NIL 节点。NIL 节点将做为红黑树的伪叶子节点出现。也就是说,所有带有关键数据的节点称为内节点,而所有其他的外节点则均指向 NIL 节点。

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第27张图片

红黑树(R-B Tree)需要满足如下性质:

  1. 节点的颜色只能是红色或者黑色;
  2. 根节点是黑色的;(根性质
  3. NIL 节点的颜色是黑色;
  4. 如果节点的颜色是红色,则其子节点均为黑色;(红性质
  5. 从任一节点到其后代任一叶子节点的nil节点的路径上的黑色节点的数量相同;(黑性质

 

只有最后一条最难理解。简单的说,从树中任意一个节点开始,从该节点到其后代的任意一个 NIL 节点的路径上的黑色节点的数量必须相同。

比如上图中,以根节点为例,从节点 41 到任意一个 NIL 节点的路径上,黑色节点的数量都是相同的,也就是 3 个。如从节点 41 到左下角的 NIL 节点的路径上,黑色节点包括 41, 2, NIL,所以黑色节点数量是 3 个。

类似于 AVL 树,红黑树也是一种自平衡二叉查找树。AVL 树的平衡性质是通过限制节点的左右子树的高度来达成,而红黑树则是通过更形象化的方式来保证树的平衡。如果一棵树满足红黑树的性质,其节点的总数量为 n,则它的高度将始终小于 2 * log2(n+1) 。鉴于这个原因,致使红黑树保证了对树的所有操作都能在 O(log2n) 渐进运行时间范围内。

AVL 树通过使用旋转操作(rotations)来恢复树的平衡。而红黑树则是通过重新着色(recoloring)和旋转两种操作共同来完成。这不仅需要判断节点的父节点的颜色,还需要对比叔父节点的颜色,使得红黑树的恢复过程变得更加复杂。

 

如何插入:

假设已存在红黑树 T,即将被插入的新节点为 K。

如果树 T 为空,则可直接将节点 K 设置为根节点,并且将颜色标为黑色,这样即可满足 R-B 树的所有要求。

如果树T不为空,则需要:

  1. 使用 BST 插入算法将节点 K 插入到树 T 中;
  2. 将节点 K 着色为红色;
  3. 如果需要,则重塑 R-B 树的性质

⚠️BST的性质,任意节点c,其左子树中的节点必小于c, 右子树的节点必大于c。

 

针对第3步, 添加一个红色的叶子节点仅有可能影响树 T 的红性质,所以我们仅需检查树的红性质,如果红性质被违背,则需要重塑树结构以重新满足红黑树性质。

我们将节点 K 的父节点称为节点 P(parent node),将节点 P 的父节点称为节点 G(grandparent node),将节点 P 的兄弟节点称为节点 S(sibling node)。

       G

   P      S

k

当向非空树 T 中插入节点 K 时,将直接受到父节点 P 的颜色的影响,可能会遇到如下多种情况。

情况1:节点 P 是黑色。

如果 P 为黑色,而节点 K 为红色,所以实际上不会违背黑性质,则树 T 已经满足所有红黑树性质条件。

情况2:节点 P 是红色。

如果节点 P 为红色,那么 P 现在有了新的子节点 K,并且 K 也为红色,所以已经违背了红性质。为了处理这种两个红节点的情况,我们需要考虑节点 G 的其他子节点,也就是节点 P 的兄弟节点 S。此时,会有两种情况发生:

情况 2a:节点 S 是黑色或者为空。

如果节点 S 是黑色或者为空,则需要对节点 K、P、G 进行旋转。根据 K、P、G 顺序的不同,旋转操作可能存在四种可能性。

前两种可能性为当 P 为 G 的左子节点时。

情况 2a:节点 S 是黑色或者为空。

如果节点 S 是黑色或者为空,则需要对节点 K、P、G 进行旋转。根据 K、P、G 顺序的不同,旋转操作可能存在四种可能性。

前两种可能性为当 P 为 G 的左子节点时。

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第28张图片

如果 S 为空,则上图中直接将 S 删除即可。

另两种可能性为当 P 为 G 的右子节点时,正好与上面图中的过程相反。

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第29张图片

 

情况 2b:节点 S 是红色。

如果 P 的兄弟节点 S 是红色,则需要对 P、S、G 进行重新着色:将 P 和 S 着色为黑色,将 G 着色为红色。

从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树_第30张图片

重新着色操作不会影响树 T 的黑性质,因为当 P、G 的颜色更改时,所有路径上的黑色节点数量并没有改变。但是,重新着色可能会使 G 和 G 的父节点产生双红情况。这种情况下,则需要从 G 和 G 的父节点开始继续遵循处理 K 和 K 的父节点的方式递归式地解决双红问题。

对于红黑树深入的讨论不在本文的范畴,这里不再赘述。

 


红黑树和AVL树的比较

红黑树在AVL树之后出现,目的在于提高效率。

自平衡二叉树的平衡条件是:

如果树中节点的数量为 n,则一棵满足O(log2n) 渐进运行时间的 BST 树的高度应接近于比 log2n 小的最大整数。

但是红黑树的查找效率时间复杂度为2*O(log2n)比AVL高一些,但是由于它不是严格的自平衡,所以在删除和插入节点时,旋转的次数比AVL树少很多。总体效率就比AVL树高!

 

这里引用一下知乎上的回答:

Answer 1:
1. 如果插入一个node引起了树的不平衡,AVL和RB-Tree都是最多只需要2次旋转操作,即两者都是O(1);但是在删除node引起树的不平衡时,最坏情况下,AVL需要维护从被删node到root这条路径上所有node的平衡性,因此需要旋转的量级O(logN),而RB-Tree最多只需3次旋转,只需要O(1)的复杂度。

2. 其次,AVL的结构相较RB-Tree来说更为平衡,在插入和删除node更容易引起Tree的unbalance,因此在大量数据需要插入或者删除时,AVL需要rebalance的频率会更高。因此,RB-Tree在需要大量插入和删除node的场景下,效率更高。自然,由于AVL高度平衡,因此AVL的search效率更高。

3. map的实现只是折衷了两者在search、insert以及delete下的效率。总体来说,RB-tree的统计性能是高于AVL的。
作者:Acjx
链接:http://www.zhihu.com/question/20545708/answer/58717264

 

Answer 2  这个总结比较好:
红黑树的查询性能略微逊色于AVL树,因为他比avl树会稍微不平衡最多一层,也就是说红黑树的查询性能只比相同内容的avl树最多多一次比较,但是,红黑树在插入和删除上完爆avl树,avl树每次插入删除会进行大量的平衡度计算,而红黑树为了维持红黑性质所做的红黑变换和旋转的开销,相较于avl树为了维持平衡的开销要小得多

作者:陈智超
链接:http://www.zhihu.com/question/43744788/answer/98258881

 

Answer 3 :
功能、性能、空间开销的折中结果。

AVL更平衡,结构上更加直观,时间效能针对读取而言更高;维护稍慢,空间开销较大。

红黑树,读取略逊于AVL,维护强于AVL,空间开销与AVL类似,内容极多时略优于AVL,维护优于AVL。
基本上主要的几种平衡树看来,红黑树有着良好的稳定性和完整的功能,性能表现也很不错,综合实力强,在诸如STL的场景中需要稳定表现。
作者:Coldwings
链接:http://www.zhihu.com/question/20545708/answer/44370878
 

下面的文章来源:http://blog.csdn.net/klarclm/article/details/7780319

1 好处 及 用途

红黑树并不追求“完全平衡”——它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。红黑树能够以O(log2 n) 的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构 能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高。

当然,红黑树并不适应所有应用树的领域。如果数据基本上是静态的,那么让他们待在他们能够插入,并且不影响平衡的地方会具有更好的性能。如果数据完全是静态的,例如,做一个哈希表,性能可能会更好一些。

在实际的系统中,例如,需要使用动态规则的防火墙系统,使用红黑树而不是散列表被实践证明具有更好的伸缩性。

2 AVL树是最先发明的自平衡二叉查找树。

在AVL树中任何节点的两个儿子子树的高度最大差别为一,所以它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下都是O(log n)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者 G.M. Adelson-Velsky 和 E.M. Landis,他们在 1962 年的论文 "An algorithm for the organization of information" 中发表了它。

 

引入二叉树的目的是为了提高二叉树的搜索的效率,减少树的平均搜索长度.为此,就必须每向二叉树插入一个结点时调整树的结构,使得二叉树搜索保持平衡,从而可能降低树的高度,减少的平均树的搜索长度. 

AVL树的定义: 
一棵AVL树满足以下的条件: 
1>它的左子树和右子树都是AVL树 
2>左子树和右子树的高度差不能超过1 
从条件1可能看出是个递归定义,如GNU一样. 

性质: 
1>一棵n个结点的AVL树的其高度保持在0(log2(n)),不会超过3/2log2(n+1) 
2>一棵n个结点的AVL树的平均搜索长度保持在0(log2(n)). 
3>一棵n个结点的AVL树删除一个结点做平衡化旋转所需要的时间为0(log2(n)). 

从1这点来看红黑树是牺牲了严格的高度平衡的优越条件为 代价红黑树能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构 能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高. 

 

 

 

你可能感兴趣的:(从二叉查找树到平衡树:avl, 2-3树,左倾红黑树(含实现代码),传统红黑树)