集合系列--TreeMap实现详解

1、TreeMap概述:

     对于TreeMap,它采用的是被叫做“红黑树”的排序二叉树来保存Map中的每个Entry,每个Entry都被当做红黑树的一个节点来对待,而红黑树是一种自平衡查找二叉树,树种每个节点的值都大于或等于他的左子树中的所有节点的值,并小于等于他的右子树中所有节点的值,这可以使我们快速的查找和定位所需的节点。

 2、TreeMap存储实现:

既然TreeMap是基于红黑树的实现,那么它保存的Entry也会和HashMap的不同,其记录了子节点和父节点的引用,我们来看一下Entry类的源码:

static final class Entry<K,V> implements Map.Entry<K,V> {
 // 键值对的“键”
 K key;
 // 键值对的“值”
     V value;
     // 左孩子
     Entry<K,V> left = null;
     // 右孩子
     Entry<K,V> right = null;
     // 父节点
     Entry<K,V> parent;
     // 红黑树的节点表示颜色的属性
     boolean color = BLACK;
     /**
      * 根据给定的键、值、父节点构造一个节点,颜色为默认的黑色
      */
     Entry(K key, V value, Entry<K,V> parent) {
         this.key = key;
         this.value = value;
         this.parent = parent;
     }
     // 获取节点的key
     public K getKey() {
         return key;
     }
     // 获取节点的value
     public V getValue() {
         return value;
     }
     /**
      * 修改并返回当前节点的value
      */
     public V setValue(V value) {
         V oldValue = this.value;
         this.value = value;
         return oldValue;
     }
     // 判断节点相等的方法(两个节点为同一类型且key值和value值都相等时两个节点相等)
     public boolean equals(Object o) {
         if (!(o instanceof Map.Entry))
             return false;
         Map.Entry<?,?> e = (Map.Entry<?,?>)o;
         return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
     }
     // 节点的哈希值计算方法
     public int hashCode() {
         int keyHash = (key==null ? 0 : key.hashCode());
         int valueHash = (value==null ? 0 : value.hashCode());
         return keyHash ^ valueHash;
     }
     public String toString() {
         return key + "=" + value;
     }
 }

每一个Entry都保存了它子节点和父节点的引用,哈希值的计算方法等。

接着我们来看一下他的put(K key,V value);方法:

public V put(K key, V value) {
         Entry<K,V> t = root;
         if (t == null) {
         //如果根节点为null,将传入的键值对构造成根节点(根节点没有父节点,所以传入的父节点为null)
             root = new Entry<K,V>(key, value, null);
             size = 1;
             modCount++;
             return null;
         }
         // 记录比较结果
         int cmp;
         Entry<K,V> parent;
         // 分割比较器和可比较接口的处理
         Comparator<? super K> cpr = comparator;
         // 有比较器的处理
         if (cpr != null) {
             // do while实现在root为根节点移动寻找传入键值对需要插入的位置
             do {
                 // 记录将要被掺入新的键值对将要节点(即新节点的父节点)
                 parent = t;
                 // 使用比较器比较父节点和插入键值对的key值的大小
                 cmp = cpr.compare(key, t.key);
                 // 插入的key较大
                 if (cmp < 0)
                     t = t.left;
                 // 插入的key较小
                 else if (cmp > 0)
                     t = t.right;
                 // key值相等,替换并返回t节点的value(put方法结束)
                 else
                     return t.setValue(value);
             } while (t != null);
         }
         // 没有比较器的处理
         else {
             // key为null抛出NullPointerException异常
             if (key == null)
                 throw new NullPointerException();
             Comparable<? super K> k = (Comparable<? super K>) key;
             // 与if中的do while类似,只是比较的方式不同
             do {
                 parent = t;
                 cmp = k.compareTo(t.key);
                 if (cmp < 0)
                     t = t.left;
                 else if (cmp > 0)
                     t = t.right;
                 else
                     return t.setValue(value);
             } while (t != null);
         }
         // 没有找到key相同的节点才会有下面的操作
         // 根据传入的键值对和找到的“父节点”创建新节点
         Entry<K,V> e = new Entry<K,V>(key, value, parent);
         // 根据最后一次的判断结果确认新节点是“父节点”的左孩子还是又孩子
         if (cmp < 0)
             parent.left = e;
         else
             parent.right = e;
         // 对加入新节点的树进行调整
         fixAfterInsertion(e);
         // 记录size和modCount
         size++;
         modCount++;
         // 因为是插入新节点,所以返回的是null
         return null;
     }


