查找算法——红黑树

引言:红黑树的出现与定义

1. BST(二叉查找树)存在的问题

  BST存在的主要问题是,数在插入的时候会致使树倾斜,不一样的插入顺序会致使树的高度不同,而树的高度直接的影响了树的查找效率。理想的高度是logN,最坏的状况是全部的节点都在一条斜线上,这样的树的高度为N。html

2. 红黑二叉树的定义与应用

  基于BST存在的问题,一种新的树——平衡二叉查找树(Balanced BST)产生了。平衡树在插入和删除的时候,会经过旋转操做将高度保持在logN。其中两款具备表明性的平衡树分别为AVL树和红黑树。AVL树因为实现比较复杂,并且插入和删除性能差,在实际环境下的应用不如红黑树。
  红黑树(Red-Black Tree,如下简称RBTree)的实际应用很是普遍,好比Linux内核中的彻底公平调度器、高精度计时器、ext3文件系统等等,各类语言的函数库如Java的TreeMap和TreeSet,C++ STL的map、multimap、multiset等。
  RBTree也是函数式语言中最经常使用的持久数据结构之一,在计算几何中也有重要做用。值得一提的是,Java 8中HashMap的实现也由于用RBTree取代链表(当链表长度8时),性能有所提高。java

  RBTree的定义以下(3结点+2路径,插入时受到威胁就是2路径):算法

  1. 任何一个节点都有颜色,黑色或者红色
  2. 根节点是黑色的
  3. 空节点被认为是黑色的(即NIL结点)
  4. 每一个红色节点必须有两个黑色的子节点。(从每一个叶子到根的全部路径上不能有两个连续的红色节点。)
  5. 从任一节点到其每一个叶子的全部简单路径都包含相同数目的黑色节点。(这也叫作完美黑色平衡,与2-3树的完美平衡有些不一样)

 

3. 红黑树的性能

  定义中的这些约束确保了红黑树的关键特性从根到叶子的最长的可能路径很少于最短的可能路径的两倍长结果是这个树大体上是平衡的(平衡的复杂度为O(lgN),简略能够说红黑树也是O(lgN)),严格的所须要数学证实。由于操做好比插入、删除和查找某个值的最坏状况时间都要求与树的高度成比例,这个在高度上的理论上限容许红黑树在最坏状况下都是高效的,而不一样于普通的二叉查找树。
  要知道为何这些性质确保了这个结果,注意到性质4致使了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。由于根据性质5全部最长的路径都有相同数目的黑色节点,这就代表了没有路径能多于任何其余路径的两倍长
  RBTree在理论上仍是一棵BST树,可是它在对BST的插入和删除操做时能经过旋转操做来保持树的平衡,即保证树的高度在[logN,logN+1](理论上,极端的状况下能够出现RBTree的高度达到2*logN,但实际上很难遇到)。这样RBTree的查找时间复杂度始终保持在O(logN)从而接近于理想的BST。RBTree的删除和插入操做的时间复杂度也是O(logN)。因为RBTree结构上就是一颗二叉查找树,因此其查找操做就是BST的查找操做。因此就是RBTree的查找、插入、删除在最坏状况下的复杂度都是O(lgN)的。安全

4. 关于本文的讨论范围与参考源码

  关于红黑树的旋转还好在算法导论上有伪代码,注释讲解也很通透,没什么问题。网上关于红黑树的插入和删除讨论不少,但其中各类错误都有的,把左倾红黑树当成原版红黑树也有之,每一个人都有各类不一样的状况分析你分三种我分五种这样,也让人看了心烦,很难找到一份靠谱或者说权威的。
  美团技术团队知乎上那篇红黑树深刻剖析及Java实现里面一些点总结的不错,但也搞错了几个小地方,致使源码也是有错误的地方,评论里也有人指出来了。后来以为这种各类不可以彻底靠谱的源码与分析看着实在费劲,由于java8当中的TreeMap就是采用的红黑树,因此干脆本身看TreeMap的源码了,当时以为源码本身看不懂再去google一些英文靠谱点的文章吧。但最后经过一步步分析源码明白了二叉红黑树的插入原理(虽然搞到了凌晨三点多...),画出流程图并进行对照总结以后发现就是美团技术团队中所总结的三种,因此说它总结的不错但它的那个少写了个黑色结点的状况包括给出的源码也是。因此对于插入操做的参考源码就是TreeMap上的源码,为了方便看我把一些保证安全的泛型去掉了,将Entry改为了Node,具体完整的实现仍是要看TreeMap源码。
  对于删除应该是更为复杂的操做了,因此暂时还没怎么看,后面可能会补上吧。数据结构

