Java集合,TreeMap底层实现和原理

Wesley13
• 阅读 931

概述

文章的内容基于JDK1.7进行分析,之所以选用这个版本,是因为1.8的有些类做了改动,增加了阅读的难度,虽然是1.7,但是对于1.8做了重大改动的内容,文章也会进行说明。

TreeMap实现了SotredMap接口,它是有序的集合。而且是一个红黑树结构,每个key-value都作为一个红黑树的节点。如果在调用TreeMap的构造函数时没有指定比较器,则根据key执行自然排序。这点会在接下来的代码中做说明,如果指定了比较器则按照比较器来进行排序。

数据结构

继承关系

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable {}

实现接口

Serializable, Cloneable, Map<K,V>, NavigableMap<K,V>, SortedMap<K,V> 

基本属性

private final Comparator<? super K> comparator;  //比较器,是自然排序,还是定制排序 ,使用final修饰,表明一旦赋值便不允许改变
private transient Entry<K,V> root = null;  //红黑树的根节点
private transient int size = 0;     //TreeMap中存放的键值对的数量
private transient int modCount = 0;   //修改的次数

源码解析

由于TreeMap中源码较长,接下来将分段解析部分源码。既然是红黑树存储,肯定要有数据结构(Node)节点的。看一下TreeMap中关于节点的定义部分。

数据结构

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;      //节点的颜色,在红黑树种,只有两种颜色,红色和黑色

    //构造方法,用指定的key,value ,parent初始化,color默认为黑色
    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;
    }

    //替换节点的值,并返回旧值
    public V setValue(V value) {
        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }
    //重写equals()方法
    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;
        //两个节点的key相等,value相等,这两个节点才相等
        return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
    }
    //重写hashCode()方法
    public int hashCode() {
        int keyHash = (key==null ? 0 : key.hashCode());
        int valueHash = (value==null ? 0 : value.hashCode());
        //key和vale hash值得异或运算,相同则为零,不同则为1 
        return keyHash ^ valueHash;
    }
    //重写toString()方法
    public String toString() {
        return key + "=" + value;
    }
}

构造方法

//构造方法,comparator用键的顺序做比较
public TreeMap() {
    comparator = null;
}

//构造方法,提供比较器,用指定比较器排序
public TreeMap(Comparator<? super K> comparator) {
    his.comparator = comparator;
}

//将m中的元素转化daoTreeMap中,按照键的顺序做比较排序
public TreeMap(Map<? extends K, ? extends V> m) {
    comparator = null;
    putAll(m);
}

//构造方法,指定的参数为SortedMap
//采用m的比较器排序
public TreeMap(SortedMap<K, ? extends V> m) {
    comparator = m.comparator();
    try {
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
    } catch (ClassNotFoundException cannotHappen) {
    }
}

TreeMap提供了四个构造方法,实现了方法的重载。无参构造方法中比较器的值为null,采用自然排序的方法,如果指定了比较器则称之为定制排序.

  • 自然排序:TreeMap的所有key必须实现Comparable接口,所有的key都是同一个类的对象
  • 定制排序:创建TreeMap对象传入了一个Comparator对象,该对象负责对TreeMap中所有的key进行排序,采用定制排序不要求Map的key实现Comparable接口。等下面分析到比较方法的时候在分析这两种比较有何不同。

对于Map来说,使用的最多的就是put()/get()/remove()等方法,下面依次进行分析

put()

public V put(K key, V value) {
    Entry<K,V> t = root;     //红黑树的根节点
    if (t == null) {        //红黑树是否为空
        compare(key, key); // type (and possibly null) check
        //构造根节点,因为根节点没有父节点,传入null值。 
        root = new Entry<>(key, value, null);  
        size = 1;     //size值加1
        modCount++;    //改变修改的次数
        return null;    //返回null 
    }
    int cmp;
    Entry<K,V> parent;    //定义节点
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;     //获取比较器
    if (cpr != null) {      //如果定义了比较器,采用自定义比较器进行比较
        do {
            parent = t;      //将红黑树根节点赋值给parent
            cmp = cpr.compare(key, t.key);     //比较key, 与根节点的大小
            if (cmp < 0)      //如果key < t.key , 指向左子树
                t = t.left;   //t = t.left  , t == 它的做孩子节点
            else if (cmp > 0)
                t = t.right;  //如果key > t.key , 指向它的右孩子节点
            else
                return t.setValue(value);      //如果它们相等,替换key的值
        } while (t != null);        //循环遍历
    }
    else {
        //自然排序方式,没有指定比较器
        if (key == null)
            throw new NullPointerException();  //抛出异常
        Comparable<? super K> k = (Comparable<? super K>) key;    //类型转换
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)     // key < t.key 
                t = t.left;   //左孩子
            else if (cmp > 0)   // key > t.key 
                t = t.right;    //右孩子
            else
                return t.setValue(value);   //t == t.key , 替换value值
        } while (t != null);
    }
    Entry<K,V> e = new Entry<>(key, value, parent);   //创建新节点,并制定父节点
    //根据比较结果,决定新节点为父节点的左孩子或者右孩子
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);   //新插入节点后重新调整红黑树 
    size++;
    modCount++;
    return null;
}
//比较方法,如果comparator==null ,采用comparable.compartTo进行比较,否则采用指定比较器比较大小
final int compare(Object k1, Object k2) {
    return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
        : comparator.compare((K)k1, (K)k2);
}

