【JDK源码】TreeMap

TreeMap与HashMap【JDK源码】HashMap

TreeMap

继承体系

在这里插入图片描述

  • SortedMap 是一个扩展自 Map 的一个接口,对该接口的实现要保证所有的 Key 是完全有序的(也就是说SortedMap规定了元素可以按key的大小来遍历)。这个顺序一般是指 Key 的自然序(实现 Comparable 接口)或在创建 SortedMap 时指定一个比较器(Comparator)。当我们元素迭代时,就可以按序访问其中的元素。
  • NavigableMap 是 JDK 1.6 之后新增的接口,扩展了 SortedMap 接口,提供了一些导航方法。 descendingMap() 和 descendingKeySet() 则会获取和原来的顺序相反的集合,集合中的元素则是同样的引用,在该视图上的修改会影响到原始的数据。NavigableMap 可以按照 Key 的升序或降序进行访问和遍历。

基本属性

/**
 * 比较器,因为TreeMap是有序的,通过comparator接口我们可以对TreeMap的内部排序进行控制
 */
private final Comparator<? super K> comparator;

/**
 *TreeMap红-黑节点,为TreeMap的内部类 root红黑树的根节点
 */
private transient Entry<K,V> root;

/**
 * TreeMap中存放的键值对的数量
 */
private transient int size = 0;

/**
 * 修改的次数
 */
private transient int modCount = 0;

(1)comparator

按key的大小排序有两种方式,一种是key实现Comparable接口,一种方式通过构造方法传入比较器

(2)root

根节点,TreeMap没有桶的概念,所有的元素都存储在一颗树中。

构造方法

/**
 * 默认构造方法,比较器为null,那么会使用key的比较器,也就意味着key必须实现Comparable 接口,否则在比较的时候就会出现异常
 */
public TreeMap() {
    
    
    comparator = null;
}

/**
 * 使用传入的comparator比较两个key的大小
 */
public TreeMap(Comparator<? super K> comparator) {
    
    
    this.comparator = comparator;
}

/**
 * key必须实现Comparable接口,把传入map中的所有元素保存到新的TreeMap中
 */
public TreeMap(Map<? extends K, ? extends V> m) {
    
    
    comparator = null;
    putAll(m);
}

/**
 * 使用传入map的比较器,并把传入map中的所有元素保存到新的TreeMap中
 */
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构造方法主要分成两类,一类是使用comparator比较器,一类是key必须实现Comparable接口。构造方法中比较器的值为null,采用自然排序的方法,如果指定了比较器则称之为定制排序

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

数据结构

也叫红黑树结构 不了解树的可以去复习复习数据结构尚硅谷韩顺平讲数据结构
以及后面的二叉排序树与其删除

static final class Entry<K,V> implements Map.Entry<K,V> {
    
    
    /**
     * 键
     */
    K key;
    /**
     * 值
     */
    V value;
    /**
     * 左结点
     */
    Entry<K,V> left;
    /**
     * 右结点
     */
    Entry<K,V> right;
    /**
     * 父结点
     */
    Entry<K,V> parent;
    /**
     * 结点颜色 默认为黑 只有两种颜色,红色和黑色
     */
    boolean color = BLACK;
}

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

put()

public V put(K key, V value) {
    
    
    Entry<K,V> t = root;
    //如果根结点为null,还没建立
    if (t == null) {
    
    
        //官方给的是:类型(可能为null)检查
        compare(key, key); // type (and possibly null) check
        //制造一个根结点,默认为黑
        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    //定义一个cmp,这个变量用来进行二分查找时的比较
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths 拆分比较器和可比较路径
    //也就是 cpr表示有无自己定义的排序规则,分两种情况遍历执行,主要目的就是找到要插入结点的父结点
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
    
    
        do {
    
    
            //存取要插入结点的父结点
            parent = t;
            //比较新的key与根结点key的大小,相当于维护了二叉排序树
            cmp = cpr.compare(key, t.key);
            //小的就放左边
            if (cmp < 0)
                t = t.left;
            //大的就放右边
            else if (cmp > 0)
                t = t.right;
            //一样就覆盖,并返回旧的值
            else
                return t.setValue(value);
        } while (t != null);
    }
    //如果比较器为空,则使用key作为比较器进行比较
    else {
    
    
        //这里要求key不能为空,并且必须实现Comparable接口
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        //类型转换,也就相当于实现Comparable接口
        Comparable<? super K> k = (Comparable<? super K>) key;
        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);
    }
    //构建新的结点,其父结点就是上面找到的
    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;
}

