Java之集合(十二)TreeMap

时间:2022-02-11 09:03:08

  转载请注明源出处:http://www.cnblogs.com/lighten/p/7411935.html

1.前言

  本章介绍Map体系中的TreeMap,顾名思义,这个是一个树结构的Map。TreeMap是一个具有比较器的Map,其是由比较器来决定get和put操作的,没有比较器的时候就使用compareTo方法进行比较。其实现是我在HashMap中所讲到的红黑树,但是实现方法却与HashMap的实现有些许区别,所以这里再进行描述一次,如果hashMap那章没看明白,可以再了解一下。

  TreeMap对于put,remove和get方法,保证其时间开销是log(n)。这个类也是一个线程非安全的类。迭代器也同样是fail-fast类型。

2.NavigableMap

  NavigableMap是一个接口,其继承自SortedMap。

    Java之集合(十二)TreeMapJava之集合(十二)TreeMap

  上面是这两个接口的所有方法定义。下面简单介绍一下这些方法的含义。

    SortedMap(最后三个方法不描述):

  (1)comparator():返回sortedMap的比较器,如果为null,意味着使用默认的Comparable顺序

  (2)subMap(K,K):返回map的一部分,范围是[K,K)。key的大小范围。

  (3)headMap(K):返回比所给key小的map部分

  (4)tailMap(K):返回比所给key大或等于的map部分

  (5)firstKey():返回当前map中最小的key

  (6)lastKey():返回当前map中最大的key

    NavigableMap:

  (1)lowerEntry(K):返回比所给key小且是其中最大的一个键值对

  (2)lowerKey(K):返回比所给key小且是其中最大的一个键

  (3)floorEntry(K):返回小于或等于所给key,且是其中最大的一个键值对

  (4)floorKey(K):返回小于或等于所给key,且是其中最大的一个键

  (5)ceilingEntry(K):返回大于或等于所给key,且是其中最小的一个键值对

  (6)ceilingKey(K):返回大于或等于所给key,且是其中最小的一个键

  (7)higherEntry(K):返回比所给key大且是其中最小的一个键值对

  (8)higherKey(K):返回比所给key大且是其中最小的一个键

  (9)firstEntry():返回当前map中key最小的键值对

  (10)lastEntry():返回当前map中key最大的键值对

  (11)pollFirstEntry():返回并移除当前map中key最小的键值对

  (12)pollLastEntry():返回并移除当前map中key最大的键值对

    后面的基本看名称也知道其作用,不再进行介绍。

3.TreeMap

Java之集合(十二)TreeMap

  TreeMap的结构是红黑树,十分的简单,就一个根节点和比较器。剩下的都是通用的size和modCount属性。下面介绍了一下get、put和remove这三个最基本的方法实现,其余的就略讲。

Java之集合(十二)TreeMap

Java之集合(十二)TreeMap

  get方法十分清楚,有比较器的时候使用比较器的相关方法(实际和非比较器的没什么区别,就比较那里使用了比较器比较而已)。从根节点开始,进行比较,小于的走左边,大于的走右边,等于的直接返回。这里就能看出,TreeMap与hashMap不同,其把比较结果相同的看做是同一个键,这个地方要小心了。树的结构也很清楚了:左节点<父节点<右节点。下面看刚刚说的“坑处”:

    @Test
public void testTreeMap() {
TreeMap<Integer, String> tree = new TreeMap<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return 0;
}
});
tree.put(1, "1");
tree.put(2, "2");
System.out.println("size:"+tree.size()+",1:"+tree.get(1));
}

  上面输出来的就是:size:1,1:2。1和2被看成了同一个键,使用TreeMap的时候这点要注意,否则会出现不想要的结果。

  put方法实现过长,这里就不进行截图。实现也比较简单,简述一下具体过程:1.根节点为null,检查放入根节点。2.比较器不为null,使用比较器循环比较,找到插入的父节点,记录是放在左边还是右边,如果相等就更新值,直接返回。3.如果没返回,意味着是新值,根据应插入的父节点和比较结果,放入具体的左边还是右边。4.这个是关键的步骤,为了保持红黑树的平衡,需要调整树。5.修改size和modCount。

  上面的关键就是第四步,保持树的平衡。此方法实现和HashMap的实现相比有小区别,但是思路看过去更清晰。