private void fixAfterInsertion(Entry<K,V> x) {
    //插入的节点默认的颜色为红色
    x.color = RED;    //
    //情形1: 新节点x 是树的根节点,没有父节点不需要任何操作
    //情形2: 新节点x 的父节点颜色是黑色的,也不需要任何操作

    while (x != null && x != root && x.parent.color == RED) {
    //情形3:新节点x的父节点颜色是红色的
    //判断x的节点的父节点位置,是否属于左孩子
    if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
          //获取x节点的父节点的兄弟节点,上面语句已经判断出x节点的父节点为左孩子,所以直接取右孩子
         Entry<K,V> y = rightOf(parentOf(parentOf(x)));
         //判断是否x节点的父节点的兄弟节点为红色。
         if (colorOf(y) == RED) {
              setColor(parentOf(x), BLACK); // x节点的父节点设置为黑色
              setColor(y, BLACK);           // y节点的颜色设置为黑色
              setColor(parentOf(parentOf(x)), RED); // x.parent.parent设置为红色
              x = parentOf(parentOf(x)); // x == x.parent.parent ,进行遍历。
         } else {
               //x的父节点的兄弟节点是黑色或者缺少的
               if (x == rightOf(parentOf(x))) {   //判断x节点是否为父节点的右孩子
                    x = parentOf(x);     //x == 父节点
                    rotateLeft(x);    //左旋转操作
               }
               //x节点是其父的左孩子
               setColor(parentOf(x), BLACK);
               setColor(parentOf(parentOf(x)), RED);  //上面两句将x.parent 和x.parent.parent的颜色做调换
               rotateRight(parentOf(parentOf(x)));   //进行右旋转
            }
        } else {
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));  //y 是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 是其父亲的左孩子
                    x = parentOf(x);
                    rotateRight(x);    //以父节点为旋转点,进行右旋操作
                }
                setColor(parentOf(x), BLACK);    //父节点为设置为黑色
                setColor(parentOf(parentOf(x)), RED);  //祖父节点设置为红色
                rotateLeft(parentOf(parentOf(x)));  //以父节点为旋转点,进行左旋操作
            }
        }
    }
    root.color = BLACK; //通过节点位置的调整,最终将红色的节点条调换到了根节点的位置,根节点重新设置为黑色
}

红黑树是一个更高效的检索二叉树,有如下特点:

  1. 每个节点只能是红色或者黑色
  2. 根节点永远是黑色的
  3. 所有的叶子的子节点都是空节点,并且都是黑色的
  4. 每个红色节点的两个子节点都是黑色的(不会有两个连续的红色节点)
  5. 从任一个节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点(叶子节点到根节点的黑色节点数量每条路径都相同)

上面的代码,详细的标注了每条语句的作用,但是我相信,如果你没有一定的功力,即使注释已经很详细了,你也会是一脸懵逼 ,二脸懵逼,全脑懵逼中,下面配合图片来梳理一下代码所表示的含义:
当一个默认为红色的节点插入树中,其实对应的是7中可能发生的情况,分别进行叙述:

  • 情形1:新插入的节点时红黑树的根节点,没有父节点,无需任何的操作,直接将颜色设置为黑色就可以了

  • 情形2:新节点的父节点颜色是黑色的,新插入的节点是红色的。也无需任何的操作。因为新节点的插入并没有影响到红黑书的特点

  • 情形3:新节点的父节点(左孩子节点)颜色是红色的,而父节点的兄弟节点颜色也是红色的。那么情况就出现了,此时插入的节点就违反了红黑树的特点4 ,需要对红黑树进行调整。 操作看下图:Java集合,TreeMap底层实现和原理
    调整操作如上图,将父节点和父节点的兄弟节点,都修改为红色,然后将祖父节点修改为红色,因为修改了祖父节点的颜色,祖父节点可能会发生颜色的冲突,所以将新插入的节点修改为祖父节点,在进行调整。

  • 情形4:父节点(左孩子节点)的颜色为红色,父节点的兄弟节点的颜色为黑色或者为null,新插入的节点为父节点的右孩子节点。如下图:
    Java集合,TreeMap底层实现和原理
    此时以父节点为旋转点,就新插入的节点进行左旋操作。便变成了情形5对应的情况,将执行情形5的操作

  • 情形5:父节点(左孩子节点)的颜色为红色,父节点的兄弟节点颜色为黑色或者null,新插入节点为父亲的左孩子节点。如下图:
    Java集合,TreeMap底层实现和原理

  • 情形6 和情形7的操作与情形4和情形5的操作相同,它们之前的区别是父节点为有孩子节点,再次不再赘述。

