从2-3-4树模型到红黑树实现

从2-3-4树模型到红黑树实现

前言

红黑树,是一个高效的二叉查找树。其定义特性保证了树的路径长度在黑色节点上完美平衡,使得其查找效率接近于完美平衡的二叉树。java

可是红黑树的实现逻辑很复杂,各类旋转,颜色变化,直接针对其分析,大多数都是死记硬背各类例子,不太容易有个直观的理解。实际上,红黑树是实现手段,是其余概念模型为了方便在二叉树上实现进而定义的节点颜色这个信息。若是从概念模型入手,再一一对应,就容易理解的多了。而红黑树可以对应的模型有2-3树,2-3-4树等,下面咱们会以2-3-4树做为概念模型,对红黑树进行分析。node

2-3-4树

2-3-4树是对完美平衡二叉树的扩展,其定义为:安全

  • 在一个节点中,能够存在1-3个key
  • 2-节点,拥有1个key和2个子节点。
  • 3-节点,拥有2个key和3个子节点。
  • 4-节点,拥有3个key和4个子节点。
  • 子节点为空的节点称为叶子节点。
  • 任意从根节点到叶子节点的路径拥有相同的长度,即路径上的连接数相同。

下图就是一个2-3-4树:数据结构

查找

2-3-4树的查找很简单,相似于二叉树,步骤以下:less

  • 将查找key和节点内的key逐一对比。
  • 若是命中,则返回节点内key的对应值。
  • 若是节点内的key都不命中,则沿着合适的连接到下一节点重复该过程直到找到或者无后续节点。

举个例子,若是咱们要在上面的2-3-4树中查询11,其步骤以下:this

插入

2-3-4树的插入,不会发生在中间节点,只会在叶子节点上进行插入。编码

code

在叶子节点上新增key,会使得2-节点变为3-节点,3-节点变为4-节点。而本来的4-节点就没有空间能够插入key了。为了解决这个问题,能够将4-节点中间的key推送给其父节点,剩下的2个key造成2个2-节点。效果以下blog

经过将4-的叶子节点拆分,产生了新的叶子节点可供key插入,同时将中间key送入父节点。该操做不会破坏树的平衡性和高度。但若是叶子节点的父节点也是4-节点,这个操做就没法进行了。为了解决这个问题,有两种思路:递归

  • 自底向上,从4-叶子节点开始分裂,若是分类后其父节点也是4-节点,继续向上分裂,直到到达根节点。若是根节点也是4-节点,分裂后树的高度+1。
  • 自顶向下,从根节点到插入所在的叶子节点路径上,遇到4-节点就将其分裂。

两种方法都能解决问题,不过自顶向下不须要递归,实现起来更简单。经过这种处理方式,确保了1)最后到达的叶子节点必然是2-或者3-节点,搜索路径上不存在4-节点。

树的生长

2-3-4树是向上生长的。这句话能够从根节点的分裂理解:若是根节点是一个4-节点,当新增key时,根节点会分裂,将中间的key推入父节点。根节点没有父节点,所以中间的key就会成为新的根节点。以下所示:

整颗树的生长能够当作是叶子节点不断的新增key,而且在成为4-节点后被下一次的新增动做分解为2个2-节点,同时将一个key送入父节点。随着这个过程的不断进行,不断有key从叶子节点向根节点汇聚,直到根节点成为4-节点并在下一次新增时被分类,进而让树升高1。

删除

删除是整个操做中最为复杂的部分,由于删除可能发生在任意节点上,而且删除后可能破坏2-3-4树的完美平衡。在这里,咱们先来处理一些简单的状况,最后再思考能够推而广之的策略。

删除最大key

在2-3-4树中,删除最大key必然是最右边的叶子节点上。若是叶子节点是3-节点或者4-节点,只须要将其中最大的key删除便可,不会对树的平衡性形成影响。但若是删除的key在2-节点上,状况就变得麻烦,由于删除2-节点,致使树的平衡被破坏。为了不这个状况的发生,不能让删除发生在2-节点上。

为了让删除不落在2-节点上,能够将2-类型的叶子节点(最终要删除的那个),从其兄弟节点“借”一个key进行融合变成3-节点;也能够将父节点的key和兄弟节点的key融合,变成一个4-节点,主要保证变化过程当中树的平衡性不被破坏便可。变换完成以后的节点类型是3-或4-,天然就能够成功删除了。变化的可能状况有:

