欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

简介

TreeMap使用红黑树存储元素,可以保证元素按key值的大小进行遍历。

继承体系

TreeMap实现了Map、SortedMap、NavigableMap、Cloneable、Serializable等接口。

SortedMap规定了元素可以按key的大小来遍历,它定义了一些返回部分map的方法。

public interface SortedMap<K,V> extends Map<K,V> {
// key的比较器
Comparator<? super K> comparator();
// 返回fromKey(包含)到toKey(不包含)之间的元素组成的子map
SortedMap<K,V> subMap(K fromKey, K toKey);
// 返回小于toKey(不包含)的子map
SortedMap<K,V> headMap(K toKey);
// 返回大于等于fromKey(包含)的子map
SortedMap<K,V> tailMap(K fromKey);
// 返回最小的key
K firstKey();
// 返回最大的key
K lastKey();
// 返回key集合
Set<K> keySet();
// 返回value集合
Collection<V> values();
// 返回节点集合
Set<Map.Entry<K, V>> entrySet();
}

NavigableMap是对SortedMap的增强,定义了一些返回离目标key最近的元素的方法。

public interface NavigableMap<K,V> extends SortedMap<K,V> {
// 小于给定key的最大节点
Map.Entry<K,V> lowerEntry(K key);
// 小于给定key的最大key
K lowerKey(K key);
// 小于等于给定key的最大节点
Map.Entry<K,V> floorEntry(K key);
// 小于等于给定key的最大key
K floorKey(K key);
// 大于等于给定key的最小节点
Map.Entry<K,V> ceilingEntry(K key);
// 大于等于给定key的最小key
K ceilingKey(K key);
// 大于给定key的最小节点
Map.Entry<K,V> higherEntry(K key);
// 大于给定key的最小key
K higherKey(K key);
// 最小的节点
Map.Entry<K,V> firstEntry();
// 最大的节点
Map.Entry<K,V> lastEntry();
// 弹出最小的节点
Map.Entry<K,V> pollFirstEntry();
// 弹出最大的节点
Map.Entry<K,V> pollLastEntry();
// 返回倒序的map
NavigableMap<K,V> descendingMap();
// 返回有序的key集合
NavigableSet<K> navigableKeySet();
// 返回倒序的key集合
NavigableSet<K> descendingKeySet();
// 返回从fromKey到toKey的子map,是否包含起止元素可以自己决定
NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive,
K toKey, boolean toInclusive);
// 返回小于toKey的子map,是否包含toKey自己决定
NavigableMap<K,V> headMap(K toKey, boolean inclusive);
// 返回大于fromKey的子map,是否包含fromKey自己决定
NavigableMap<K,V> tailMap(K fromKey, boolean inclusive);
// 等价于subMap(fromKey, true, toKey, false)
SortedMap<K,V> subMap(K fromKey, K toKey);
// 等价于headMap(toKey, false)
SortedMap<K,V> headMap(K toKey);
// 等价于tailMap(fromKey, true)
SortedMap<K,V> tailMap(K fromKey);
}

存储结构

TreeMap只使用到了红黑树,所以它的时间复杂度为O(log n),我们再来回顾一下红黑树的特性。

(1)每个节点或者是黑色,或者是红色。

(2)根节点是黑色。

(3)每个叶子节点(NIL)是黑色。(注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!)

(4)如果一个节点是红色的,则它的子节点必须是黑色的。

(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

源码解析

属性

/**
* 比较器,如果没传则key要实现Comparable接口
*/
private final Comparator<? super K> comparator; /**
* 根节点
*/
private transient Entry<K,V> root; /**
* 元素个数
*/
private transient int size = 0; /**
* 修改次数
*/
private transient int modCount = 0;

(1)comparator

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

(2)root

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

Entry内部类

存储节点,典型的红黑树结构。

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;
}

构造方法