remove()

public V remove(Object key) {
    Entry<K,V> p = getEntry(key);  //根据key查找节点,并返回该节点
    if (p == null)
        return null;

    V oldValue = p.value;    //获取key对应的值
    deleteEntry(p);     //删除节点
    return oldValue;   //返回key对应的值
}

final Entry<K,V> getEntry(Object key) {
    //根据键寻找节点,有非为两种方式,如果定制了比较器,采用定制排序方式,否则使用自然排序
    if (comparator != null) 
        return getEntryUsingComparator(key); //循环遍历树,寻找和key相等的节点
    if (key == null)
        throw new NullPointerException();
    Comparable<? super K> k = (Comparable<? super K>) key;
    Entry<K,V> p = root;
    while (p != null) {  //循环遍历树,寻找和key相等的节点
        int cmp = k.compareTo(p.key);
        if (cmp < 0)
            p = p.left;
        else if (cmp > 0)
            p = p.right;
        else
            return p;
    }
    return null;
}
//删除节点
private void deleteEntry(Entry<K,V> p) {
    modCount++;  //记录修改的次数
    size--;   //数量减1

    //当前节点的两个孩子都不为空
    if (p.left != null && p.right != null) {
        //寻找继承者,继承者为当前节点的右孩子节点或者右孩子节点的最小左孩子
        Entry<K,V> s = successor(p);
        p.key = s.key;     //key - value  的替换 ,并没有替换颜色
        p.value = s.value;
        p = s;  //指向继承者
    } // p has 2 children

    // Start fixup at replacement node, if it exists.
    //开始修复树结构,继承者的左孩子不为空,返回左孩子,否则返回右孩子
    //不可能存在左右两个孩子都存在的情况,successor寻找的就是最小节点,它的左孩子节点为null
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);

    if (replacement != null) {
        // Link replacement to parent
        //已经被选为继承者,当前拥有的一切放弃,所以将孩子交给爷爷抚养
        replacement.parent = p.parent;
        //p节点没有父节点,则指向根节点
        if (p.parent == null)
           root = replacement;
        //如果p为左孩子,如果p为左孩子,则将p.parent.left = p.left
        else if (p == p.parent.left)
            p.parent.left  = replacement;
        else
            p.parent.right = replacement;

        //删除p节点到左右分支,和父节点的引用
        p.left = p.right = p.parent = null;

        // Fix replacement
        if (p.color == BLACK)
            //恢复颜色分配
            fixAfterDeletion(replacement);
    } else if (p.parent == null) { // return if we are the only node.
        //红黑书中父节点为空的只能是根节点。
        root = null;
    } else { //  No children. Use self as phantom replacement and unlink.
        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;
        }
    }
}
private void fixAfterDeletion(Entry<K,V> x) {
    //不是根节点,颜色为黑色,调整结构
    while (x != root && colorOf(x) == BLACK) {

        //判断x是否为左孩子
        if (x == leftOf(parentOf(x))) {
            //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)); //重新设置x的兄弟节点
            }

            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED); //兄弟节点的两个孩子都是黑色的重新设置兄弟节点的颜色,修改为红色
                x = parentOf(x);   //将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;  //设置x为根节点
            }
        } else { // symmetric
            //x为父节点的右节点,参考上面的操作
            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);
}

删除红黑树的操作比插入操作要稍微麻烦一点,分为两步:

  • 以排序二叉树的方法删除指定节点。删除的节点存在三种情况:
    • 被删除节点,没有左右孩子节点,直接删除即可
    • 被删除节点,有一个孩子节点,那么让它的孩子节点指向它的父节点即可
    • 本删除的节点,有两个非空的孩子节点,那么需要找到该节点的前驱或者后继节点,更换元素值,在将前驱或者后继节点删除(任意一个节点的前驱或者后继都必定至多有一个非空的子节点,可以按照前面的两种情形进行操作)
  • 进行颜色的调换和树的旋转,满足红黑树的特征