变化的策略是:

  1. 将父节点的key,自身的key,兄弟节点的key的合并后造成一个逻辑节点。
  2. 变化一:新节点为4-节点的状况下,父节点还有key,则新节点替换目标节点;
  3. 变化二:新节点为5-节点的状况下,最小key还给兄弟节点,次小key还给父节点,剩余2个key设置到目标节点。
  4. 变化三:新节点为6-节点的状况下,最小key还给兄弟节点,次小key还给父节点,剩余3个key设置到目标节点。

向下的搜索,最终达到须要删除key的叶子节点。叶子节点的兄弟节点没法控制,而若是能保证目标key所在的叶子节点的父节点不是2-节点,就能够安全删除key而不会破坏树的结构。所以,在自顶向下的过程当中,非根节点若是为2-节点,则经过变化成为非2-节点。这个转化,仅仅针对搜索路径的下一个节点而言,所以可能出现节点1被转化为非2-节点后,其子节点是2-节点,子节点转化为非2-节点时将父节点(节点1)恢复成2-节点。转化的最终目的是为了保证叶子节点的父节点是非2-节点便可,只不过为了达成这个保证,整个转化行为须要从根节点一直进行下去。所以若是在叶子节点的时候执行转化可能会致使子树高度减1,这种变化会影响到全局树的平衡。就须要循环向上迭代到根节点,比较复杂。而从根节点开始一路转化下去,则容易理解和实现,也不会影响树的平衡。

经过执行这种变化,在叶子节点中,就能够安全删除key

删除最小key

最小key的删除思路和操做方式和删除最大key类似,只不过搜索路径的方向是最左而已,其节点变化策略也是类似的,具体的变化有如下几种:

变化的策略是:

  1. 将父节点的key,自身的key,兄弟节点的key的合并后造成一个逻辑节点。
  2. 变化一:新节点为4-节点的状况下,父节点还有key,则新节点替换目标节点;
  3. 变化二:新节点为5-节点的状况下,最大key还给兄弟节点,次大key还给父节点,剩余2个key设置到目标节点。
  4. 变化三:新节点为6-节点的状况下,最大key还给兄弟节点,次大key还给父节点,剩余3个key设置到目标节点。

删除任意key

删除任一key就变得比较麻烦,key可能出如今中间节点上,删除的话,树的结构就被破坏了。这里,咱们能够采用一个取巧的思路:若是删除的key是树的中间节点,将该key替换为其中序遍历的后继key;该后继key是删除key的节点的右子树的最小key

key的替换对树无影响;而将替换key删除,则转换为删除对应子树最小Key的问。删除最小Key,须要从根节点自顶向下变化2-节点才能保证叶子节点中key的成功删除。所以,删除任一Key的具体处理思路能够总结为:

  1. 从根节点开始自顶向下搜索,非根节点若是为2-节点,则经过变化成为非2-节点。
  2. 搜索发现目标key,将其替换为中序搜索后继key。
  3. 删除步骤2节点的右子树最小key。

左倾红黑树

2-3-4树是一种概念模型,直接按照这个概念模型用代码实现则比较复杂,主要的复杂有:

  • 维持3种节点类型。
  • 多种节点类型之间须要互相转换。
  • 在树中移动须要进行屡次比较,若是节点不是2-节点的话。

所以在表现形式上,咱们将2-3-4树换另一种形式来展示,进行如下变换:

  • 将2-3-4树用二叉树的形式表现。
  • 节点之间的连接区分为红色和黑色。红色连接用于将节点连接起来视做3-节点和4-节点。

这种转换的关键点在于:

  • 转换后的二叉树可使用二叉树的搜索方式。
  • 转换后的二叉树和2-3-4树处于一致关系,改变的只是表现形式。

不过因为3-节点两种表现形式,增大了复杂性,所以对变换要求增长一条:红色连接只能为左链接。经过三个约束后,转换获得二叉树咱们称之为左倾斜红黑树,其关键特性有:

  • 可使用二叉树搜索方式。
  • 与2-3-4树保持一一对应。
  • 红黑树是黑色连接完美平衡的,也就是从根节点到叶子节点的任意路径上,黑色连接的数量一致。

其对应方式以下:

