java数据结构之TreeMap

来源:互联网 发布:万方数据库和中国知网 编辑:程序博客网 时间:2024/06/04 20:07

官方解释

基于红黑树(Red-Black tree)的 NavigableMap 实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator进行排序,具体取决于使用的构造方法。注意,如果要正确实现 Map 接口,则有序映射所保持的顺序(无论是否明确提供了比较器)都必须与 equals 一致。也就是说c.compare(e1,e2) == 0 与 e1.equals(e2)具有相等的布尔值。
这是因为Map接口是按照 equals操作定义的,但有序映射使用它的 compareTo(或 compare)方法对所有键进行比较,因此从有序映射的观点来看,此方法认为相等的两个键就是相等的。即使排序与 equals 不一致,有序映射的行为仍然是 定义良好的,只不过没有遵守 Map 接口的常规协定。
解释一下Map接口与equals和compare方法的关系,其实很简单拿HashMap来说,大家都明白HashMap中不能放相同的key,这里的相同说的就是两个key通过equals方法返回true。如果两个key相同那么就会替换原来的key。这是Map接口的协定。HashMap也完美遵守了此协定。换做TreeMap来说如果要完美最受此协定,那么c.compare(e1,e2) == 0 与 e1.equals(e2)具有相等的布尔值。如果不遵守此协定即使两个key的equals相同,也会被放入TreeMap中。因为TreeMap通过compare方法来比较对象,这就会让人感觉有些怪异。

数据结构

红黑树是一种自平衡二叉查找树,一个节点的左节点小于本身,右节点大于本身。了解了红黑树也就了解了TreeMap的数据结构,R-B tree的性质如下:

  • 性质1. 节点是红色或黑色。
  • 性质2. 根节点是黑色。
  • 性质3. 每个叶节点(NIL节点,空节点)是黑色的。
  • 性质4. 每个红色节点的两个子节点都是黑色。
  • 性质5. 从任一节点到其下每个叶子节点的所有路径中都包含相同数目的黑色节点。

    需要说明的是,红黑树的性质以及算法会保证红色节点的子节点以及父节点都是黑色的。 也就是说R-B树中不可能出现两个相连的红节点。那么R-B tree的性质可概括为一句话就是:两头黑,红不连,黑相同。Nil表示叶子节点。时间复杂度为O(lgn),效率非常之高,所以应用十分广泛。数据结构如下:

这里写图片描述

省略去了叶子节点。每个节点的结构如下所示:
这里写图片描述

算法

在添加和删除数据后,红黑树将违反一些性质,那么我们需要重新对树进行修正。有两个基本的修正方法:左旋和右旋,两个方法互为逆运算。如下图所示:
这里写图片描述

代码如下:

//左旋private void rotateLeft(Entry<K,V> p) {        if (p != null) {            Entry<K,V> 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(Entry<K,V> p) {        if (p != null) {            Entry<K,V> 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;        }    }

左旋和右旋保证了红黑树的性质以及树的平衡。

插入操作

假设插入数据节点为z。那么插入的步骤如下:

  • 1:把z节点的颜色设置为RED.
  • 2:按照left小,right大的特点,把z节点插入到树中。
  • 3:修正R-B tree

为什么要把z节点设置成RED?为了之后修正更方便,因为设置为RED之后,每条路径上的黑色节点数量不会改变,也就不会破坏黑相同的性质,只用处理红子黑的性质,相对来说比设置为BLACK要更为方便。前两步比较简单,一目了然,重要的第三步,如何修正呢?我们一步一步来。
先定义几个字母的含义。
字母说明:z-表示当前节点 ;z.p-表示父节点 ;z.g-表示祖父节点 ;z.y-表示叔叔节点。
修正逻辑如下:

如果z节点的父节点是BLACK。

那么我们就不必修正,因为插入z后并没有违反任何一条性质。

如果z节点的父节点是RED

根据“红不连”性质,z.g必为BLACK。z是RED,z.p也是RED,这就违反了“红不连”的性质,需要我们后续分处理。因为z.p不是左节点就是右节点,先讨论左节点。

z.p是左节点

因为z和z.p都是红色,且z.p是左节点,我把这种情况成为“父左连”。还需要根据叔叔节点的颜色来进行判断(叔叔节点肯定是右节点了)。即是z.y为Nil,我们也当做z.y是存在的BLACK。那么z.y要不就是RED,要不就是BLACK。

如果z.y.color为RED