可以看出,每当程序希望添加新节点时,总是会从根节点进行比较,将根节点作为当前节点,如果新增节点大于当前节点,并且当前节点的右子节点存在,则以右子节点作为当前节点继续比较,如果新增节点小于当前节点且当前节点的左子节点存在,则以左子节点作为当前节点进行比较,如果新增节点等于当前节点,新增节点则覆盖当前节点,如果当前节点的为叶子节点(既无左子节点,也无右子结点),那么新增节点比当前节点大,则增加为当前节点的右子结点,如果比当前节点小,则增加为左子节点。

put方法在最后调用了fixAfterInsertion(e)方法,来对新树进行调整,调整其新树的结构和着色,以满足红黑树的要求,我们来看一下fixAfterInsertion(e)方法的源码:

private void fixAfterInsertion(Entry<K,V> x) {
     // 插入节点默认为红色
     x.color = RED;
     // 循环条件是x不为空、不是根节点、父节点的颜色是红色(如果父节点不是红色,则没有连续的红色节点,不再调整)
     while (x != null && x != root && x.parent.color == RED) {
         // x节点的父节点p(记作p)是其父节点pp(p的父节点,记作pp)的左孩子(pp的左孩子)
         if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
             // 获取pp节点的右孩子r
             Entry<K,V> y = rightOf(parentOf(parentOf(x)));
             // pp右孩子的颜色是红色(colorOf(Entry e)方法在e为空时返回BLACK),不需要进行旋转操作(因为红黑树不是严格的平衡二叉树)
             if (colorOf(y) == RED) {
                 // 将父节点设置为黑色
                 setColor(parentOf(x), BLACK);
                 // y节点,即r设置成黑色
                 setColor(y, BLACK);
                 // pp节点设置成红色
                 setColor(parentOf(parentOf(x)), RED);
                 // x“移动”到pp节点
                 x = parentOf(parentOf(x));
             } else {//父亲的兄弟是黑色的,这时需要进行旋转操作,根据是“内部”还是“外部”的情况决定是双旋转还是单旋转
                 // x节点是父节点的右孩子(因为上面已近确认p是pp的左孩子,所以这是一个“内部,左-右”插入的情况,需要进行双旋转处理)
                 if (x == rightOf(parentOf(x))) {
                     // x移动到它的父节点
                     x = parentOf(x);
                     // 左旋操作
                     rotateLeft(x);
                 }
                 // x的父节点设置成黑色
                 setColor(parentOf(x), BLACK);
                 // x的父节点的父节点设置成红色
                 setColor(parentOf(parentOf(x)), RED);
                 // 右旋操作
                 rotateRight(parentOf(parentOf(x)));
             }
         } else {
             // 获取x的父节点(记作p)的父节点(记作pp)的左孩子
             Entry<K,V> y = leftOf(parentOf(parentOf(x)));
             // y节点是红色的
             if (colorOf(y) == RED) {
                 // x的父节点,即p节点,设置成黑色
                 setColor(parentOf(x), BLACK);
                 // y节点设置成黑色
                 setColor(y, BLACK);
                 // pp节点设置成红色
                 setColor(parentOf(parentOf(x)), RED);
                 // x移动到pp节点
                 x = parentOf(parentOf(x));
             } else {
                 // x是父节点的左孩子(因为上面已近确认p是pp的右孩子,所以这是一个“内部,右-左”插入的情况,需要进行双旋转处理),
                 if (x == leftOf(parentOf(x))) {
                     // x移动到父节点
                     x = parentOf(x);
                     // 右旋操作
                     rotateRight(x);
                 }
                 // x的父节点设置成黑色
                 setColor(parentOf(x), BLACK);
                 // x的父节点的父节点设置成红色
                 setColor(parentOf(parentOf(x)), RED);
                 // 左旋操作
                 rotateLeft(parentOf(parentOf(x)));
             }
         }
     }
     // 根节点为黑色
     root.color = BLACK;
 }