能够看到,若是将红色连接放平,就和2-3-4树在展示上一致了。2-3-4树是完美平衡的,其对应的左倾斜红黑树是黑色连接完美平衡,由于红色连接是用于3-节点和4-节点的;而黑色连接就对应2-3-4树中的连接。

左倾斜红黑树的转换中不容许2种形式:

  • 右倾斜的红色连接。
  • 两个连续的红连接在一个搜索路径中(从根到叶子节点的路径)

形象的说如下几种不容许:

禁止的状况,减小了须要考虑的情形,为后续的编码实现下降了难度。对于上述定义的左倾斜红黑树,使用数据结构来表达的话,在本来的二叉树的节点中,增长一个属性color,用于表示指向该节点的连接的颜色。

public class RedBlackTree
{
    private static final boolean RED   = true;
    private static final boolean BLACK = false;

    private       Node root;            // root of the BST
    private       int  heightBLACK;      // black height of tree

    private class Node
    {
        `key`   `key`;                  // `key`
        Value value;              // associated data
        Node  left, right;         // left and right subtrees
        boolean color;            // color of parent link
        private int    N;            // number of nodes in tree rooted here
        private int    height;       // height of tree rooted here

        Node(`key` `key`, Value value)
        {
            this.`key` = `key`;
            this.value = value;
            this.color = RED;
            this.N = 1;
            this.height = 1;
        }
    }
}

查找

红黑树的查找和二叉树的一致,可是会更加快速,由于红黑树是黑色平衡的,搜索长度获得了控制。

插入

在介绍插入实现以前,首先要介绍红黑树中的两种旋转操做:

  • 右旋:将一个左倾斜的红连接转化为右连接。
  • 左旋:将一个右倾斜的红连接转化为左链接。

这两个操做的重要性在于其变化是局部的,不影响黑色链接的平衡性。其变化以下:

红黑树的插入和二叉树插入是相同的,只不过新增的连接是红色的。由于红黑树的概念模型是2-3-4树,2-3-4树新增节点是在叶子节点上,而且新增后必然成为3-节点或者4-节点,因此新增连接均为红色。

在新增完毕后,根据红连接具体状况,进行旋转处理,以保持左倾斜红黑树的要求。可能出现的状况有:

  • 在2-节点上新增key,表如今红黑树上,就是一个黑色的节点新增左链接或者右链接
  • 在3-节点上新增key,表如今红黑树上,就是被红连接相连的两个节点上3个可新增链接的地方新增红连接。

2-节点的状况以下所示:

3-节点的状况以下所示:

左旋和右旋的方法以下:

private Node rotateLeft(Node h)//将节点的右红连接转化为左连接,而且返回本来节点的子树转化后新的子树的根节点
    {
        Node x = h.right;
        h.right = x.left;
        x.left = setN(h);
        x.color = x.left.color;
        x.left.color = RED;
        return setN(x);//该方法用于计算节点x的子树内的节点数量以及高度
    }

    private Node rotateRight(Node h)//将节点的左红连接转化为右连接,而且返回本来节点的子树转化后新的子树的根节点
    {
        Node x = h.left;
        h.left = x.right;
        x.right = setN(h);
        x.color = x.right.color;
        x.right.color = RED;
        return setN(x);
    }

在2-3-4树的节点插入中,为了不叶子节点是4-节点致使没有空间插入,因此从根节点到叶子节点的搜索路径中,采用自顶向下的4-节点分解策略。而在红黑树中,对4-节点的分解动做是经过对节点的颜色变化完成的,以下图所示:

翻转的过程很简单,就是将节点,节点的左右孩子节点的颜色都进行调整便可,颜色翻转的代码以下

private void colorFlip(Node h)
    {
        h.color = !h.color;
        h.left.color = !h.left.color;
        h.right.color = !h.right.color;
    }

4-节点的分解带来的效果是将红色连接向上层移动,这个移动可能产生一个红色的右连接,此时须要经过左旋来修正;或者产生2个连续的红连接,此时须要将其右旋,造成一个符合定义的4-节点。

总结来讲,插入的过程首先是自顶向下,遇到4-节点就进行分解,直到到达叶子节点插入新的key;因为向下过程的4-节点可能产生右倾斜的红连接,或者连续的2个红连接,所以须要从叶子节点处向上到达根节点,修复产生的这些问题。处理方式主要是:

  • 右倾斜的红连接左旋。
  • 连续的红连接,经过右旋来达到符合定义的4-节点。