那么结构图有以下两种情况:
这里写图片描述
不管是那种情况,我们均可以使用以下方法,进行修正违反“红不连”的问题.就是把z.p,z.y,z.g的颜色全部翻转一下,这样就保证了每条路径上依旧有相同的黑色节点。伪代码如下:

z.p.color = BLACKz.y.color = BLACKz.g.color = RED

因为我们不知道z.g.p的颜色,如果是RED,那么就会出现z.g和z.g.p都是红色问题。所以还需要把当前节点设置为z.g,进行下一轮按断。

z = z.g

就变成了如下图所示:
这里写图片描述
总结为一句话就是:叔红反色

如果z.y.color为BLACK

那么结构图有以下两种情况,z是左节点或者右节点。
这里写图片描述

如果z是右节点,为了统一操作,z = z.p,然后先把z左旋,左旋之后就变成如下图所示:
这里写图片描述

此时z就变成了左节点。然后进行如下操作:

z.p.color = BLACKz.g.color = REDrightRotate(z.g)

就变成了如下所示:
这里写图片描述
总结为一句话就是:右叔黑,变左,反色,右旋

z.p是右节点

和z.p是左节点完全是相对应的,这里就不再赘述。
总结为一句话就是:左叔黑,变右,反色,左旋

我们利用,这三句话就可以完成插入操作。

插入修正代码如下所示:

private void fixAfterInsertion(Entry<K,V> x) {        x.color = RED;        while (x != null && x != root && x.parent.color == RED) {            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {                Entry<K,V> y = rightOf(parentOf(parentOf(x)));                if (colorOf(y) == RED) {                    setColor(parentOf(x), BLACK);                    setColor(y, BLACK);                    setColor(parentOf(parentOf(x)), RED);                    x = parentOf(parentOf(x));                } else {                    if (x == rightOf(parentOf(x))) {                        x = parentOf(x);                        rotateLeft(x);                    }                    setColor(parentOf(x), BLACK);                    setColor(parentOf(parentOf(x)), RED);                    rotateRight(parentOf(parentOf(x)));                }            } else {                Entry<K,V> y = leftOf(parentOf(parentOf(x)));                if (colorOf(y) == RED) {                    setColor(parentOf(x), BLACK);                    setColor(y, BLACK);                    setColor(parentOf(parentOf(x)), RED);                    x = parentOf(parentOf(x));                } else {                    if (x == leftOf(parentOf(x))) {                        x = parentOf(x);                        rotateRight(x);                    }                    setColor(parentOf(x), BLACK);                    setColor(parentOf(parentOf(x)), RED);                    rotateLeft(parentOf(parentOf(x)));                }            }        }        root.color = BLACK;    }

模拟插入操作

假设我们的节点是个数字,我们按照100-80-70-85-90-95-55-60-30-75的顺序插入节点。

插入100

先插入第一个红色节点100,为根节点,根节点必须为黑色,我们只需要把颜色变成黑色就可以,如下:
这里写图片描述

插入80

先设置为红色,插入后如图所示:
这里写图片描述
因为父节点的颜色是黑色,那么并没有违反任何红黑树的性质,所以我们不用处理。

插入70

先设置为红色,插入后如图所示:
这里写图片描述

这里就违反了“红不连”的性质。也就是说红色节点的孩子必须是黑色(或者说不能出现两个相连的红色节点)。根据右叔黑,变左,反色,右旋的操作进行修正,先把自己变成左节点,发现自己就是左节点,那么不用变左操作,接下来就是反色操作,反色是把父节点和祖父节点的颜色变成对立颜色。反色后如下图所示:
这里写图片描述
接下来进行右旋操作,是把祖父节点右旋,右旋后如下图所示:
这里写图片描述

插入85

先设置红色,根据左小,右大的性质,插入后如下图所示:

这里写图片描述
很显然违反了“红不连”的性质。因为叔叔是红色,那么我们直接进行叔红反色操作就可以,切记操作后仍要查看树的性质,进行反色操作,把父亲、叔叔和祖父全部反色,反色后,根节点变为了红色,那么我们需要把跟节点变为黑色。如下图所示:
这里写图片描述

插入90

设置为红色,然后插入,如下图所示:
这里写图片描述
还是根据右叔黑,变左,反色,右旋来操作,具体操作如下图所示:
这里写图片描述

插入95

设置为红色插入,如下图所示:
这里写图片描述

很显然,违反“红不连”性质。根据叔红反色来操作,如下图所示:
这里写图片描述

反色后,并不违反性质,搞定。