/**
* 默认构造方法,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) {
}
}

构造方法主要分成两类,一类是使用comparator比较器,一类是key必须实现Comparable接口。

其实,笔者认为这两种比较方式可以合并成一种,当没有传comparator的时候,可以用以下方式来给comparator赋值,这样后续所有的比较操作都可以使用一样的逻辑处理了,而不用每次都检查comparator为空的时候又用Comparable来实现一遍逻辑。

// 如果comparator为空,则key必须实现Comparable接口,所以这里肯定可以强转
// 这样在构造方法中统一替换掉,后续的逻辑就都一致了
comparator = (k1, k2) -> ((Comparable<? super K>)k1).compareTo(k2);

get(Object key)方法

获取元素,典型的二叉查找树的查找方法。

public V get(Object key) {
// 根据key查找元素
Entry<K,V> p = getEntry(key);
// 找到了返回value值,没找到返回null
return (p==null ? null : p.value);
} final Entry<K,V> getEntry(Object key) {
// 如果comparator不为空,使用comparator的版本获取元素
if (comparator != null)
return getEntryUsingComparator(key);
// 如果key为空返回空指针异常
if (key == null)
throw new NullPointerException();
// 将key强转为Comparable
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
// 从根元素开始遍历
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
// 如果小于0从左子树查找
p = p.left;
else if (cmp > 0)
// 如果大于0从右子树查找
p = p.right;
else
// 如果相等说明找到了直接返回
return p;
}
// 没找到返回null
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)
// 如果小于0从左子树查找
p = p.left;
else if (cmp > 0)
// 如果大于0从右子树查找
p = p.right;
else
// 如果相等说明找到了直接返回
return p;
}
}
// 没找到返回null
return null;
}

(1)从root遍历整个树;

(2)如果待查找的key比当前遍历的key小,则在其左子树中查找;

(3)如果待查找的key比当前遍历的key大,则在其右子树中查找;

(4)如果待查找的key与当前遍历的key相等,则找到了该元素,直接返回;

(5)从这里可以看出是否有comparator分化成了两个方法,但是内部逻辑一模一样,因此可见笔者comparator = (k1, k2) -> ((Comparable<? super K>)k1).compareTo(k2);这种改造的必要性。


我是一条美丽的分割线,前方高能,请做好准备。


特性再回顾

(1)每个节点或者是黑色,或者是红色。

(2)根节点是黑色。

(3)每个叶子节点(NIL)是黑色。(注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!)

(4)如果一个节点是红色的,则它的子节点必须是黑色的。

(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

左旋

左旋,就是以某个节点为支点向左旋转。

整个左旋过程如下:

(1)将 y的左节点 设为 x的右节点,即将 β 设为 x的右节点;

(2)将 x 设为 y的左节点的父节点,即将 β的父节点 设为 x;

(3)将 x的父节点 设为 y的父节点;

(4)如果 x的父节点 为空节点,则将y设置为根节点;如果x是它父节点的左(右)节点,则将y设置为x父节点的左(右)节点;

(5)将 x 设为 y的左节点;

(6)将 x的父节点 设为 y;

让我们来看看TreeMap中的实现:

/**
* 以p为支点进行左旋
* 假设p为图中的x
*/
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
// p的右节点,即y
Entry<K,V> r = p.right; // (1)将 y的左节点 设为 x的右节点
p.right = r.left; // (2)将 x 设为 y的左节点的父节点(如果y的左节点存在的话)
if (r.left != null)
r.left.parent = p; // (3)将 x的父节点 设为 y的父节点
r.parent = p.parent; // (4)...
if (p.parent == null)
// 如果 x的父节点 为空,则将y设置为根节点
root = r;
else if (p.parent.left == p)
// 如果x是它父节点的左节点,则将y设置为x父节点的左节点
p.parent.left = r;
else
// 如果x是它父节点的右节点,则将y设置为x父节点的右节点
p.parent.right = r; // (5)将 x 设为 y的左节点
r.left = p; // (6)将 x的父节点 设为 y
p.parent = r;
}
}

右旋

右旋,就是以某个节点为支点向右旋转。

整个右旋过程如下:

(1)将 x的右节点 设为 y的左节点,即 将 β 设为 y的左节点;

(2)将 y 设为 x的右节点的父节点,即 将 β的父节点 设为 y;

(3)将 y的父节点 设为 x的父节点;

(4)如果 y的父节点 是 空节点,则将x设为根节点;如果y是它父节点的左(右)节点,则将x设为y的父节点的左(右)节点;

(5)将 y 设为 x的右节点;

(6)将 y的父节点 设为 x;

让我们来看看TreeMap中的实现:

/**
* 以p为支点进行右旋
* 假设p为图中的y
*/
private void rotateRight(Entry<K,V> p) {
if (p != null) {
// p的左节点,即x
Entry<K,V> l = p.left; // (1)将 x的右节点 设为 y的左节点
p.left = l.right; // (2)将 y 设为 x的右节点的父节点(如果x有右节点的话)
if (l.right != null) l.right.parent = p; // (3)将 y的父节点 设为 x的父节点
l.parent = p.parent; // (4)...
if (p.parent == null)
// 如果 y的父节点 是 空节点,则将x设为根节点
root = l;
else if (p.parent.right == p)
// 如果y是它父节点的右节点,则将x设为y的父节点的右节点
p.parent.right = l;
else
// 如果y是它父节点的左节点,则将x设为y的父节点的左节点
p.parent.left = l; // (5)将 y 设为 x的右节点
l.right = p; // (6)将 y的父节点 设为 x
p.parent = l;
}
}

未完待续,下一节我们一起探讨红黑树插入元素的操作。

现在公众号文章没办法留言了,如果有什么疑问或者建议请直接在公众号给我留言。


欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