按照上述的总结,咱们能够将新增节点的方法实现为

private Node insert(Node h, `key` `key`, Value value)//将KV对插入以h节点为根节点的树,而且返回插入后该树的根节点
    {
        if (h == null)//寻找到空白连接,返回新的节点。该节点为红色连接指向的节点。
        {
            return new Node(`key`, value);
        }
        if (isRed(h.left) && isRed(h.right))//自顶向下的过程当中,分裂4-节点。
        {
            colorFlip(h);
        }
        if (eq(`key`, h.`key`))
        {
            h.value = value;
        }
        else if (less(`key`, h.`key`))
        {
            h.left = insert(h.left, `key`, value);
        }
        else
        {
            h.right = insert(h.right, `key`, value);
        }
        if (isRed(h.right))//右倾斜的红色连接,进行左旋。
        {
            h = rotateLeft(h);
        }
        if (isRed(h.left) && isRed(h.left.left))//连续的红色连接,右旋变化为符合定义的4-节点
        {
            h = rotateRight(h);
        }
        return setN(h);
    }

删除

和概念模型相同的方法,咱们首先尝试实现删除最大key和删除最小key,以后经过替换key位置来实现删除任意Key功能。

删除最大Key

和概念模型相同,删除要发生在非2-节点上才能保证树的平衡不被破坏。这就意味着删除必定要发生在一个被红色连接相连的节点上。概念模型当中,在自顶向下搜索过程须要保证中间节点不是2-节点来使得叶子节点必然能够转化为非2-节点进行安全删除;反应在红黑树中,搜索路径的下一个节点,必需要被红色连接相连。若是不是的话,则要进行变化,具体的手段包括:

  • **当前节点有左倾斜红色连接时,将其进行右旋。**右旋能够从概念模型上理解,能够认为搜索路径是进行到3-节点或4-节点,而且从小key搜索到大key
  • **搜索路径的下一节点为2-节点,转化为非-2节点。**这个转化过程,参考概念模型中的作法,将当前节点的key,右子节点的key,左子节点的key先合并,产生红连接相连的逻辑节点。以后按照概念模型的拆分方式进行拆分。

针对步骤二,咱们作下具体的分析。

当前节点不是2-节点,且3-节点的红色左链接被转化为右连接,所以在下一个节点为2-节点的状况下,当前节点必然是被右倾斜红连接指向。所示初始状态可能以下:

对于状况一,咱们只须要对节点20进行颜色翻转,就可让其后继节点变为红色,也就是连接变红,便可。这种转换对应概念模型中的变化1。

对于状况二,比较复杂。首先咱们须要对节点20进行颜色翻转。此时节点10和20在一行路径上,对节点20的左链接右旋,右旋以后节点10变为新的根节点,对齐进行颜色翻转。整个过程以下

这种转换对应概念模型中的变化2。

状况三和状况二能够采用彻底相同的变化步骤,转换方式对应概念模型中的变化3。以下图所示:

综合状况1、2、三,咱们能够将变换的代码撰写为

private Node moveRedRight(Node h)
    {
        colorFlip(h);//先翻转
        if (isRed(h.left.left))//此时为状况二或者三
        {
            h = rotateRight(h);
            colorFlip(h);
        }
        return h;
    }

结合红连接右旋和转换2-节点,咱们能够将删除最大key的代码编写以下:

public void deleteMax()
    {
        root = deleteMax(root);
        root.color = BLACK;//若是根节点右子节点为2-节点,翻转root节点会致使其颜色变红,不符合定义。所以删除完成后,将颜色恢复为颜色。
    }
private Node deleteMax(Node h)//删除h节点为根节点的子树中的最大节点,而且返回删除后的子树的根节点
    {
        if (isRed(h.left))
        {
            h = rotateRight(h);
        }
        if (h.right == null)//没有右子树了,删除该节点
        {
            return null;
        }
        if (!isRed(h.right) && !isRed(h.right.left))//右子节点为2-节点,进行变化过程。
        {
            h = moveRedRight(h);
        }
        h.right = deleteMax(h.right);
        return fixUp(h);
    }
    private Node fixUp(Node h) //修正可能存在的异常连接状况。
    {
        if (isRed(h.right))
        {
            h = rotateLeft(h);
        }
        if (isRed(h.left) && isRed(h.left.left))
        {
            h = rotateRight(h);
        }
        if (isRed(h.left) && isRed(h.right))
        {
            colorFlip(h);
        }
        return setN(h);
    }