插入55

设置为红色插入,如下图所示:
这里写图片描述

父节点为黑色,不违反性质,不需要操作。

插入60

设置为红色插入,如下图所示:
这里写图片描述

违反了“红不连”性质。根据右叔黑,变左,反色,右旋来操作。具体步骤如下图所示:
这里写图片描述

插入30

设置为红色插入,如下图所示:
这里写图片描述

根据叔红反色来操作,如下图所示:
这里写图片描述
反色后,没有违反树的性质。

插入75

设置红色插入,如下图所示:
这里写图片描述
父亲节点为黑色,不违反性质,无需操作。

删除操作

将红黑树内的某一个节点删除。需要执行的操作依次是:首先,将红黑树当作一颗二叉查找树,将该节点从二叉查找树中删除;然后,通过”旋转和重新着色”等一系列来修正该树,使之重新成为一棵红黑树。

第一步

抛去颜色,将红黑树当作一颗二叉查找树,将节点删除。分为下面三种情况:

  • 1:被删除节点没有儿子,即为叶节点。那么,直接将该节点删除就OK了。
  • 2:被删除节点只有一个儿子。那么,直接删除该节点,并用该节点的唯一子节点顶替它的位置。
  • 3: 被删除节点有两个儿子。那么,先找出它的后继节点(我们从右边找后继节点);然后把“它的后继节点的内容”复制给“该节点的内容”;之后,删除“它的后继节点”。在这里,后继节点相当于替身,在将后继节点的内容复制给”被删除节点”之后,再将后继节点删除。这样就巧妙的将问题转换为”删除后继节点”的情况了,下面就考虑后继节点。 在”被删除节点”有两个非空子节点的情况下,它的后继节点不可能是双子非空。既然”的后继节点”不可能双子都非空,就意味着”该节点的后继节点”要么没有儿子,要么只有一个儿子。若没有儿子,则按”情况① “进行处理,若只有一个儿子,则按”情况② “进行处理。

这么说可能不太明了,我们用图来分析一下,有如下红黑树,抛去颜色如下图所示:

这里写图片描述

1:如果删除的是1节点:没有儿子,直接删除。如下图:
这里写图片描述
2:如果删除的是7节点:有一个儿子。那么用孩子替换它的位置。如下图:
这里写图片描述
3:如果删除的是5节点:两个孩子。先找后继节点7,用7节点替换5节点,然后删除7节点。
这里写图片描述

代码如下所示(可以先忽略代码,继续往下看):