Java之集合(十二)TreeMap

  每一个插入的结点都被看做是红链接,调整完成之后根节点必须是非红链接。这里还是给出HashMap中所给的红黑树基础概念的参考文章:这里。关键是2-3树的理解,就能更好的理解这段代码了。每一个插入节点都被看成是红链接,如果其父节点也是红链接,就认为该节点可能需要进行调整。为什么这样需要进行调整呢?看2-3树插入分裂的过程:

  1.当前插入结点是2节点,将其变成一个3节点,3节点就是包含2个元素的结点;

  2.当前插入结点是3节点,将其变成一个4节点,4节点是包含3个元素的结点,这样就可以很容易分裂成左-父-右节点;

    分裂之后会产生父节点归属的问题,所以会出现3种情况:

    (1)插入的3节点没有父节点,则分裂出来的父节点为根节点

    (2)插入的3节点父节点是2节点,则分裂出来的父节点并入其中变成3节点

    (3)插入的3节点父节点是3节点,则分裂出来的父节点变成4节点,循环第2步再往上进行分裂

  2-3树这种分裂方式会自动平衡选择出根节点。红黑树将所有节点都看做是2节点,并且先当成是一个红链接。红链接的含义就是这是一个左链接(暂不清楚为什么是必须是左链接),并且与其父节点构成了3节点。所以根据2-3树的定义,红链接是不可能出现连续出现2个的,当前结点与父节点是一个红链接,父节点与父父节点是一个红链接,这样就构成了一个包含3个元素的4节点,四节点根据上面所说就是需要分裂的。以上就是对判断调整条件的说明。根节点一定不是红链接,其没有父节点进行合并。

  由于红黑树中每个插入节点是被看成是插入左边并且与父节点构成了3节点,实际情况可能不是这样。这就是第一个if判断的情况了:1.其父节点真的是父父节点的左边;2.其父节点不是父父节点的左边。

  先看第一种情况

Java之集合(十二)TreeMap

  第一种情况不确定的因素还是有很多:X是否是真的红链接,即在XP的左边,另外就是XPP右边的情况。所以if块里面的代码就是为了明确这些情况来进行判断是否需要进行相应操作的代码。

  1.如果Y也是被认作是一个红链接,此时Y当然不是一个左边的结点,但是其含义是Y和XPP是在同一个节点中,这样XP、XPP、Y就构成了一个4节点,X插入4节点中触发了4节点的分裂(这个与2-3树不同,2-3树是3节点变成4节点就进行分裂了),用图表示该段代码的结果如下:

Java之集合(十二)TreeMap

  4节点分裂,XPP归到上面节点合并成一个节点,然后继续循环处理XPP上面的相关内容了。由于每一步插入都保持平衡,此处XPP深度归于上面节点,则没有产生新的深度,此轮依旧平衡。

  2.如果Y不是一个红链接的时候,此时Y也可能就不存在,此时X的插入就比较麻烦了。X很有可能是打破了树的平衡,需要通过旋转重新将树保持平衡。旋转的作用就是减少旋转边树的深度,右旋就是减小左深度,左旋就是减小右深度,这句应该很好理解。理解不了看下图,下图给了一个左边深度失衡,进行右旋的例子。

Java之集合(十二)TreeMap

  X的插入虽然可能打破了平衡,但是并不清楚这个循环的X是否是叶子节点,叶子节点当然无所谓,但是非叶子节点的时候就需要进行判断X是插入XP的左边还是右边。

  首先要明确,所有的操作都保证了之前树是平衡的,现在插入X:

  ①如果在XP的右边,其XP的平衡可能被打破,XP的右边可能失衡,需要先进行左旋,维持XP的平衡,XP平衡后并不代表XPP平衡了,XPP的左边可能失衡了,需要进行右旋保持XPP的左右平衡。

  ②如果在XP的左边,由于之前就是平衡的,现在插入左边失衡了,不需要管XP的左边是否失衡,只要让XPP保持平衡就可以了,所以进行XPP的右旋。

  第二种情况:X的父节点不在父父节点的左边

