-
【集合】TreeMap底层原理
2020-02-19 20:14:021 前言 本人使用的是jdk1.8版本。...学过数据结构的都知道二叉查找树是一种有序树,即进行中序遍历可以得到一组有序序列,所以TreeMap也是有序的Map集合。 在红黑树的加持下,TreeMap的众多方...1 前言
本人使用的是jdk1.8版本。
2 Map集合继承结构
3 底层原理
TreeMap底层使用红黑树的结构进行数据的增删改查,红黑树是一种自平衡的二叉查找树,想了解红黑树推荐看看这篇博文:30张图带你彻底理解红黑树。学过数据结构的都知道二叉查找树是一种有序树,即进行中序遍历可以得到一组有序序列,所以TreeMap也是有序的Map集合。
在红黑树的加持下,TreeMap的众多方法,如:containsKey、get、put和reomve,都能保证log(n)的时间复杂度,这个效率可以说是相当高了。
/** * A Red-Black tree based {@link NavigableMap} implementation. * The map is sorted according to the {@linkplain Comparable natural * ordering} of its keys, or by a {@link Comparator} provided at map * creation time, depending on which constructor is used. * * <p>This implementation provides guaranteed log(n) time cost for the * {@code containsKey}, {@code get}, {@code put} and {@code remove} * operations. Algorithms are adaptations of those in Cormen, Leiserson, and * Rivest's <em>Introduction to Algorithms</em>. */ 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; // 根节点 ...... }
4 有序性如何保证
TreeMap使用两种方法来保证有序性:Comparator和Comparable。
4.1 Comparator
你可以把它看做一个比较器,它的compare方法可以比较两个传入类型的对象,至于比较的规则是你实现这个接口后自己重写。从上面的代码看到,TreeMap中有一个全局comparator属性,你可以在构造其中传入自己的实现类。后面再put、get时就会调用comparator的compare方法来比较你的key和已存在的key,以此来决定存或取的位置。
public interface Comparator<T> { int compare(T o1, T o2); ...... }
4.2 Comparable
TreeMap规定,put、get和remove等方法传入的参数key必须是实现了Comparable接口的,否则在调用这些方法的时候会抛出类型转换的异常,因为要调用compareTo方法就必须把key强制转换成Comparable类型,即:(Comparable<? super K>)key。
所以这种比较方式就是:key1.compareTo(key2)。
public interface Comparable<T> { public int compareTo(T o); ...... }
4.3 两种比较策略?
当然,TreeMap中不可能同时使用两种策略,而是先使用comparator属性去比较,若comparator为空(没有在构造方法中传入实例)则采用Comparable策略。代码如下:
@SuppressWarnings("unchecked") final int compare(Object k1, Object k2) { return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2) : comparator.compare((K)k1, (K)k2); }
5 方法介绍
5.1 put()
就是按排序二叉树的方式插入节点,插入key比遍历节点的key小就往左边,大就往右,相等就替换。在插入结束后会对整个红黑树进行调整,已使它达到基本平衡,保证操作效率。
public V put(K key, V value) { Entry<K,V> t = root; if (t == null) { compare(key, key); // 若两种比较策略都没有使用,则抛出异常 root = new Entry<>(key, value, null); size = 1; modCount++; return null; } int cmp; Entry<K,V> parent; // split comparator and comparable paths Comparator<? super K> cpr = comparator; if (cpr != null) { // 使用comparator策略 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); } // 使用comparable策略 else { if (key == null) throw new NullPointerException(); @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; }
5.2 get()
与put一样的道理,查找key比遍历节点的key小,就往左边找,否则往右边,相等直接返回。同样要判断两种比较策略。
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; 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; }
5.3 remove()
remove也是一样的道理,先找到,后移除,不过移除后要对红黑树进行调整。
-
Java 018 集合 Map集合(功能、LinkedHashMap、TreeMap、遍历)、Collection工具类方法、斗地主案例
2018-11-06 22:08:36 -
TreeMap原理实现及常用方法
2020-09-20 22:39:43遍历 八. 总结 前面我们分别讲了Map接口的两个实现类HashMap和LinkedHashMap,本章我们讲一下Map接口另一个重要的实现类TreeMap,TreeMap或许不如HashMap那么常用,但存在即合理,它也有自己的应用场景,TreeMap...目录
前面我们分别讲了Map接口的两个实现类HashMap和LinkedHashMap,本章我们讲一下Map接口另一个重要的实现类TreeMap,TreeMap或许不如HashMap那么常用,但存在即合理,它也有自己的应用场景,TreeMap可以实现元素的自动排序。
一. TreeMap概述
- TreeMap存储K-V键值对,通过红黑树(R-B tree)实现;
- TreeMap继承了NavigableMap接口,NavigableMap接口继承了SortedMap接口,可支持一系列的导航定位以及导航操作的方法,当然只是提供了接口,需要TreeMap自己去实现;
- TreeMap实现了Cloneable接口,可被克隆,实现了Serializable接口,可序列化;
- TreeMap因为是通过红黑树实现,红黑树结构天然支持排序,默认情况下通过Key值的自然顺序进行排序;
二. 红黑树回顾
因为TreeMap的存储结构是红黑树,我们回顾一下红黑树的特点以及基本操作,红黑树的原理可参考关于红黑树(R-B tree)原理,看这篇如何。下图为典型的红黑树:
红黑树规则特点:
- 节点分为红色或者黑色;
- 根节点必为黑色;
- 叶子节点都为黑色,且为null;
- 连接红色节点的两个子节点都为黑色(红黑树不会出现相邻的红色节点);
- 从任意节点出发,到其每个叶子节点的路径中包含相同数量的黑色节点;
- 新加入到红黑树的节点为红色节点;
红黑树自平衡基本操作:
- 变色:在不违反上述红黑树规则特点情况下,将红黑树某个node节点颜色由红变黑,或者由黑变红;
- 左旋:逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点
- 右旋:顺时针旋转两个节点,让一个节点被其左子节点取代,而该节点成为左子节点的右子节点
三. TreeMap构造
我们先看一下TreeMap中主要的成员变量
/** * 我们前面提到TreeMap是可以自动排序的,默认情况下comparator为null,这个时候按照key的自然顺序进行排 * 序,然而并不是所有情况下都可以直接使用key的自然顺序,有时候我们想让Map的自动排序按照我们自己的规则, * 这个时候你就需要传递Comparator的实现类 */ private final Comparator<? super K> comparator; /** * TreeMap的存储结构既然是红黑树,那么必然会有唯一的根节点。 */ private transient Entry<K,V> root; /** * Map中key-val对的数量,也即是红黑树中节点Entry的数量 */ private transient int size = 0; /** * 红黑树结构的调整次数 */ private transient int modCount = 0;
上面的主要成员变量根节点root是Entry类的实体,我们来看一下Entry类的源码
static final class Entry<K,V> implements Map.Entry<K,V> { //key,val是存储的原始数据 K key; V value; //定义了节点的左孩子 Entry<K,V> left; //定义了节点的右孩子 Entry<K,V> right; //通过该节点可以反过来往上找到自己的父亲 Entry<K,V> parent; //默认情况下为黑色节点,可调整 boolean color = BLACK; /** * 构造器 */ 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; } public boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<?,?> e = (Map.Entry<?,?>)o; return valEquals(key,e.getKey()) && valEquals(value,e.getValue()); } public int hashCode() { int keyHash = (key==null ? 0 : key.hashCode()); int valueHash = (value==null ? 0 : value.hashCode()); return keyHash ^ valueHash; } public String toString() { return key + "=" + value; } }
Entry静态内部类实现了Map的内部接口Entry,提供了红黑树存储结构的java实现,通过left属性可以建立左子树,通过right属性可以建立右子树,通过parent可以往上找到父节点。
大体的实现结构图如下:
TreeMap构造函数:
//默认构造函数,按照key的自然顺序排列 public TreeMap() {comparator = null;} //传递Comparator具体实现,按照该实现规则进行排序 public TreeMap(Comparator<? super K> comparator) {this.comparator = comparator;} //传递一个map实体构建TreeMap,按照默认规则排序 public TreeMap(Map<? extends K, ? extends V> m) { comparator = null; putAll(m); } //传递一个map实体构建TreeMap,按照传递的map的排序规则进行排序 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方法
put方法为Map的核心方法,TreeMap的put方法大概流程如下:
我们来分析一下源码
public V put(K key, V value) { Entry<K,V> t = root; /** * 如果根节点都为null,还没建立起来红黑树,我们先new Entry并赋值给root把红黑树建立起来,这个时候红 * 黑树中已经有一个节点了,同时修改操作+1。 */ if (t == null) { compare(key, key); root = new Entry<>(key, value, null); size = 1; modCount++; return null; } /** * 如果节点不为null,定义一个cmp,这个变量用来进行二分查找时的比较;定义parent,是new Entry时必须 * 要的参数 */ int cmp; Entry<K,V> parent; // cpr表示有无自己定义的排序规则,分两种情况遍历执行 Comparator<? super K> cpr = comparator; if (cpr != null) { /** * 从root节点开始遍历,通过二分查找逐步向下找 * 第一次循环:从根节点开始,这个时候parent就是根节点,然后通过自定义的排序算法 * cpr.compare(key, t.key)比较传入的key和根节点的key值,如果传入的key<root.key,那么 * 继续在root的左子树中找,从root的左孩子节点(root.left)开始:如果传入的key>root.key, * 那么继续在root的右子树中找,从root的右孩子节点(root.right)开始;如果恰好key==root.key, * 那么直接根据root节点的value值即可。 * 后面的循环规则一样,当遍历到的当前节点作为起始节点,逐步往下找 * * 需要注意的是:这里并没有对key是否为null进行判断,建议自己的实现Comparator时应该要考虑在内 */ 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 { //从这里看出,当默认排序时,key值是不能为null的 if (key == null) throw new NullPointerException(); @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); } /** * 能执行到这里,说明前面并没有找到相同的key,节点已经遍历到最后了,我们只需要new一个Entry放到 * parent下面即可,但放到左子节点上还是右子节点上,就需要按照红黑树的规则来。 */ 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; }
put方法源码中通过fixAfterInsertion(e)方法来进行自平衡处理,我们回顾一下插入时自平衡调整的逻辑,下表中看不懂的名词可以参考关于红黑树(R-B tree)原理,看这篇如何
无需调整 【变色】即可实现平衡 【旋转+变色】才可实现平衡 情况1: 当父节点为黑色时插入子节点 空树插入根节点,将根节点红色变为黑色 父节点为红色左节点,叔父节点为黑色,插入左子节点,那么通过【左左节点旋转】 情况2: - 父节点和叔父节点都为红色 父节点为红色左节点,叔父节点为黑色,插入右子节点,那么通过【左右节点旋转】 情况3: - - 父节点为红色右节点,叔父节点为黑色,插入左子节点,那么通过【右左节点旋转】 情况4: - - 父节点为红色右节点,叔父节点为黑色,插入右子节点,那么通过【右右节点旋转】 接下来我们看一看这个方法
private void fixAfterInsertion(Entry<K,V> x) { //新插入的节点为红色节点 x.color = RED; //我们知道父节点为黑色时,并不需要进行树结构调整,只有当父节点为红色时,才需要调整 while (x != null && x != root && x.parent.color == RED) { //如果父节点是左节点,对应上表中情况1和情况2 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; }
源码中通过 rotateLeft 进行【左旋】,通过 rotateRight 进行【右旋】。都非常类似,我们就看一下【左旋】的代码,【左旋】规则如下:“逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点”。
private void rotateLeft(Entry<K,V> p) { if (p != null) { /** * 断开当前节点p与其右子节点的关联,重新将节点p的右子节点的地址指向节点p的右子节点的左子节点 * 这个时候节点r没有父节点 */ Entry<K,V> r = p.right; p.right = r.left; //将节点p作为节点r的父节点 if (r.left != null) r.left.parent = p; //将节点p的父节点和r的父节点指向同一处 r.parent = p.parent; //p的父节点为null,则将节点r设置为root if (p.parent == null) root = r; //如果节点p是左子节点,则将该左子节点替换为节点r else if (p.parent.left == p) p.parent.left = r; //如果节点p为右子节点,则将该右子节点替换为节点r else p.parent.right = r; //重新建立p与r的关系 r.left = p; p.parent = r; } }
就算是看了上面的注释还是并不清晰,看下图你就懂了
五. get 方法
get方法是通过二分查找的思想,我们看一下源码
public V get(Object key) { Entry<K,V> p = getEntry(key); return (p==null ? null : p.value); } /** * 从root节点开始遍历,通过二分查找逐步向下找 * 第一次循环:从根节点开始,这个时候parent就是根节点,然后通过k.compareTo(p.key)比较传入的key和 * 根节点的key值; * 如果传入的key<root.key, 那么继续在root的左子树中找,从root的左孩子节点(root.left)开始; * 如果传入的key>root.key, 那么继续在root的右子树中找,从root的右孩子节点(root.right)开始; * 如果恰好key==root.key,那么直接根据root节点的value值即可。 * 后面的循环规则一样,当遍历到的当前节点作为起始节点,逐步往下找 */ //默认排序情况下的查找 final Entry<K,V> getEntry(Object key) { if (comparator != null) return getEntryUsingComparator(key); if (key == null) throw new NullPointerException(); @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) p = p.left; else if (cmp > 0) p = p.right; else return p; } return null; } /** * 从root节点开始遍历,通过二分查找逐步向下找 * 第一次循环:从根节点开始,这个时候parent就是根节点,然后通过自定义的排序算法 * cpr.compare(key, t.key)比较传入的key和根节点的key值,如果传入的key<root.key,那么 * 继续在root的左子树中找,从root的左孩子节点(root.left)开始:如果传入的key>root.key, * 那么继续在root的右子树中找,从root的右孩子节点(root.right)开始;如果恰好key==root.key, * 那么直接根据root节点的value值即可。 * 后面的循环规则一样,当遍历到的当前节点作为起始节点,逐步往下找 */ //自定义排序规则下的查找 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; }
六. remove方法
remove方法可以分为两个步骤,先是找到这个节点,直接调用了上面介绍的getEntry(Object key),这个步骤我们就不说了,直接说第二个步骤,找到后的删除操作。
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(p)进行删除操作,删除操作的原理我们在前面已经讲过
- 删除的是根节点,则直接将根节点置为null;
- 待删除节点的左右子节点都为null,删除时将该节点置为null;
- 待删除节点的左右子节点有一个有值,则用有值的节点替换该节点即可;
- 待删除节点的左右子节点都不为null,则找前驱或者后继,将前驱或者后继的值复制到该节点中,然后删除前驱或者后继(前驱:左子树中值最大的节点,后继:右子树中值最小的节点);
private void deleteEntry(Entry<K,V> p) { modCount++; size--; //当左右子节点都不为null时,通过successor(p)遍历红黑树找到前驱或者后继 if (p.left != null && p.right != null) { Entry<K,V> s = successor(p); //将前驱或者后继的key和value复制到当前节点p中,然后删除节点s(通过将节点p引用指向s) p.key = s.key; p.value = s.value; p = s; } Entry<K,V> replacement = (p.left != null ? p.left : p.right); /** * 至少有一个子节点不为null,直接用这个有值的节点替换掉当前节点,给replacement的parent属性赋值,给 * parent节点的left属性和right属性赋值,同时要记住叶子节点必须为null,然后用fixAfterDeletion方法 * 进行自平衡处理 */ if (replacement != null) { //将待删除节点的子节点挂到待删除节点的父节点上。 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; p.left = p.right = p.parent = null; /** * p如果是红色节点的话,那么其子节点replacement必然为红色的,并不影响红黑树的结构 * 但如果p为黑色节点的话,那么其父节点以及子节点都可能是红色的,那么很明显可能会存在红色相连的情 * 况,因此需要进行自平衡的调整 */ if (p.color == BLACK) fixAfterDeletion(replacement); } else if (p.parent == null) {//这种情况就不用多说了吧 root = null; } else { /** * 如果p节点为黑色,那么p节点删除后,就可能违背每个节点到其叶子节点路径上黑色节点数量一致的规则, * 因此需要进行自平衡的调整 */ 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; } } }
操作的操作其实很简单,场景也不多,我们看一下删除后的自平衡操作方法fixAfterDeletion
private void fixAfterDeletion(Entry<K,V> x) { /** * 当x不是root节点且颜色为黑色时 */ while (x != root && colorOf(x) == BLACK) { /** * 首先分为两种情况,当前节点x是左节点或者当前节点x是右节点,这两种情况下面都是四种场景,这里通过 * 代码分析一下x为左节点的情况,右节点可参考左节点理解,因为它们非常类似 */ if (x == leftOf(parentOf(x))) { Entry<K,V> sib = rightOf(parentOf(x)); /** * 场景1:当x是左黑色节点,兄弟节点sib是红色节点 * 兄弟节点由红转黑,父节点由黑转红,按父节点左旋, * 左旋后树的结构变化了,这时重新赋值sib,这个时候sib指向了x的兄弟节点 */ if (colorOf(sib) == RED) { setColor(sib, BLACK); setColor(parentOf(x), RED); rotateLeft(parentOf(x)); sib = rightOf(parentOf(x)); } /** * 场景2:节点x、x的兄弟节点sib、sib的左子节点和右子节点都为黑色时,需要将该节点sib由黑变 * 红,同时将x指向当前x的父节点 */ if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) { setColor(sib, RED); x = parentOf(x); } else { /** * 场景3:节点x、x的兄弟节点sib、sib的右子节点都为黑色,sib的左子节点为红色时, * 需要将sib左子节点设置为黑色,sib节点设置为红色,同时按sib右旋,再将sib指向x的 * 兄弟节点 */ if (colorOf(rightOf(sib)) == BLACK) { setColor(leftOf(sib), BLACK); setColor(sib, RED); rotateRight(sib); sib = rightOf(parentOf(x)); } /** * 场景4:节点x、x的兄弟节点sib都为黑色,而sib的左右子节点都为红色或者右子节点为红色、 * 左子节点为黑色,此时需要将sib节点的颜色设置成和x的父节点p相同的颜色, * 设置x的父节点为黑色,设置sib右子节点为黑色,左旋x的父节点p,然后将x赋值为root */ setColor(sib, colorOf(parentOf(x))); setColor(parentOf(x), BLACK); setColor(rightOf(sib), BLACK); rotateLeft(parentOf(x)); x = root; } } else {//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); }
当待操作节点为左节点时,上面描述了四种场景,而且场景之间可以相互转换,如deleteEntry后进入了场景1,经过场景1的一些列操作后,红黑树的结构并没有调整完成,而是进入了场景2,场景2执行完成后跳出循环,将待操作节点设置为黑色,完成。我们下面用图来说明一下四种场景帮助理解,当然大家最好自己手动画一下。
场景1:
当x是左黑色节点,兄弟节点sib是红色节点,需要兄弟节点由红转黑,父节点由黑转红,按父节点左旋,左旋后树的结构变化了,这时重新赋值sib,这个时候sib指向了x的兄弟节点。
但经过这一系列操作后,并没有结束,而是可能到了场景2,或者场景3和4
场景2:
节点x、x的兄弟节点sib、sib的左子节点和右子节点都为黑色时,需要将该节点sib由黑变红,同时将x指向当前x的父节点
经过场景2的一系列操作后,循环就结束了,我们跳出循环,将节点x设置为黑色,自平衡调整完成。
场景3:
节点x、x的兄弟节点sib、sib的右子节点都为黑色,sib的左子节点为红色时,需要将sib左子节点设置为黑色,sib节点设置为红色,同时按sib右旋,再将sib指向x的兄弟节点
并没有完,场景3的一系列操作后,会进入到场景4
场景4:
节点x、x的兄弟节点sib都为黑色,而sib的左右子节点都为红色或者右子节点为红色、左子节点为黑色,此时需要将sib节点的颜色设置成和x的父节点p相同的颜色,设置x的父节点颜色为黑色,设置sib右孩子的颜色为黑色,左旋x的父节点p,然后将x赋值为root
四种场景讲完了,删除后的自平衡操作不太好理解,代码层面的已经弄明白了,但如果让我自己去实现的话,还是差了一些,还需要再研究。
七. 遍历
遍历比较简单,TreeMap的遍历可以使用map.values(), map.keySet(),map.entrySet(),map.forEach(),这里不再多说。
八. 总结
本文详细介绍了TreeMap的基本特点,并对其底层数据结构红黑树进行了回顾,同时讲述了其自动排序的原理,并从源码的角度结合红黑树图形对put方法、get方法、remove方法进行了讲解,最后简单提了一下遍历操作,若有不对之处,请批评指正,望共同进步,谢谢!
-
TreeMap 实现降序原理及Key 是否可以为 null
2016-09-25 15:04:42一、TreeMap 实现降序排列的原理TreeMap 底层为数据结构为红黑树,默认为升序排序方式。整个红黑树的结构为:根节点值大于所有左子树节点值,小于所有右子树节点值,由此整个红黑树以深度优先搜索方式遍历一遍为从小...一、TreeMap 实现降序排列的原理
TreeMap 底层为数据结构为红黑树,默认为升序排序方式。整个红黑树的结构为:根节点值大于所有左子树节点值,小于所有右子树节点值,由此整个红黑树以深度优先搜索方式遍历一遍为从小到大的升序排列。
如需改为降序排列方式,则应自定义实现 Comparator 接口。降序排列实现示例:
Map<Integer, Object> map = new TreeMap<Integer, Object>(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } }); map.put(1, 111); map.put(2, 222); map.put(3, 333); map.put(4, 444); map.put(5, 555); System.out.println(map);
输出为:
为何会如此定义 Comparator 实现呢?查看 compare 接口注释如下:
由 compare 接口注释可知,返回负数表示比较的两个对象 o1 小于 o2, 0 表示 o1 等于 o2, 正数表示 o1 大于 o2。
而 TreeMap put方法实现为:
由上图可知,TreeMap put操作时,将put操作的key和此时红黑树的根节点root对应的 t.key 进行比较时:
- 小于0,则根节点往左子树节点移动;
- 大于0,则根节点往右子树节点移动;
- 相等时,将当前节点值设为put操作的 value;
- 遍历到无左右子节点时,将节点插入(此处涉及到红黑树的旋转,参看红黑树插入原理)。
由此可得,当要降序排列时,则需让更大的值往左子树节点移动,即:
Map<Integer, Object> map = new TreeMap<Integer, Object>(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } });
注:o1 表示put进TreeMap的key,o2表示已经存在于TreeMap的key。根据 compare(o1, o2) 的返回值决定o1 的位置。
二、TreeMap key值是否可以为null
先放出结论:
1. 当未实现 Comparator 接口时,key 不可以为null,否则抛 NullPointerException 异常;
2. 当实现 Comparator 接口时,若未对 null 情况进行判断,则可能抛 NullPointerException 异常。
1、当未实现 Comparator 接口时,查看 put 源码:由源码可知,当TreeMap不为空时,key 为 null,则抛 NullPointerException 异常;
当 TreeMap 为空时,put 操作,调用 compare() 方法,源码为:
假如此时 K 类型为Integer,将 key 强制转换时,抛 NullPointerException 异常;
2、当实现 Comparator 接口时, 若未对 null 情况作出判断,则可能抛 NullPointerException 异常:
对对象进行 null 判断的实现示例如下:
public static void main(String[] args) { Map<Integer, Object> map1 = new TreeMap<Integer, Object>(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { if (o1 != null && o2 != null) { return o1 - o2; } if (o1 == null && o2 != null) { return -1; } if (o1 != null && o2 == null) { return 1; } return 0; } }); map1.put(2, "222"); map1.put(null, "111"); System.out.println(map1); }
输出为:
-
考考基础部分,你能说出 TreeMap 原理实现及常用方法吗?
2020-08-08 17:54:03考考基础部分,你能说出 TreeMap 原理实现及常用方法吗? 目录 TreeMap概述 红黑树回顾 TreeMap构造 put方法 get 方法 remove方法 遍历 总结 一. TreeMap概述 TreeMap存储K-V键值对,通过红黑树(R-B tree)实现; ... -
oracle树子类遍历父类_红黑树TreeSet原理HashSet底层原理Map集合遍历
2020-12-30 13:07:26哈希表数据结构)3、能够掌握Map集合的特点以及使用(特点,常见方法,Map集合的遍历)4、能够掌握HashMap集合的特点以及使用5、能够掌握TreeMap集合的特点以及使用==知识点==红黑树HashSetMapHashMapTreeMap==知识点... -
treemap排序_【144期】考考基础部分,你能说出 TreeMap 原理实现及常用方法吗?...
2020-12-09 11:49:12目录TreeMap概述红黑树回顾TreeMap构造put方法get 方法remove方法遍历总结一. TreeMap概述TreeMap存储K-V键值对,通过红黑树(R-B tree)实现;TreeMap继承了NavigableMap接口,NavigableMap接口继承了SortedMap接口... -
HashMap(存储原理、图解、常用方法、遍历)、LinkedHashMap(常用方法、遍历)、TreeMap(排序机制、排序...
2019-11-13 18:09:22本章目录 Map 1、Map 集合概述 2、Map 集合的功能 HashMap 1、HashMap 的实现原理 2、HashMap 存储原理图解 3、HashMap 常用方法 5、HashMap 遍历 LinkedHashMap 1、LinkedHashMap 概述 2、LinkedHashMap 常用方法 3...