这个实现中引入了一个以前不曾提到的方法fixUp。由于在删除的过程自顶向下的变换会产生一些不符合定义的连接状况:好比右倾斜的红连接,好比连续的红连接。在删除完毕后,须要沿着以前的搜索路径,自底向上,进行异常连接修复。

删除最小Key

删除最小Key的思路和删除最大Key的思路很是接近,只不过在于搜索的方向不一样,是沿着左子节点一直向下搜索。相比于删除最大Key,删除最小Key在搜索路径向下的过程当中不须要对红连接方向进行旋转,当搜索路径的下一节点存在2-节点时转化为非2-节点。可能存在的初始状况以下图:

状况一很简单,只须要对节点20进行颜色翻转。该变换对应概念树中的变化1。

对于状况二,先对节点20进行翻转,再对节点30的左链接右旋,再对节点20的右连接左旋,最后对顶点进行翻转。流程以下图所示:

该变换对应概念模型中的变化2。

对于状况三则更复杂一些,其对应概念模型中的变化3,流程以下

和删除最大key不一样的地方在于状况三没法复用状况2的操做,不然会产生一个右倾斜的红连接。不过即便是右倾斜红连接,仍然是黑色平衡。可是与左倾斜红黑树定义不吻合,因此状况三使用了更多的步骤来产生符合定义的红黑树。

结合上述过程,咱们能够将删除最小Key的代码编写以下

public void deleteMin()
    {
        root = deleteMin(root);
        root.color = BLACK;
    }

    private Node deleteMin(Node h)
    {
        if (h.left == null)
        {
            return null;
        }
        if (!isRed(h.left) && !isRed(h.left.left))
        {
            h = moveRedLeft(h);
        }
        h.left = deleteMin(h.left);
        return fixUp(h);
    }
private Node moveRedLeft(Node h)
    {
        colorFlip(h);
        if (isRed(h.right.left))
        {
            if (isRed(h.right.right))
            {
                h.right = rotateRight(h.right);
                h = rotateLeft(h);
                h = rotateLeft(h);
                h.left = rotateRight(h.left);
                colorFlip(h);
            }
            else
            {
                h.right = rotateRight(h.right);
                h = rotateLeft(h);
                colorFlip(h);
            }

        }
        return h;
    }

删除任意Key

和概念模型的删除操做类似,自顶向下搜索,按照key的比较结果,可能向左右任意方向前进,若是下一步是一个2-节点,则参照删除最大最小key中的变化方式进行变化。在肯定key所在的节点后,将该key值替换为中序遍历的后继节点,继而删除该节点右子树的最小节点。代码以下

public void delete(Key key)
    {
        root = delete(root, key);
        root.color = BLACK;
}
 private Node delete(Node h, Key key)
    {
        if (less(key, h.key))
        {
            if (!isRed(h.left) && !isRed(h.left.left))
            {
                h = moveRedLeft(h);
            }
            h.left = delete(h.left, key);
        }
        else
        {
            if (isRed(h.left))
            {
                h = rotateRight(h);
            }
            if (eq(key, h.key) && (h.right == null))
            {
                return null;
            }
            if (!isRed(h.right) && !isRed(h.right.left))
            {
                h = moveRedRight(h);
            }
            if (eq(key, h.key))
            {
                h.value = get(h.right, min(h.right));
                h.key = min(h.right);
                h.right = deleteMin(h.right);
            }
            else
            {
                h.right = delete(h.right, key);
            }
        }
        return fixUp(h);
    }

总结

红黑树做为概念树的实际实现,其代码很复杂,要求的变化方式也不少。从概念树的映射来讲,2-3树,2-3-4树均可以映射到红黑树上。而左倾斜红黑树,使用递归方式实现的代码,无疑是很好理解,代码量也较少的。而JDK中TreeMap采用概念模型也是2-3-4树,不过并不限制右倾斜。整体而言,红黑树有太多变种,了解其原理最为重要,实现上,能使用便可,深究的话,意义不大。

参考文献

《Left-Leaning Red-Black Trees》(Princeton University)


文章原创首发于公众号:林斌说Java,转载请注明来源,谢谢。 更多高质量原创文章,欢迎扫码关注

相关文章
相关标签/搜索