Java之集合(十二)TreeMap

  做法是相同的,先判断左边是不是构成了4节点需要分裂,左边没构成,再看X是在XP的左边还是右边,左边的话需要保持XP的平衡,需要右旋,之后再保持XPP的平衡进行左旋。X在XP右边就没这么麻烦,只需要保持XPP的平衡,进行左旋就可以了。

  关于左旋右旋的方法,在HashMap那章中感觉已经介绍的很多了,这里不再进行介绍。链接点这里

  

  红黑树的删除暂时没有想清楚-。-???,等我研究明白再补充。

  虽然删除操作还是有些没看明白,但是还是记录一些明白了的地方,如果之后想明白了,再继续补充更新。下面是JDK8中的remove源码:

Java之集合(十二)TreeMap

  JDK将一个节点移除,会将其后继节点的键值放入该节点。注意是键值改变了,对象并没有变化。后继节点也不是随便挑选的,其通过方法successor(p),来找到后继节点。后继节点的选择原则是尽可能选择大于删除键的最小值,过程是如果删除节点存在右节点,选择右节点的最左边的子节点。如果没有右节点,如果该节点是父节点的右边,继续判断父节点是否是父父节点的右边,最后当父节点左边的时候,选择那个父节点(父节点右边,意味着删除节点大于父节点,最后处于某一个子树的左边,意味着该子树的父节点大于左边的所有节点,这样也满足了大于删除键的最小值)。代码如下:

Java之集合(十二)TreeMap

  这样选出来的后继节点都可以不破坏红黑树左节点<父节点<右节点的这一原则。剩下的就是处理这个后继节点了(因为其值已经赋给移除节点,该后继节点要从树结构中移除)。移除后继节点又分成了两种可能。这就是if-elseif-else中的内容了:后继节点分为不是叶子节点,或是叶子节点(又分为是根节点和非根节点)。根节点移除就十分简单了,直接root=null就行了。如果非叶子节点,就需要将其子节点替换掉该节点,优先左节点,替换完了之后可能打破了树的平衡,需要进行调整(这里有个问题没想明白,如果后继左右节点都存在,那后继节点的右节点怎么办,难道能保证后继节点只存在一个子节点-。-?)。叶子节点只需要解除链接就可以了,但是同样可能打破平衡,由于其是最后一个节点,所以必须先调整,再解除链接。打破平衡只可能发生在后继节点为黑的时候,红的话就没必要了,移除依旧是平衡的,因为红的高度实质上是隐藏了。

  fixAfterDeletion方法就是用来调整删除后的结点顺序,入参是后继节点位置的那个节点。所以非叶子节点时,先将后继节点的子节点上移了,移除占住了后继节点,而在叶子节点时,没有子节点,就不能先移除这个节点,必须先调整了。fixAfterDeletion只看一部分,另一部分是对称的不进行叙述。

Java之集合(十二)TreeMap

  循环条件当然是不为根节点,并且是可能移除打破平衡的黑节点。要跳出循环我们也可以看到,直接将x置为root节点就跳出了。这一段代码还是有些疑惑,还是给出自己的理解。先看后继节点X是其父节点左边的情况:

  X是左节点,同级的右节点是红节点,那么肯定打破了平衡,将XP这个子树左旋,维护深度。不论第一步有没有,XP的左右节点都是黑的时候,XP肯定是红,不能通过调整XP子树维持平衡,继续以XP为X进入下一个循环,扩大调整范围。否则第一步之后XP,通过XP的子树调节又是可以调整好的,关注XP的右节点SIB,SIB的右节点是黑的,将其并入SIB进行右旋,先调整好SIB的高度。如果是红的就不需要了,直接左旋。这里也只是有点感觉,为的就是只通过调整X的子树来维护平衡,维护不了就扩大子树进行,维护的了的时候会进行更下层的调整,让下层调整能够成功。大概是一个这个意思。

4.后记

  TreeMap中又重看了一遍红黑树,比HashMap中所说的要清楚一些,删除操作还没想明白,需要一段时间来解决。