因为TreeMap的相关知识较多,故TreeMap的分析将会分成三篇文章来写,望大家谅解。
本篇文章先给大家介绍一下红黑树基本概念,并分析一下在红黑树中查找某个结点的相关源码实现。
TreeMap是啥
从TreeMap的类名上就能知道它的底层存储结构其实是红黑树。首先简单介绍一下红黑树的相关知识,以便理解后续内容。
什么是红黑树?先放一张红黑树的示意图看看:
注:图片出处为 博客园 —— 五月的仓颉
简单解释一下树的相关术语的含义:
1.根结点(即0026结点):整个树结构图中最上面的一个结点,其他所有的结点都是由该结点延伸出去的。
2.父结点:在树的上下结构中,有连接关系并处于上方的结点。比如0026是0017和0041的父结点,0017是0012和0021的父结点等等。
3.子结点:在树的上下结构中,有连接关系并处于下方的结点。比如0017是0026的左子结点,0041是0026的右子结点;0030是0041的左子结点,0047是0041的右子结点。
并且左子节点是小于父结点的,而右子节点一般是大于父结点的。比如0012比0017小,而0010比0012小;同样0041比0026大,并且0047也比0041大。
4.兄弟结点:属于同一个父结点的结点互相称为兄弟结点。比如0017和0041同属于0026的子结点,则0017和0041就是兄弟节点。
5.叶子结点:没有任何子结点的结点称为叶子节点。如上图中所有的NULL LEAF结点都是叶子结点。
6.二叉树:每个节点最多只有两个子结点的树结构称为二叉树。如上图就是一种二叉树的数据结构。
树的一些基本术语就介绍到这里,其他的请同学们自行查阅资料学习吧,这里就不再多说了 : )
什么是红黑树
上面已经说了TreeMap是基于红黑树的存储结构,那什么是红黑树呢?
红黑树是一种特殊的二叉树,它的每个结点都额外有一个颜色的属性,颜色只有两种:红色和黑色。
关于红黑树有以下几个规定:
1.首先当前树必须为二叉树的结构。
2.每个结点都有一种颜色,且颜色只能为红色或黑色
3.根结点必须是黑色。
4.叶子结点必须是黑色。
5.不能同时出现父结点和子结点都是红色的情况。即如果一个结点是父结点且为红色,则它的子结点必须全部为黑色。
6.从根结点到每个叶子结点经过的路程中,黑色结点的数量必须是一样的。
比如 0026 -> 0017 -> 0012 -> 0010 -> 0003 -> 叶子结点,这条路径上共有4个黑色结点;
0026 -> 0041 -> 0030 -> 0038 -> 叶子结点,这条路径上也有4个黑色结点。
只有以上6个条件全部满足的树,我们才称其为红黑树。比如上图的中的树结构就是一个标准的红黑树。
TreeMap源码中的数据结构及相关属性
1 /** 2 * 红黑树每个结点对象的数据结构 3 * 4 * 5 * @param <K> key 6 * @param <V> value 7 */ 8 static final class Entry<K,V> implements Map.Entry<K,V> { 9 K key; //键 10 V value; //值 11 Entry<K,V> left; //左子结点引用 12 Entry<K,V> right; //右子结点引用 13 Entry<K,V> parent; //父结点引用 14 boolean color = BLACK; //结点默认为黑色 15 16 /** 17 * 构造一个叶子结点,默认无左右子结点,颜色为黑色 18 */ 19 Entry(K key, V value, Entry<K,V> parent) { 20 this.key = key; 21 this.value = value; 22 this.parent = parent; 23 } 24 25 /** 26 * 返回key 27 * 28 * @return the key 29 */ 30 public K getKey() { 31 return key; 32 } 33 34 /** 35 * 返回key对应的value 36 * 37 * @return the value associated with the key 38 */ 39 public V getValue() { 40 return value; 41 } 42 43 /** 44 * 替换对应的值 45 * 46 * @return 原先被替换的值 47 */ 48 public V setValue(V value) { 49 V oldValue = this.value; 50 this.value = value; 51 return oldValue; 52 } 53 54 /** 55 * 重写结点的equals()方法,比较结点的大小时使用 56 */ 57 public boolean equals(Object o) { 58 if (!(o instanceof Map.Entry)) 59 return false; 60 Map.Entry<?,?> e = (Map.Entry<?,?>)o; 61 //必须key和value都相同才算相等 62 return valEquals(key,e.getKey()) && valEquals(value,e.getValue()); 63 } 64 65 /** 66 * 重写结点的hashCode()方法 67 */ 68 public int hashCode() { 69 int keyHash = (key==null ? 0 : key.hashCode()); 70 int valueHash = (value==null ? 0 : value.hashCode()); 71 return keyHash ^ valueHash; //key的hashCode 异或 value的hashCode 72 } 73 74 /** 75 * 重写toString()方法 76 */ 77 public String toString() { 78 return key + "=" + value; 79 } 80 } 81 82 /** 用户自定义的比较器 */ 83 private final Comparator<? super K> comparator; 84 85 /** 红黑树根结点 */ 86 private transient Entry<K,V> root; 87 88 /** 红黑树总结点个数 */ 89 private transient int size = 0;
可以看到TreeMap因为实现了Map的结点的数据结构,所以同样有key和value两个属性。并且因为是红黑树,所以还有父结点、左右子结点的引用以及结点颜色这些属性。
我们看到第83行有个 comparator 属性,它表示当前TreeMap使用的是哪种比较器。
大家都知道TreeMap是支持结点排序的,它有两种排序方式:
1.按照结点key的自然顺序维护结点的顺序。比如有两个结点 A Entry<1,"11"> 和 B Entry<2,"222">,以这种key的自然排序来讲,如果先插入A后插入B,则A为B父结点,B为A的右子结点;如果先插入B后插入A,那么B为A的父结点,A为B的左子结点。
2.另一种排序是用户自定义的排序方式。在构造方法中传入一个自定义的比较器,则TreeMap会根据用户自己定义的结点对象比较大小的方法来维护结点的顺序。
TreeMap构造方法
1 /** 2 * 无参数构造方法,使用key的自然比较顺序来维护树中结点的顺序 3 */ 4 public TreeMap() { 5 comparator = null; 6 } 7 8 /** 9 * 带参数构造方法,使用用户传入的比较器来维护树中结点的顺序 10 * @param 用户自定义比较器,如果为空,则该Map会使用key的自然排序维护树的顺序 11 */ 12 public TreeMap(Comparator<? super K> comparator) { 13 this.comparator = comparator; 14 }
TreeMap获取某个结点的源码分析
1 /** 2 * 获取节点的操作 3 * 如果在map中找到了这个键,则返回键对应的值;找不到对应的键或者键指向为null,否则返回null 4 * 5 * @throws ClassCastException 用户传入的key和当前map中的key无法比较(不是同一类型),则报该异常 6 * @throws NullPointerException 使用了自然排序但传入的key为null,或者比较器不支持key为null的情况,则报此异常 7 * 8 */ 9 public V get(Object key) { 10 Entry<K,V> p = getEntry(key); //可以看到实际上是调用getEntry()这个方法来获取节点的 11 return (p==null ? null : p.value); 12 } 13 14 15 /** 16 * 返回通过传入的键在map中找到的相应entry,若未找到则返回null 17 * 18 * @throws ClassCastException 用户传入的key和当前map中的key无法比较(不是同一类型),则报该异常 19 * @throws NullPointerException 使用了自然排序但传入的key为null,或者比较器不支持key为null的情况,则报此异常 20 */ 21 final Entry<K,V> getEntry(Object key) { 22 // 首先要区分是使用map键的自然排序查找还是使用用户自定义的比较器来进行查找 23 if (comparator != null) //如果用户传入了自定义比较器 24 return getEntryUsingComparator(key); //调用getEntryUsingComparator()方法,通过用户自定义比较器的compare()方法查找entry 25 if (key == null) //如果key为null,报空指针异常 26 throw new NullPointerException(); 27 Comparable<? super K> k = (Comparable<? super K>) key; //没有传入自定义比较器,使用key的自然排序比较传入的key和map中的key 28 Entry<K,V> p = root; //从根节点开始比较 29 while (p != null) { //如果节点不是空,则一直循环遍历比较 30 int cmp = k.compareTo(p.key); //获取传入的key和当前节点key的比较结果,使用自然排序Comparable实现的compareTo()方法进行比较 31 if (cmp < 0) //结果<0,说明传入的key比当前节点的key小 32 p = p.left; //将下次比较的节点变更为当前节点的左子节点 33 else if (cmp > 0) //结果>0,说明传入的key比当前节点的key大 34 p = p.right; //将下次比较的节点变更为当前节点的右子节点 35 else //结果相等,则该节点就是要查找的节点 36 return p; //直接返回节点对应的Entry 37 } 38 return null; //遍历完整个树也没找到,则返回null 39 } 40 41 42 43 /** 44 * 使用用户自定义比较器的比较方法来查找节点 45 * 逻辑与通过自然排序查找类似,不再赘述 46 */ 47 final Entry<K,V> getEntryUsingComparator(Object key) { 48 @SuppressWarnings("unchecked") 49 K k = (K) key; 50 Comparator<? super K> cpr = comparator; 51 if (cpr != null) { 52 Entry<K,V> p = root; 53 while (p != null) { 54 int cmp = cpr.compare(k, p.key); //仅仅这里和getEntry()方法不同,使用自定义比较器的compare()方法进行比较 55 if (cmp < 0) 56 p = p.left; 57 else if (cmp > 0) 58 p = p.right; 59 else 60 return p; 61 } 62 } 63 return null; 64 }
可以看到查找其实很简单,就是从根结点开始往下遍历比较。如果要查找的节点比较小,则继续往左子结点比较;反之则往右子结点比较;如果相等,则当前结点就是要查找的结点。
要注意的只有一点,即TreeMap使用的是key的自然排序还是用户自定义的排序。
key自然排序使用的是实现了Comparable接口的compareTo()方法来进行比较,而用户自定义排序则使用的实现Comparator接口的compare()方法来比较。
这里延伸一个经典的面试题:Comparable接口和Comparator接口有啥相同和不同之处?
相同点:两个接口都是用于支持对象进行比较大小所定义的。
不同点:
1.Comparable具体实现比较大小的方法是CompareTo(),而Comparator具体实现比较大小的方法是Compare()。
2.Comparable一般强调相同类型的对象进行比较(比如小明和小红都是人类,那他们之间的大小就是按年龄大小来进行比较的,那么CompareTo()方法中其实就是两人的age进行比较);
而相对的Comparator一般强调不同类型的对象进行比较(比如小华是人类,旺财是狗,而人类要和狗进行比较大小,则必须由用户自己制定一个比较规则来确定谁大谁小,那么compare()方法中其实就是自定义的两个类型的不同属性进行比较)。
明确这两个接口的区别后,上面查询结点的方法也就不难理解啦 : )
本篇文章到此结束,内容还是偏基础,希望大家不吝赐教。
那么下篇文章我会和大家一起分析下在插入节点后,红黑树内部数据结构是怎样一步步变化的,敬请期待哟~