• TreeMap分析(上)


    因为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()方法中其实就是自定义的两个类型的不同属性进行比较)。

    明确这两个接口的区别后,上面查询结点的方法也就不难理解啦  : )

    本篇文章到此结束,内容还是偏基础,希望大家不吝赐教。

    那么下篇文章我会和大家一起分析下在插入节点后,红黑树内部数据结构是怎样一步步变化的,敬请期待哟~

  • 相关阅读:
    You have new mail in /var/spool/mail/root
    ./configure: error: C compiler cc is not found
    XAMPP下MYSQL中文乱码问题的解决
    Linux服务器配置秘钥对连接
    linux根分区满了如何处理,查找大文件方法
    Linux下挂载windows的共享文件夹
    zabbix分布式监控系统安装配置
    远程桌面,身份验证错误:要求的函数不正确等解决办法
    Linux之Shell 脚本加密工具-shc
    Shell脚本监控专线Network并SendEmail报警
  • 原文地址:https://www.cnblogs.com/smartchen/p/9032266.html
Copyright © 2020-2023  润新知