死磕 java集合之TreeMap源码分析(一)- 内含红黑树分析全过程的更多相关文章

  1. 死磕 java集合之TreeMap源码分析(四)-内含彩蛋

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 二叉树的遍历 我们知道二叉查找树的遍历有前序遍历.中序遍历.后序遍历. (1)前序遍历,先遍历 ...

  2. 死磕 java集合之TreeMap源码分析(三)- 内含红黑树分析全过程

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 删除元素 删除元素本身比较简单,就是采用二叉树的删除规则. (1)如果删除的位置有两个叶子节点 ...

  3. 死磕 java集合之TreeMap源码分析(二)- 内含红黑树分析全过程

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 插入元素 插入元素,如果元素在树中存在,则替换value:如果元素不存在,则插入到对应的位置, ...

  4. 死磕 java集合之DelayQueue源码分析

    问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于 ...

  5. 死磕 java集合之PriorityBlockingQueue源码分析

    问题 (1)PriorityBlockingQueue的实现方式? (2)PriorityBlockingQueue是否需要扩容? (3)PriorityBlockingQueue是怎么控制并发安全的 ...

  6. 死磕 java集合之PriorityQueue源码分析

    问题 (1)什么是优先级队列? (2)怎么实现一个优先级队列? (3)PriorityQueue是线程安全的吗? (4)PriorityQueue就有序的吗? 简介 优先级队列,是0个或多个元素的集合 ...

  7. 死磕 java集合之CopyOnWriteArraySet源码分析——内含巧妙设计

    问题 (1)CopyOnWriteArraySet是用Map实现的吗? (2)CopyOnWriteArraySet是有序的吗? (3)CopyOnWriteArraySet是并发安全的吗? (4)C ...

  8. 死磕 java集合之LinkedHashSet源码分析

    问题 (1)LinkedHashSet的底层使用什么存储元素? (2)LinkedHashSet与HashSet有什么不同? (3)LinkedHashSet是有序的吗? (4)LinkedHashS ...

  9. 死磕 java集合之ConcurrentHashMap源码分析(三)

    本章接着上两章,链接直达: 死磕 java集合之ConcurrentHashMap源码分析(一) 死磕 java集合之ConcurrentHashMap源码分析(二) 删除元素 删除元素跟添加元素一样 ...

随机推荐

  1. Spark内核

    一些名词概念 AM : ApplicationMaster RM : ResourceManager NM : NodeManager Backend : 后台 RpcEnv : RPC 进程和进程的 ...

  2. eclipse的常用设置

    参考文档:https://www.cnblogs.com/maoniu602/p/3585049.html 版本和jdk的版本搭配问题 eclipse和JDK版本应搭配,而且,若使用32位则都使用32 ...

  3. 一种DTO的规划方案

    现在以网页发布的软件非常普遍,叫BS模式.前后端分离也是大趋势,或者说逐渐普及开来,深受前后端程序员的喜爱,我还是习惯以程序员来泛称所有软件制作者.后端需要把数据传送给前端,往往是通过DTO的序列化来 ...

  4. 第三次作业-结对编程(wordcount)

    GIT地址 https://github.com/gentlemanzq/WordCount.git GIT用户名  gentlemanzq 结对伙伴博客地址 https://home.cnblogs ...

  5. Date动态获取时间

    ·getDate            |  根据本地时间获取当前日期(本月的几号) ·getDay             |  根据本地时间获取今天是星期几(0-Sunday,1-Monday.. ...

  6. Python数据处理PDF

    Python数据处理(高清版)PDF 百度网盘 链接:https://pan.baidu.com/s/1h8a5-iUr4mF7cVujgTSGOA 提取码:6fsl 复制这段内容后打开百度网盘手机A ...

  7. fiddler 应用

    一   pc 端抓取 例:本地调试代码,转换域名,请求网络数据 1:设置代理,以smart header 为例 ip为 127.0.0.1 端口与自己的fillder一致,注意将不代理的地址列表做修改 ...

  8. 错误提示:Dynamic Performance Tables not accessible, Automatic Statistics Disabled for this session You can disable statistics in the preference menu,or obtanin select priviliges on the v$session,v$sess

    1.错误提示:Dynamic Performance Tables not accessible, Automatic Statistics Disabled for this session You ...

  9. C# 使用WinApi操作剪切板Clipboard

    前言: 最近正好写一个程序,需要操作剪切板 功能很简单,只需要从剪切板内读取字符串,然后清空剪切板,然后再把字符串导入剪切板 我想当然的使用我最拿手的C#来完成这项工作,原因无他,因为.Net框架封装 ...

  10. 玩转vue前进刷新,后退不刷新and按需刷新

    大白萝卜小课堂开讲了!带你玩转vue前进后退按需刷新! 用vue做后台管理项目,特别是有列表页.列表数据详情页.列表数据修改页功能的码友们,几乎都被vue前进后退都刷新的逻辑坑过,本萝卜更是! 萝卜的 ...