TreeMap 也是我们日常开发中比较常用的容器,它具有排序
的功能,先简单看下它的继承结构:
TreeMap 具有如下特点:
TreeMap
是一个双列集合,是Map
的子类,底层由红黑树
实现key
不能为空(因为key
要用来排序
),value
可以为空key
要么实现了Comparable
接口,要么创建TreeMap
对象时,指定Comparator
比较器- 元素会默认按照
key
大小顺序排序 key
如果重复,新值覆盖旧值
在探究TreeMap
之前,因为TreeMap
的底层是红黑树
,所以在看红黑树
之前,我们先了解下二叉搜索树
以及平衡二叉搜索树
.
1.二叉搜索树
我们先了解下树的基本概念:
◼ 节点、根节点、父节点、子节点、兄弟节点
◼ 一棵树可以没有任何节点,称为空树
◼ 一棵树可以只有 1 个节点,也就是只有根节点
◼ 子树、左子树、右子树
◼ 节点的度(degree):子树的个数
◼ 树的度:所有节点度中的最大值
◼ 叶子节点(leaf):度为 0 的节点
◼ 非叶子节点:度不为 0 的节点
二叉树的特点:
◼ 每个节点的度最大为 2(最多拥有 2 棵子树)
◼ 左子树和右子树是有顺序的
◼ 即使某节点只有一棵子树,也要区分左右子树
二叉搜索树特点:
◼ 任意一个节点的值都大于其左子树所有节点的值
◼ 任意一个节点的值都小于其右子树所有节点的值
◼ 它的左右子树也是一棵二叉搜索树
◼ 二叉搜索树可以大大提高搜索数据的效率
◼ 二叉搜索树存储的元素必须具备可比较性(比如 int、string, double)
◼ 如果是自定义类型,需要指定比较方式
查找过程:
- 从根节点开始,如果要查找的值等于根节点,直接返回
- 如果要查找的值小于根节点的值,则在左子树中递归查找
- 如果要查找的值大于根节点的值,则在右子树中递归查找
我们以查找13为例:
- 首先和根节点45比较,比45小,那么去根节点的左子树找
- 然后和12比较,比12大,然后去节点12的右子树找
- 然后和22比较,比22小,去22的左子树找
- 然后和13比较,和13相等,返回13
在线演示二叉搜索树
2. 平衡二叉搜索树
为了避免一个树出现"瘸子"的情况,在二叉搜索树的基础上产生了平衡二叉搜索树
,具有如下特点:
- 每个节点的平衡因子只可能是 1、0、-1(绝对值 ≤ 1,如果超过 1,称之为“失衡”)
- 每个节点的左右子树高度差不超过 1
平衡因子(Balance Factor):某结点的左右子树的高度差
在线演示二叉搜索树和平衡二叉搜索树
动画演示AVL树(平衡二叉搜索树)
比如:往树中添加 [1, 2, 3, 4, 5, 6, 7] 个数据
二叉搜索树:
树已经退化成链表了
平衡二叉搜索树:
2.1 二叉搜索树的旋转
在构建平衡二叉树的时候,当有新的元素节点插入时,需要检查插入后是否破坏了平衡,如果破坏了平衡,则需要通过旋转来维持平衡。
2.1.1 左旋
左旋就是将失衡节点
的右支往左拉,右子节点变成父节点,并把晋升之后多余的左子节点出让给降级节点的右子节点。
2.1.2 右旋
右旋就是将失衡节点
的左支往右拉,左子节点变成了父节点,并把晋升之后多余的右子节点出让给降级节点的左子节点。
2.2 二叉树的四种失衡情况
2.2.1 左左(LL)情况:右旋转(单旋)
左左(LL): 节点P的左子树的左子树导致P失衡
10是失衡节点,以10为基准右旋,这样失衡节点(10)及失衡节点的右子树(15)降下来,失衡节点的左子树(7,4,8,5)升上去。然后把升上去的右子树(8)出让给降级后的左子树。
2.2.2 右右(RR)情况:左旋转(单旋)
右右(RR): 节点P的右子树的右子树导致节点P失衡
11是失衡节点,以11为基准左旋,这样失衡节点(11)以及失衡节点的左子树(9)降下来,失衡节点的右子树(13,12,15,19)升上去。然后把升上去的左子树(12)出让给降级后的右子树。
可以分成两个步骤看:
2.2.3 左右(LR)情况:先左旋(RR)再右旋(LL)——双旋
左右情况:先让失衡节点的左子树左旋(不用出让节点),然后再让失衡节点右旋(出让节点,升上去的节点10出让给降级后左节点)
2.2.4 右左(RL)情况:先右旋(LL)再左旋(RR)——双旋
右左情况:先让失衡节点的右子树右旋(需要出让节点:将升上去之后的右子节点14出让给降级下来的左子节点), 然后再按照失衡节点左旋(不用出让节点)。
总结:左左和右右情况,只需要旋转一次即可,左右和右左需要旋转两次。
3.红黑树
红黑树也是一种自平衡的二叉搜索树
红黑树的性质:
- 节点是 或者
- 根节点是
- 叶子节点(外部节点,空节点)都是
- 节点的子节点都是 ( 节点的 parent 都是 )
- 从根节点到叶子节点的所有路径上不能有 2 个连续的 节点
- 从任一节点到叶子节点的所有路径都包含相同数目的 节点
注意:这里的叶子节点就是 null 节点,实际中我们并不会去管他。在实际开发中,我们可能就添加17, 33 这样的节点,至于它们下面的null 节点是不需要管的。
红黑树中所说的叶子节点和我们以前 BinarySerachTree , AVLTree 所说的叶子节点不一样,红黑树中的叶子节点是值这些假想的null节点。比如17 可能是AVL 树的叶子节点,但是在红黑树中,它下面的null才是叶子节点。
红黑树让那些度为0或者度为1的节点都变成度为2的节点,怎么变呢?就是添加 null 节点。这样就变成了 真二叉树
平衡二叉搜索树VS红黑树
平衡二叉搜索树
:
- 平衡标准比较严格:每个左右子树的高度差不超过1
- 搜索、添加、删除都是 O(logn) 复杂度,其中添加仅需 O(1) 次旋转调整、删除最多需要 O(logn) 次旋转调整
- 100W个节点,AVL树最大树高28
红黑树
:
- 平衡标准比较宽松:没有一条路径会大于其他路径的2倍
- 搜索、添加、删除都是 O(logn) 复杂度,其中添加、删除都仅需 O(1) 次旋转调整
- 100W个节点,红黑树最大树高40
总结
:
- 搜索的次数远远大于插入和删除,选择AVL树;搜索、插入、删除次数几乎差不多,选择红黑树
- 相对于AVL树来说,红黑树牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能要优于AVL树
- 红黑树的平均统计性能优于AVL树,实际应用中更多选择使用红黑树
4.TreeMap源码分析
4.1 类结构
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
/**
* 比较器,通过构造器传入
*/
private final Comparator<? super K> comparator;
/**
* 根节点
*/
private transient Entry<K,V> root;
/**
* 元素个数
*/
private transient int size = 0;
/**
* 集合都有的属性,表示修改次数
*/
private transient int modCount = 0;
/**
* 空参构造器,没有指定比较器
*/
public TreeMap() {
comparator = null;
}
/**
* 制定了比较器
*/
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
//其他构造器
//.........
/**
* 元素
*/
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; //节点颜色
//.....其他方法
}
}
TreeMap要想完成比较功能,必须满足以下要求:
- 要么通过构造器传入比较器
java.util.Comparator
接口 - 要么key实现了
java.lang.Comparable
接口
4.2 put()方法分析
测试数据:
Map<Integer, String> map = new TreeMap<>();
map.put(1005, "小王");
map.put(1001, "小明");
map.put(1003, "小刚");
map.put(1002, "小红");
map.put(1004, "小飞");
map.forEach((k, v) -> System.out.printf("%s = %s, ", k, v));
put源码分析:
public V put(K key, V value) {
//将根节点赋给t
Entry<K,V> t = root;
//如果t是null,说明这是第一次put元素
if (t == null) {
/**
* 自己和自己比较
* 1.判断key是否为空,如果为空,则抛出空指针异常
* 2.判断是否指定了Comparator比较器或者key是否实现了Comparable接口
*/
compare(key, key); // type (and possibly null) check
//创建根节点元素
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
//走下面的逻辑说明不是第一次put元素
int cmp;
Entry<K,V> parent;
//如果通过构造器制定了比较器,那么就用比较器进行比较,否则就使用Comparable比较
Comparator<? super K> cpr = comparator;
if (cpr != null) {
//请看下面Comparable的do-while分析,他们两个本质上是一样的逻辑
do {
parent = t;
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);
}
else { //使用Comparable比较
//如果key是null,抛出异常,说明TreeMap的key不允许为空
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
//整个do-while的逻辑就是在做一件事:那就是找到当前put节点的父节点
do {
//找到根节点(第一次t是根节点,经过循环后会用子节点逐级覆盖)
parent = t;
//当前传入的key和根节点的key比较
cmp = k.compareTo(t.key);
//如果cmp < 0, 说明当前key 小于 t.key
if (cmp < 0)
t = t.left;
//如果cmp > 0, 说明当前key 大于 t.key
else if (cmp > 0)
t = t.right;
//否则说明key的值一样,则用新值覆盖旧值
else
return t.setValue(value);
} while (t != null);
}
//创建新的节点元素,parent就是上面通过do-while找到的
Entry<K,V> e = new Entry<>(key, value, parent);
//如果 cmp < 0, 说明当前节点要放到父节点的左子节点
if (cmp < 0)
parent.left = e;
//如果 cmp > 0, 说明当前节点要放到父节点的右子节点
else
parent.right = e;
//红黑树的旋转和染色!!!
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
总结:
- TreeMap的key不允许为空,否则抛出空指针异常
- 整个do-while逻辑就是做一件事:找到当前put元素的父节点
4.2 get()方法分析
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
核心逻辑就是getEntry()
方法:
final Entry<K,V> getEntry(Object key) {
//如果我们通过构造器传入了比较器,那么就用传入的比较器进行比较
if (comparator != null)
return getEntryUsingComparator(key);
//如果key是null, 则抛出一样
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
//使用key的Comparable进行比较
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; //找到了
}
return null;
}
查找比较简单,就是按照二叉树的查找逻辑进行查找。
好了,关于TreeMap的介绍就到这里吧,其中关于红黑树的逻辑并没有详细说明,如果打算专门写一篇文章来记录红黑树。
5.总结
- TreeMap的 key 不允许为空
- TreeMap可以根据key实现排序,但必须满足以下两个条件之一:要么通过构造器制定Comparator比较器,要么key实现了Comparable接口,否则无法完成排序将报错
- get的时候也不能指定null作为key,否则将报错
- 底层是红黑树
限于作者水平,文中难免有错误之处,欢迎指正,勿喷,感谢感谢