private void deleteEntry(Entry<K,V> p) {        //如果被删除节点有两个儿,那么,先找出它的后继节点;然后把“它的后继节点的内容”复制给“该节点的内容”        if (p.left != null && p.right != null) {            Entry<K,V> s = successor(p);            p.key = s.key;            p.value = s.value;            p = s;        }        Entry<K,V> replacement = (p.left != null ? p.left : p.right);                //如果删除节点有孩子节点        if (replacement != null) {            replacement.parent = p.parent;            if (p.parent == null)                root = replacement;            else if (p == p.parent.left)                p.parent.left  = replacement;            else                p.parent.right = replacement;            //删除节点            p.left = p.right = p.parent = null;            //如果删除节点是黑色,那么修正树            if (p.color == BLACK)                fixAfterDeletion(replacement);        } else if (p.parent == null) { //如果删除的是根节点,那么直接删除            root = null;        } else { ////如果删除节点没有孩子节点                //如果删除节点是黑色,那么修正树            if (p.color == BLACK)                fixAfterDeletion(p);                        //删除该节点            if (p.parent != null) {                if (p == p.parent.left)                    p.parent.left = null;                else if (p == p.parent.right)                    p.parent.right = null;                p.parent = null;            }        }    }

第二步

通过”旋转和重新着色”等一系列来修正该树,使之重新成为一棵红黑树。因为”第一步”中删除节点之后,可能会违背红黑树的特性。所以需要通过”旋转和重新着色”来修正该树,使之重新成为一棵红黑树。
如果删除的节点是红色,那么不需要处理,删除后不会违反任何性质。如果删除的是一个黑色的节点,那么可能会违反红不连和黑相同的性质,需要修正。节点说明如下:

  • z节点:当前节点,也是真正删除节点的孩子节点,没有孩子节点的话就是要删除的节点。
  • z.p节点:z节点的父节点
  • z.g节点:z节点的祖父节点
  • z.y节点:z节点的叔叔节点
  • z.yl节点:z节点的叔叔节点的左孩子节点
  • z.yr节点:z节点的叔叔节点的右孩子节点

因为删除了一个黑色节点,那么z节点路径上就少了一个黑色节点,为了便于分析,我们在z节点上额外增加一个黑色,也就是说z节点现在有两个颜色,这样的话z节点路径上的黑色节点就不会少了。之后要解决的问题就是把z节点的颜色变成单一的颜色。这个额外的节点只是为了便于分析使用,是不会存在于代码中的。
需要说明的是,下面图中的白色节点表示任意一种颜色的节点,可以是红色的也可以是黑色的。像下面这样:
这里写图片描述
下面我们分情况来讨论,如何删除后修正。

情况1:如果z节点原本是红色

那么直接把z节点设置为黑色就可以。这样即解决了红不连的问题,也解决了黑相同的问题。
这种情况比较简单,就不上图了。

情况2:如果z节点原本是黑色,z.y,z.yl,z.yr全是黑色。

如下图:
这里写图片描述

图中z节点中额外的黑色,用一个包含在其中的黑色圆圈表示。
针对这种情况我们需要做的就是把额外的黑色上移。具体做法如下:

  • z.y变为红色。
  • 设置当前节点为z.p
  • 继续判断

因为额外黑色上移后,兄弟节点路径上就多出一个黑色,所以把z.y变成红色,这样就正好不多不少。
变换如下图所示:
这里写图片描述
总结为一句话就是:“兄全黑,黑上移,兄变红”

情况3:如果z节点原本是黑色,z.y是黑色,但z.yr是红色,z.yl是任意颜色。

如下图所示:
这里写图片描述
图中白色节点,表示任意颜色。
这一情况的根本就是要消除,z节点上的双色,使其变成单一的颜色。具体做法如下:

  • 将z.p的颜色赋值给z.y。
  • 将z.p设为“黑色”。
  • 将z.yr设为“黑色”。
  • 对z.p左旋。
  • 结束

这个过程中,我们是交换了z.p和z.y的颜色,以及设置z.yr为黑色,这样一来,z.p,z.y,z.yr这条路径上就多出一个黑色。然后通过左旋,使得z.p,z.y,z.yr路径上的黑色减少一个,单着又使得z.p,z的路径上多出一个黑色来,那么直接把z上多余的黑色去掉就可以了。
变换如下图所示:
这里写图片描述

总结为一句话就是:“兄右红,兄右黑,父兄换,父左旋”
此种情况的修正,消除了z节点的双色,对于下面出现的情况都可以转化成这种情况,然后进行修正。

情况4:如果z节点原本是黑色,z.y是黑色,但z.yr是黑色,z.yl是红色。

如下图:
这里写图片描述

我们需要把这种情况,变成情况3.然后按照情况3进行修正。具体步骤如下:

  • 将z.yl设为“黑色”。
  • 将z.y设为“红色”。
  • 对z.y进行右旋。
  • 继续判断。

要变成情况3,我们需要交换z.y和z.yl的颜色,交换后,z.yr的路径上会少一个黑色,对z.y进行右旋后,可修正此问题。变换如下图所示:
这里写图片描述

总结一句话:“兄左红,兄左换,兄右旋”

情况5:如果z节点原本是黑色,z.y是红色。

如下图:
这里写图片描述

这种情况,也需要转化成2,3,4情况,然后进行修正。具体如下:

  • 将z.y设为“黑色”。
  • 将z.p设为“红色”。
  • 对z.p进行左旋。
  • 继续判断

这个过程,交换了z.y和z.p的颜色,交换后z节点路径上就少了一个黑色,然后对z.p进行左旋来解决这个问题。变换如下所示:

这里写图片描述

总结一句话:“兄红,父兄换,父左旋”

到这里我们就介绍完了,但是我们只介绍了z节点是左节点的情况,如果z节点是右节点那么和左节点完全是一样的操作,就不在赘述了。

代码如下所示:

 private void fixAfterDeletion(Entry<K,V> x) {        while (x != root && colorOf(x) == BLACK) {            if (x == leftOf(parentOf(x))) {                Entry<K,V> sib = rightOf(parentOf(x));                if (colorOf(sib) == RED) {                    setColor(sib, BLACK);                    setColor(parentOf(x), RED);                    rotateLeft(parentOf(x));                    sib = rightOf(parentOf(x));                }                if (colorOf(leftOf(sib))  == BLACK &&                    colorOf(rightOf(sib)) == BLACK) {                    setColor(sib, RED);                    x = parentOf(x);                } else {                    if (colorOf(rightOf(sib)) == BLACK) {                        setColor(leftOf(sib), BLACK);                        setColor(sib, RED);                        rotateRight(sib);                        sib = rightOf(parentOf(x));                    }                    setColor(sib, colorOf(parentOf(x)));                    setColor(parentOf(x), BLACK);                    setColor(rightOf(sib), BLACK);                    rotateLeft(parentOf(x));                    x = root;                }            } else { // symmetric                Entry<K,V> sib = leftOf(parentOf(x));                if (colorOf(sib) == RED) {                    setColor(sib, BLACK);                    setColor(parentOf(x), RED);                    rotateRight(parentOf(x));                    sib = leftOf(parentOf(x));                }                if (colorOf(rightOf(sib)) == BLACK &&                    colorOf(leftOf(sib)) == BLACK) {                    setColor(sib, RED);                    x = parentOf(x);                } else {                    if (colorOf(leftOf(sib)) == BLACK) {                        setColor(rightOf(sib), BLACK);                        setColor(sib, RED);                        rotateLeft(sib);                        sib = leftOf(parentOf(x));                    }                    setColor(sib, colorOf(parentOf(x)));                    setColor(parentOf(x), BLACK);                    setColor(leftOf(sib), BLACK);                    rotateRight(parentOf(x));                    x = root;                }            }        }        setColor(x, BLACK);    }

模拟删除

我们使用上面,模拟插入的红黑树,进行删除操作,如图:
这里写图片描述

我们依次删除节点95-70-60-80-55-30-90-85-75-100。

删除95

这里写图片描述

95节点没有孩子节点,且是红色,所以直接删除,无需另外操作。

删除70

这里写图片描述

70节点有一个孩子节点,用子节点替换70节点的位置,加一个多余的黑色(z节点)。因为z节点本身就是红色,那么直接变为黑色就可以了。如下操作:
这里写图片描述

删除60

60节点有两个孩子节点,那么需要寻找其后继节点,我们向右找到后继节点为75。然后替换60的内容为75,原75节点没有孩子节点,那么就变为z节点,修正之后再删除。如下所示:
这里写图片描述

上面我们介绍过一种情况:如果z节点原本是黑色,z.y是黑色,但z.yr是红色,z.yl是任意颜色。那么口令是“兄右红,兄右黑,父兄换,父左旋”,这时z是左节点的情况。而现在的情况是与之对应的z是右节点的情况,那么对应的口令就是兄左红,兄左黑,父兄换,父右旋,这口令不用记直接推理就可以出来,变换如下:
这里写图片描述
这里写图片描述

删除80

80节点有两个孩子节点,那么需要寻找其后继节点,我们向右找到后继节点为85。替换内容后,85节点变为z节点,修正后再删除,如下图:
这里写图片描述

我们看到z节点的兄弟节点,以及兄弟节点的孩子全黑(没有孩子则视孩子为黑色)。那么使用口令:兄全黑,黑上移,兄变红,如下图所示:
这里写图片描述

变换之后,发现90节点本身为红色,那么直接变为黑色,删除z节点,就可以了,如下:

这里写图片描述

删除55

55有两个孩子节点,寻找后继节点,找到75,替换内容,75变z节点,修正后删除。如下:
这里写图片描述

我们看到z节点的兄弟节点,以及兄弟节点的孩子全黑(没有孩子则视孩子为黑色)。那么使用口令:兄全黑,黑上移,兄变红,然后删除z节点。如下图所示:
这里写图片描述

发现75节点为红色节点,直接变黑,如下图:
这里写图片描述

删除30

30节点没有孩子节点,且为红色,那么直接删除,无需修正,如下:
这里写图片描述

90

90有一个孩子节点,用孩子节点替换自己,孩子变为z,然后删除z节点。如下:
这里写图片描述

删除85

85有两个孩子,找后继节点,找到100,替换内容后100节点变为z,修正后删除,如下:
这里写图片描述

兄全黑,黑上移,兄变红,如下:
这里写图片描述

变换后z节点为100,位跟节点,直接设置为黑色,如下:
这里写图片描述

删除75

75为红色,没有孩子节点,直接删除,如下:
这里写图片描述

删除100

100为根节点,直接设置根节点为Nil,如下所示:
这里写图片描述

搞定,全部删除完成。

到这里红黑树的插入和删除操作就完成了,TreeMap的结构也就完全清楚了。