下面来分情形讨论一下可能发生的情况:

  • 情形1:被删除的节点为根节点或者颜色为空色,此时删除该节点不影响红黑树的特点。无需操作

  • 情形2:被删除节点为黑色,兄弟节点为红色,如下图:
    Java集合,TreeMap底层实现和原理
    若删除上图中的x节点,将缺少一个黑节点,与红黑树的性质冲突,所以修改sib颜色为黑色,设置p节点为红色,并进行左旋操作。在进行后续的处理。

  • 情形3:被删除节点为黑色,x节点的兄弟节点的子节点都是黑色,如下图:
    Java集合,TreeMap底层实现和原理
    x节点是黑色的,兄弟节点(黑色的)的子节点也是黑色的,p节点的颜色无法确定,有可能是红色的,也有可能是黑色的。如果是红色的直接设置为黑色即可,如果为黑色的,则需要将x定位的p节点,在进行处理。

  • 情形4:被删除节点为黑色,x的兄弟节点的右自子节点为黑色。如下图:
    Java集合,TreeMap底层实现和原理
    情形4的调整为了转变成情形5的情况,来进行处理。

  • 情形5:被删除节点为黑色,x的兄弟节点右子节点为红色。如下图:
    Java集合,TreeMap底层实现和原理
    sib的左子节点的颜色不确定,可能是红色也可能是黑色,但是对它并没有什么影响,因为变换前后它的上层分支的黑色节点数并没有改变。

上面的情形只是针对删除的节点是左孩子的情况,进行的分析,被删除的节点也可能是右分支。情况完全相同只不过左右顺序发生了颠倒,不再进行复述。

至此TreeMap中实现的最重要已经说完了。

下面简单说一下一些方法的作用

  • firstEntry() 返回Map中最小的key
  • higherEntry(Object key ) 返回该Map中位于key后一位的key-value
  • lowerEntry(Object key ) 返回该Map中唯一key前一位的key-value
  • tailMap(Object key , boolean inclusive) 返回该Map的子Map

总结

  • 关于红黑树的节点插入操作,首先是改变新节点,新节点的父节点,祖父节点,和新节点的颜色,能在当前分支通过节点的旋转改变的,则通过此种操作,来满足红黑书的特点。
  • 如果当前相关节点的旋转解决不了红黑树的冲突,则通过将红色的节点移动到根节点解决,最后在将根节点设置为黑色
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
java集合系列之HashMap源码
java集合系列之HashMap源码HashMap的源码可真不好消化!!!首先简单介绍一下HashMap集合的特点。HashMap存放键值对,键值对封装在Node(代码如下,比较简单,不再介绍)节点中,Node节点实现了Map.Entry。存放的键值对的键不可重复。jdk1.8后,HashMap底层采用的是数组加链表、红黑树的数据结构,因此实现起
大厂Java初级开发工程师!!!面试必问项之Set实现类:TreeSet
一、TreeSet概述1、TreeSet是SortedSet接口的实现类,TreeSet可以确保集合元素处于排序状态。2、TreeSet顾名思义他内部维护的是一个TreeMap,底层是红黑二叉树,他使得集合内都是有序的序列。  3、Tree可以按照添加对象的指定属性,进行排序,所以向TreeSet中添加的数据,要求是相同类的对象。4、两
zhenghaoz zhenghaoz
3年前
算法笔记:红黑树
红黑树,一种平衡二叉树,最为著名的应用就是CSTL中的map,是有序集合最为理想的存储方式之一。除了二叉树所具有的属性之后,红黑树中每个节点多了一个“颜色”属性,可以是红色或者是黑色。一棵红黑树应该满足一下的性质:1.每个节点是红色或者黑色的;2.根节点是黑色的;3.每个叶节点nil是黑色的(使用哨兵节点在删除调整时可以方便不少);4.如
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
Java 之 HashMap 集合
一、HashMap概述java.util.HashMap<k,v集合implementsMap<k,v接口HashMap集合的特点:1、HashMap集合底层是哈希表:查询速度特别的快JDK1.8之前:数组单向链表JDK1.8之后:数组单向链表|红黑树(
Wesley13 Wesley13
3年前
Java集合,HashMap底层实现和原理(1.7数组+链表与1.8+的数组+链表+红黑树)
概述文章的内容基于JDK1.7进行分析,之所以选用这个版本,是因为1.8的有些类做了改动,增加了阅读的难度,虽然是1.7,但是对于1.8做了重大改动的内容,文章也会进行说明。HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null建和null 值, 因为key不允许重复,因此只能有一个键为null,另外HashMap不能
京东云开发者 京东云开发者
11个月前
深入理解经典红黑树 | 京东物流技术团队
本篇我们讲红黑树的经典实现,Java中对红黑树的实现便采用的是经典红黑树。前一篇文章我们介绍过左倾红黑树,它相对来说比较简单,需要大家看完上篇再来看这一篇,因为旋转等基础知识不会再本篇文章中赘述。本篇的大部分内容参考《算法导论》和Java实现红黑树的源码,