红黑树要求:每一个节点或者成红色或者为黑色,根节点为黑色,如果一个节点为红色,那么它的子节点必为黑色,一个节点到一个null引用的每一条路径必须包含相同数量的黑色节点。

 

3、TreeMap读取实现

我们来看一下get(Object key);方法

public V get(Object key) {
    Entry<K,V> p = getEntry(key);
    return (p==null ? null : p.value);
}


该方法实际上调用了getEntry(key)方法,getEntry(key);方法如下:

final Entry<K,V> getEntry(Object key) {
     // 如果有比较器,返回getEntryUsingComparator(Object key)的结果
     if (comparator != null)
         return getEntryUsingComparator(key);
     // 查找的key为null,抛出NullPointerException
     if (key == null)
         throw new NullPointerException();
     // 如果没有比较器,而是实现了可比较接口
     Comparable<? super K> k = (Comparable<? super K>) key;
     // 获取根节点
     Entry<K,V> p = root;
     // 对树进行遍历查找节点
     while (p != null) {
         // 把key和当前节点的key进行比较
         int cmp = k.compareTo(p.key);
         // key小于当前节点的key
         if (cmp < 0)
             // p “移动”到左节点上
             p = p.left;
         // key大于当前节点的key
         else if (cmp > 0)
             // p “移动”到右节点上
 p = p.right;
         // key值相等则当前节点就是要找的节点
         else
             // 返回找到的节点
             return p;
         }
     // 没找到则返回null
     return null;
 }

该方法和存储方法类似,依然是从根节点出发,将根节点作为当前节点,如果被搜索节点大于当前节点,则程序向右子树搜索,如果小于则程序向左子树搜索,如果相等则返回当前节点。

 

如果程序采用了定制排序方式,那么读取时会调用getEntryUsingComparator(key);方法来获取Entry代码如下:

final Entry<K,V> getEntryUsingComparator(Object key) {
     K k = (K) key;
     // 获取比较器
 Comparator<? super K> cpr = comparator;
 // 其实在调用此方法的get(Object key)中已经对比较器为null的情况进行判断,这里是防御性的判断
 if (cpr != null) {
     // 获取根节点
         Entry<K,V> p = root;
         // 遍历树
         while (p != null) {
             // 获取key和当前节点的key的比较结果
             int cmp = cpr.compare(k, p.key);
             // 查找的key值较小
             if (cmp < 0)
                 // p“移动”到左孩子
                 p = p.left;
             // 查找的key值较大
             else if (cmp > 0)
                 // p“移动”到右节点
                 p = p.right;
             // key值相等
             else
                 // 返回找到的节点
                 return p;
         }
 }
 // 没找到key值对应的节点,返回null
     return null;
 }


这两种实现大体相同,前者是在使用自然排序的时候使用后者是在使用定制排序的时候使用。

 

4、TreeMap的排序方式

TreeMap有自然排序和定制排序两种排序方式:

自然排序:TreeMap的所有的key必须实现Comparable接口,并且所有的key必须是同一个类的对象,否则将会抛出ClassCastException异常,排序方式按照key的值升序排序。

定制排序:

首先看一下TreeMap的构造方法:

 public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }

通过传入一个Comparator对象来实现定制排序,该对象负责对treemap中的所有key进行排序,并且采用定制排序不要求key实现comparable接口。具体排序方式可以在compare(Object o1, Object o2)方法中指定。

 

对于TreeMap 的删除操作,大致就是由根节点开始依次比较,找到要删除的元素,然后删除,这里就不详述。

 

参考文章:TreeMap源码解析

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值