剖析Java 集合框架(八)-TreeMap

前面介绍过ConcurrentSkipListMap,支持排序(基于跳表),线程安全的Map。现在介绍一种同样支持排序,但是非线程安全,使用更频繁的类-TreeMap

红黑树

TreeMap支持排序的原理在于底层使用红黑树,本篇不详细手撕红黑树,简易的分析下原理。

下面基于《算法 4》定义的红黑树的特点,

  1. 根节点必为黑色;
  2. 节点分为红色或者黑色;
  3. 连接红色节点的两个子节点都为黑色(红黑树不会出现相邻的红色节点);
  4. 从任意节点出发,到其每个叶子节点的路径中包含相同数量的黑色节点;
  5. 新加入到红黑树的节点为红色节点;

为保证每次操作之后,这些特性不变,需要维护该平衡,该算法的的核心操作如下:
下面就插入的几种情况画图分析:

  1. 变色在不违反上述红黑树规则特点情况下,将红黑树某个 node 节点颜色由红变黑,或者由黑变红
    如果插入之后的父节点(10)和父节点同级的节点(20)都为红色,则将父级节点都变为黑的。父级节点的父级节点(19)变为红色,则以(19)节点为起点重新自平衡。
    红黑树-插入-变色
  2. 如果插入节点为左节点,则进行右旋。 右旋顺时针旋转两个节点,让一个节点被其左子节点取代,而该节点成为左子节点的右子节点。
    红黑树-插入-右旋

3.如果插入节点为右节点,则先对父级节点进行左旋,再对祖父节点进行右旋。 左旋: 逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点。

红黑树-插入-左右旋

TreeMap 源码分析

一 、TreeMap 数据结构

先看下 TreeMap 的定义

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
  1. TreeMap 继承了 AbstractMap,所以它是一个 Map,即一个 key-value 集合。并且它是一个有序的,通过 红黑树(R-B tree) 实现
  2. TreeMap 实现了 NavigableMap 接口,意味着它一系列的导航定位以及导航操作的方法。
  3. TreeMap 实现了 Cloneable 接口,可被克隆,实现了 Serializable 接口,可序列化。
    再来看下 TreeMap 的主要成员
    // 比较器 用于保存map的排序,为空时 使用key的自然顺序排序
    private final Comparator<? super K> comparator;
   // 红黑树的 根节点
    private transient Entry<K,V> root;
   // 红黑树中 节点个数 容器大小 
    private transient int size = 0;
   // 红黑树 调整次数 快速失败(fail-fast) 关键
    private transient int modCount = 0;
   // 定义的红黑树的节点颜色-红色
    private static final boolean RED   = false;
   // 定义的红黑树的节点颜色-黑色
    private static final boolean BLACK = true;

Entry是个内部类

static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;          // 当前节点的key
        V value;            // 当前节点的值
        Entry<K,V> left;        // 左子节点
        Entry<K,V> right;      // 右子节点
        Entry<K,V> parent;     // 父节点
        boolean color = BLACK; // 颜色

下面画下类图和数据结构图


TreeMap 类图

[图片上传失败...(image-fd9ac7-1609665042157)].png)

二、TreeMap 构造方法

TreeMap 有4个构造方法

// 默认构造方法, 按照 key 的自然顺序排序
    public TreeMap() {
        comparator = null;
    }

// 传入比较器的构造方法  按照该规则排序
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }

//传入一个map对象创建TreeMap,按照key的自然顺序排序
    public TreeMap(Map<? extends K, ? extends V> m) {
        comparator = null;
        putAll(m);
    }
// 遍历逐个插入
public void putAll(Map<? extends K, ? extends V> m) {
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        put(e.getKey(), e.getValue());
} 
// 传入一个按照特定方式排好序了的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) {
        }
    }

三、 put插入方法

插入是map的核心方法,TreeMap 的插入也就是向红黑树里面插入, 大概分为两大步,1.插入新值2.平衡红黑树
先来看第一步 插入新值:

public V put(K key, V value) {
    Entry<K,V> t = root;
    // 如果跟节点为空 新建个节点 并赋值给root
    if (t == null) {
        compare(key, key); // type (and possibly null) check
        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }

    int cmp;
    // 插入新值的 父节点
    Entry<K,V> parent;
    // 区分比较器:是用自定义的  还是使用key自带的
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        // 使用二分法查找
        do {
            parent = t;
            // 使用自定义比较器 进行对比遍历到的key和新key
            cmp = cpr.compare(key, t.key);
            // 对比结果小于0  说明往左子树继续查找
            if (cmp < 0)
                // 将当前节点置为 左子节点
                t = t.left;
            // 大于0  说明需要往右子树继续查找
            else if (cmp > 0)
                // 将当前节点置为 右子节点
                t = t.right;
            else
                // 找到了 直接更新改节点的value
                return t.setValue(value);
        } while (t != null);
    }
    else {
        if (key == null)
            throw new NullPointerException();
        // 使用key的比较器 步骤同上
        @SuppressWarnings("unchecked")
        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;
}

大概步骤如下:

  1. 根节点为空,则构建一个节点做完跟节点,并返回。
  2. 以根节点为起点,使用二分搜索法,找到一样的则替换值,并返回。
  3. 未找到则在正确位置的叶子节点插入子节点。
  4. 然后进行调整。

下面看下插入之后,自平衡红黑树

private void fixAfterInsertion(Entry<K,V> x) {
    // 新节点 都是红色
    x.color = RED;

    //只有父级节点为红色时  才需要调整
    while (x != null && x != root && x.parent.color == RED) {
        // 如果父节点 是左节点
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            // 获取父节点的父节点的右子节点 如果也是红色 则执行变色操作
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                // 将父节点和叔父节点设置为黑色
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                // 将祖父节点 设置为 红色
                setColor(parentOf(parentOf(x)), RED);
                // 将祖父节点置为当前节点 继续循环调整
                x = parentOf(parentOf(x));
            } else {
                // 如果当前节点为右节点
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    // 先对父节点左旋
                    rotateLeft(x);
                }
                // 对祖父节点进行右旋
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        // 父节点 是右节点
        } else {
            // 父节点 和 叔父节点都是 红色  变色操作
            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 {
                // 如果是当前节点是左节点  现在对父节点 进行右旋转
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                // 对祖父节点 进行左旋
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    // 跟节点 黑色
    root.color = BLACK;
}

就是根据父节点,叔父节点,祖父节点的位置和颜色,然后变色,左旋,右旋操作。

四、 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
    if (comparator != null)
        // 使用自定义的比较器  查找
        return getEntryUsingComparator(key);
    if (key == null)
        throw new NullPointerException();
      // 使用默认的比较器 查找
    @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
    // 从root开始遍历查找
    Entry<K,V> p = root;
    // 典型的二分查找 
    // 小: 则继续往左边找
    // 大:则继续往右边找
    // 相等: 返回
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0)
            p = p.left;
        else if (cmp > 0)
            p = p.right;
        else
            return p;
    }
    return null;
}

// 使用自定义的 比较器 也是使用而二分查找法
final Entry<K,V> getEntryUsingComparator(Object key) {
    @SuppressWarnings("unchecked")
        K k = (K) key;
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        Entry<K,V> p = root;
        while (p != null) {
            int cmp = cpr.compare(k, p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
    }
    return null;
}

五、TreeMap的遍历

TreeMap提供多种遍历方法:map.values(), map.keySet(),map.entrySet(),map.forEach(),都挺简单的遍历,特别的是还提供逆序/方向遍历,刚兴趣的同学可以去了解下。

总结

本文简单介绍了TreeMap基本特点和几个方法,没有说到删除方法,很多情况需要考虑,打算放到算法分析红黑树的时候再细讲。有错误或者不清楚的地方还请评论指出。

量变引发质变,经常进步一点点,期待蜕变的自己。

©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352

推荐阅读更多精彩内容