compare

//比较方法,如果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);
}

除去调整红黑树的部分,其余的都很简单,就是搜索二叉树的插入过程

平衡红黑树

fixAfterInsertion

private void fixAfterInsertion(Entry<K,V> x) {
    
    
    //插入结点的颜色默认是红色
    x.color = RED;
    //非空,非根结点,父结点为红结点,否则不用操作
    while (x != null && x != root && x.parent.color == RED) {
    
    
        //判断x父结点是否是x爷爷结点的左节结点
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
    
    
            //找到爷爷结点的右结点给到y
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            //如果x的父结点的兄弟结点y是红色,则x的父结点肯定也是红色的
            if (colorOf(y) == RED) {
    
    
                //父结点和叔结节点都为红色,此时通过变色即可实现平衡
                //x的父结点设置为黑色
                setColor(parentOf(x), BLACK);
                //x的父结点的兄弟结点y也设置成黑色
                setColor(y, BLACK);
                //x的爷爷结点设置为红色
                setColor(parentOf(parentOf(x)), RED);
                //将x的爷爷结点重置给x
                x = parentOf(parentOf(x));
                //如果x的父结点和叔父结点是黑色
            } else {
    
    
                //如果x是父结点的右结点
                if (x == rightOf(parentOf(x))) {
    
    
                    //将x的父结点重置给x
                    x = parentOf(x);
                    //然后左旋
                    rotateLeft(x);
                }
                //设置x的父结点为黑色
                setColor(parentOf(x), BLACK);
                //设置x的爷爷结点为红色
                setColor(parentOf(parentOf(x)), RED);
                //将x的爷爷结点右旋
                rotateRight(parentOf(parentOf(x)));
            }
            //x父结点是x爷爷结点的右节结点
        } else {
    
    
            //找到爷爷结点的左结点给到y
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            //如果父结点和叔结节点都为红色,此时通过变色即可实现平衡 和上面一样
            if (colorOf(y) == RED) {
    
    
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
                //都为黑
            } else {
    
    
                //如果x是父结点的左结点
                if (x == leftOf(parentOf(x))) {
    
    
                    //将父结点重置给x
                    x = parentOf(x);
                    //右旋x
                    rotateRight(x);
                }
                //设置x的父结点为黑色
                setColor(parentOf(x), BLACK);
                //设置x的爷爷结点为红色
                setColor(parentOf(parentOf(x)), RED);
                //左旋爷爷结点
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    //根结点一定是黑色
    root.color = BLACK;
}
  • 设置颜色左右旋的目的就是保证红黑树的规则

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

  1. 每个节点只能是红色或者黑色
  2. 根节点永远是黑色的
  3. 每个叶子节点(NIL)是黑色。(注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!)
  4. 如果一个节点是红色的,则它的子节点必须是黑色的。
  5. 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点

左旋

在这里插入图片描述

  • 其实就是 P结点为树的结构改变,将P结点的右结点R作为父结点,将R的左结点给到P结点的右结点,将P结点给到R结点的左结点,重新变成以R结点为树节点的二叉树
/** From CLR */
/**
 * 
 *  以p为树 左旋
 */
private void rotateLeft(Entry<K,V> p) {
    
    
    if (p != null) {
    
    
        //取出P的右结点给r
        Entry<K,V> r = p.right;
        //将r的左结点给p的右结点,其实就是p的右结点指向p的右结点的左结点
        p.right = r.left;
        //不为空则反过来指向
        if (r.left != null)
            r.left.parent = p;
        //将r的父结点指向p的父结点
        r.parent = p.parent;
        //如果p本来就是整个树的根结点,则将r作为根结点
        if (p.parent == null)
            root = r;
        //将r作为此树的根结点
        else if (p.parent.left == p)
            //原来p是父结点的左结点  就将r作为p父结点的左结点
            p.parent.left = r;
        else
            p.parent.right = r;
        //再将r左结点指向p
        r.left = p;
        p.parent = r;
    }
}

右旋

在这里插入图片描述

  • 其实就是 P结点为树的结构改变,将P结点的左结点R作为父结点,将L的右结点给到P结点的左结点,将P结点给到L结点的右结点,重新变成以L结点为树节点的二叉树
/** From CLR */
/**
 * 
 * 以p为树 右旋  原理和左旋一样
 */
private void rotateRight(Entry<K,V> p) {
    
    
    if (p != null) {
    
    
        Entry<K,V> l = p.left;
        p.left = l.right;
        if (l.right != null) l.right.parent = p;
        l.parent = p.parent;
        if (p.parent == null)
            root = l;
        else if (p.parent.right == p)
            p.parent.right = l;
        else p.parent.left = l;
        l.right = p;
        p.parent = l;
    }
}

get()

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

final Entry<K,V> getEntry(Object key) {
    
    
    // Offload comparator-based version for sake of performance
    //如果comparator不为空,使用comparator的版本获取元素
    if (comparator != null)
        //代码和根节点遍历一样
        return getEntryUsingComparator(key);
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
        //类型转换 就是去实现Comparable
        Comparable<? super K> k = (Comparable<? super K>) key;
    //从根节点开始遍历
    Entry<K,V> p = root;
    while (p != null) {
    
    
        int cmp = k.compareTo(p.key);
        //小就从左边找  因为在put的时候满足二叉排序树
        if (cmp < 0)
            p = p.left;
        //大就从右边找
        else if (cmp > 0)
            p = p.right;
        else
            return p;
    }
    return null;
}

remove()

public V remove(Object key) {
    
    
    Entry<K,V> p = getEntry(key);
    if (p == null)
        return null;

    V oldValue = p.value;
    deleteEntry(p);//删除结点
    return oldValue; // 返回原来的值
}

deleteEntry 删除结点 其实就是一个二叉排序树的删除结点

/**
 * Delete node p, and then rebalance the tree.
 * 删除结点p, 重新平衡树木
 */
private void deleteEntry(Entry<K,V> p) {
    
    
    modCount++;
    size--;

    // If strictly internal, copy successor's element to p and then make p
    // point to successor.
    //p结点左右不为空
    if (p.left != null && p.right != null) {
    
    
        //寻找到p的右结点中最小的结点
        Entry<K,V> s = successor(p);
        //将最小结点s的key和vaule给p结点
        p.key = s.key;
        p.value = s.value;
        p = s;
        // 这种情况实际上并没有删除p节点,而是把p节点的值改了,实际删除的是p的后继节点
    } // p has 2 children

    // Start fixup at replacement node, if it exists.
    // 如果原来的当前节点(p)有2个子节点,则当前节点已经变成原来p的右子树中的最小节点了,也就是说其没有左子节点了
    // 到这一步,p肯定只有一个子节点了
    //如果当前节点有子节点,则用子节点替换当前节点
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);


    if (replacement != null) {
    
    
        // Link replacement to parent
        // 把替换节点直接放到当前节点的位置上(相当于删除了p,并把替换节点移动过来了)
        replacement.parent = p.parent;
        if (p.parent == null)
            root = replacement;
        else if (p == p.parent.left)
            p.parent.left  = replacement;
        else
            p.parent.right = replacement;

        // Null out links so they are OK to use by fixAfterDeletion.
        // 将p的各项属性都设为空 方便gc
        p.left = p.right = p.parent = null;

        // Fix replacement
        // 如果p是黑节点,则需要再平衡
        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;
        }
    }
}

总结TreeMap特性

(1)TreeMap的存储结构只有一颗红黑树;

(2)TreeMap中的元素是有序的,按key的顺序排列;

(3)TreeMap比HashMap要慢一些,因为HashMap前面还做了一层桶,寻找元素要快很多;

(4)TreeMap没有扩容的概念;

(5)TreeMap可以按范围查找元素,查找最近的元素;

参考文章
Java集合,TreeMap底层实现和原理
路飞:JDK集合源码之TreeMap解析
史上最清晰的红黑树讲解

猜你喜欢

转载自blog.csdn.net/qq_51998352/article/details/121162527