1、结点

  红黑树中的结点增长了一个属性color,来表示结点的颜色,能够是RED或者BLACk,因此共包含6个属性:color、key、value、left、right和parent函数

private static final boolean RED   = false;
    private static final boolean BLACK = true;

    static final class Node {
        K key;
        V value;
        Node  left;
        Node  right;
        Node  parent;
        boolean color = BLACK;

        Node(K key, V value, Node  parent) {
            this.key = key;
            this.value = value;
            this.parent = parent;
        }

2、旋转操做

  描述的更准确些,不是红黑树的旋转操做,而是二叉树树的旋转操做,也就是在二叉树中的一种子树调整操做, 每一次旋转并不影响对该二叉树进行中序遍历的结果,也就是旋转先后两棵树上的结点直线投影来的序列同样的,这样旋转不会违反结点的大小关系(即左小右大)。树旋转一般应用于须要调整树的局部平衡性的场合。旋转分为左旋转右旋转,其实直白点应该说成是把示意图中展示出的结点多的一方向左旋转向右旋转。在写旋转代码的时候能够配合示意图来写(对于旋转的两个根对象,写完每一行指针指向相应的对象)。图中所画出来的结点也就是咱们旋转的时候会涉及到的结点。
clipboard.png
 
  为了更加针对红黑树(结点有父指针),我画出了下面的这张图,双向箭头表明着左或右指针指向和父指针指向,红色部分表明进行旋转时须要改变的连接关系。性能

clipboard.png

private void rotateLeft(Node p) {
        if (p != null) {
            Node r = p.right;
            p.right = r.left;
            if (r.left != null)
                r.left.parent = p;
            r.parent = p.parent;
            if (p.parent == null)
                root = r;
            else if (p.parent.left == p)
                p.parent.left = r;
            else
                p.parent.right = r;
            r.left = p;
            p.parent = r;
        }
    }
    private void rotateRight(Node p) {
        if (p != null) {
            Node l = p.left;
            p.left = l.right;
            if (l.right != null) l.right.parent = p;
            l.parent = p.parent;
            if (p.parent == null)
                root = l;
            else if (p.parent.right == p)
                p.parent.right = l;
            else p.parent.left = l;
            l.right = p;
            p.parent = l;
        }
    }

  旋转的过程如代码所示,能够划分为下面的四个过程:this

  • 咱们传入的这个结点时要降低的x,先找到另外一个要上升的结点并命名为y(就是x和旋转方向相反的那个子结点)
  • 而后找出y与旋转方向一致的那个子结点,将它赋给x与旋转方向相反的那个指针;
  • 而后看y的那个与旋转方向一致的那个子结点是否不为空,不为空则将其父指针指向x;再将x的父结点赋给y的父指针;而后接下来判断这个父结点是否是空,是的话则将y赋值给整棵树的root,不为空继续判断x是否是这个父结点的左孩子,是的话将这个父结点的左指针指向为y,如还不是将这个父结点的右指针指向为y。
  • 而后将y的与旋转方向一致的那个指针指向x,x的父指针指向y。

 
  关键注意的就是y在旋转的时候会丢失一个与旋转方向一致的结点,而后用x来填补这个位置(赋值给与旋转方向一致的指针)。x则会用与旋转方向相反的那个指针来改指向这个结点,而后就是这个结点若是存在的话就把它的父指针也改指为x。程序流程中先处理了那个丢失的结点(y结点自己是逆贼上位,在旋转方向上有失有得,x在旋转方向相反方向上有失有得),再处理了父结点,最后处理了x、y之间的关系google

3、插入操做

1. 插入时的要求与影响

  咱们首先以二叉查找树的方法增长节点并标记它为红色。(若是设为黑色,就会致使根到叶子的路径上有一条路上,多一个额外的黑节点,这个是很难调整的。可是设为红色节点后,可能会致使出现两个连续红色节点的冲突,那么能够经过颜色调换(color flips)和树旋转来调整。)下面要进行什么操做取决于其余临近节点的颜色。其次每次插入以及插入修复后将根节点置为黑色。最后是空节点实现上处理为颜色为黑。这两条保证了性质123始终是成立的。同人类的家族树中同样,咱们将使用术语叔节点来指一个节点的父节点的兄弟节点。注意:spa

  • 性质123老是保持着。
  • 性质4只在增长红色节点、重绘黑色节点为红色,或作旋转时受到威胁。即:每一个红色节点必须有两个黑色的子节点。(从每一个叶子到根的全部路径上不能有两个连续的红色节点。)
  • 性质5只在增长黑色节点、重绘红色节点为黑色,或作旋转时受到威胁。即:从任一节点到其每一个叶子的全部简单路径都包含相同数目的黑色节点。

 

2. 插入后不须要修复的状况

  为了方便操做,TreeMap红黑树中还设置了以下几个辅助函数

//返回结点的颜色,空结点返回黑色
    private static boolean colorOf(Node p) {
        return (p == null ? BLACK : p.color);
    }
    //返回结点的父结点或者null
    private static Node parentOf(Node p) {
        return (p == null ? null: p.parent);
    }
    //设置结点的颜色
    private static void setColor(Node p, boolean c) {
        if (p != null)
            p.color = c;
    }
    //返回该结点的左节点
    private static Node leftOf(Node p) {
        return (p == null) ? null: p.left;
    }
    //返回该结点的右节点
    private static Node rightOf(Node p) {
        return (p == null) ? null: p.right;
    }

  首先讲下插入与插入修复,顾名思义插入修复是对插入后进行的一种修复,为何要进行修复,由于你的插入可能会形成上面提到的性质4和性质5被破坏。下面提到的重绘(不包括每次将结束将根节点设置为黑色)实际上至关于将结点的颜色进行了反转,先说下不会形成性质破坏的插入:

  1. 插入节点做为根节点
  2. 插入节点做为根节点的子节点
  3. 插入节点的父结点是黑色的

 
  首先咱们要明确一个事实,在咱们插入以前,红黑树是严格遵照上面5条性质的。性质13是不会被破坏的以上三种状况,
  其中1是根节点,咱们只须要将它在插入后重绘为黑色便可,更不会破坏性质4和性质5。
  对于2,既然可以在根节点进行插入也就是说在插入前空节点到根节点的黑色结点路径为0,那么插入后新结点的两个子节点(空节点)及其新结点自己到根节点黑色距离也是0(由于插入结点自己是红色),不会破坏性质5,性质4由于新插入结点的两个子节点(空节点都是黑色),因此也不会破坏。
  对于3,与2同理,不会破坏性质4,对于性质5,任何一个节点到新结点及其两个子节点的距离是相同的,由于新结点是红色,此外任何节点到新结点及其子节点的距离确定与该结点到新结点所替换的那个空节点的距离相同,因此不会破坏性质5。

3. 插入后须要修复的状况

  因此以上这三种状况的插入是被排除在插入后修复的。那么那些须要修补的插入状况是什么呢?首先要知足不是上面那几种状况之一,因此即必须有祖父节点(不为空)而且父结点是红色的,这是下面说的几种状况的前提条件(而且下面的状况是过滤式的,即if 、else if 、else的关系)
  下面图中的虚线段代表子节点可能为父结点的左节点或者父结点的右节点两种状况,绿色虚线圆圈包围着的结点表示在即将的修复操做后会被重绘的。
  一张图中的全部的黑色外圈数字结点(多是一颗子树,由于状况1可能会迭代修复)表示它们是内含有相同的黑色长度的黑色外圈指的是它的根节点必须是黑色的,否则就不知足性质4。内含有相同的黑色长度就是从它们的根节点到各自子树的空节点的路过的黑色结点个数相同(这是根据性质5推出的)。当它们其中之一是null即空节点,即内含长度是0,而它们的根节点自己黑色的,因此他们全部黑色外圈数字节点都是null即空节点(这就是插入新结点的第一次插入修复的时候)
  一张图中的全部红黑外圈数字节点(多是一颗子树,由于状况1可能会迭代修复)都是挂在叔结点下面的,表示它们内含长度必定是比黑色外圈数字结点少1(根据性质5推出),当黑色外圈数字已是null空节点时,由于内含长度不多是-1(根据性质5推出),这时表示的是叔结点为null即空节点(根据性质3空节点是黑色的,仍知足叔结点为黑色)(这就是插入新结点的第一次插入修复的时候)。**红黑外圈表示它们的根节点多是红色或者黑色的。

1. 叔结点为红色

clipboard.png
clipboard.png

  叔结点为红色的时候,则须要进行插入修复,如上图表示的四种状况所示,修复操做统一为:将叔、父重绘为黑色,祖绘为红色,这种修复并非一次性的,修复完毕须要继续进行从头开始的插入后修复判断(即从判断是否须要插入修复开始往下也要能又回到这种状况)。

2. 叔结点为黑色,而且子父祖三节点位于同侧斜线上

clipboard.png
clipboard.png

  叔结点为黑色而且子父祖在同侧斜线上时,修复操做为:将父绘黑,祖绘红,而后以祖为枢纽进行向相反侧的方向旋转,同在右侧斜线上的就往左旋转了,同在左侧斜线上的就往右旋转了。注意咱们这个时候绘黑的是即将成为这颗子树新的根节点的的父,绘红的是再也不是根节点的祖,此种状况是修复中止的,由于该子树的根节点仍是黑色没有发生变化,而字数内部又知足了性质三四五,而且没有破坏有序性。

3. 叔结点为黑色,而且子父祖三节点位于异侧斜线上

clipboard.png
clipboard.png

  当叔结点为黑色而且子父祖异侧的时候,状况如图所示,修复操做分为了两步:第一步是根据子父所在的那一侧的方向,以父为枢向相反方向旋转,这样就实现了将子父祖异侧转换为了父子祖同侧(而且是子父原所在侧的相反侧),第二步是发现它符合上一种状况了,那就将如今的父(原来的子)绘黑,祖(原来的祖)绘红,由于上种状况是修复中止的因此修复结束

4. 插入总结

  在上面三种修补状况当中后两种是不会改变所在子树的黑色路径长度的,只有第一种状况可能改变黑色路径长度,由于它可能子树根节点涂红,若是子树根节点就是整棵树的根节点时,在修补结束后会被从新设置成黑色,黑色路径长度就加1了,但这个时候黑色路径增长针对的再也不是这个子树了,而是整颗红黑树,因此是不会破坏性质5的至于性质4看修补后的图中结点的颜色就知道是不会破坏了。
  插入结束以后,这颗子树的知足了性质4(不能有两个连续的红色节点)和性质5(从任一节点到其每一个叶子的全部简单路径都包含相同数目的黑色节点),又是一颗新的红黑树了。

4、删除操做

参考文献:

红黑树深刻剖析及Java实现
看图轻松理解数据结构与算法系列(红黑树)
红黑树——维基百科

相关文章
相关标签/搜索