【数据结构】11.java源码关于TreeMap
目录
1.TreehMap的内部结构
2.TreehMap构造函数
3.元素新增策略
4.元素删除
5.元素修改和查找
6.特殊操作
7.扩容
8.总结
1.TreeMap的内部结构
首先确认一点,treemap是一个基于红黑树的map,这个集合的一个特点就是排序,是的如果不是排序,那么hashmap可以完美取代
再开始前我们要熟悉一个红黑树的概念:
对于红黑树的定义:
1.节点是红色或黑色。
2.根是黑色。
3.所有叶子都是黑色(叶子是NIL节点)。
4.每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
5.从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
2.TreeMap构造函数
通过已排序的map进行新的treemap的构造生成,这里用到了一个buildFromSorted函数,这个方法会递归数据
参考buildFromSorted实现,这个实现的红黑树,说白了,就是把最后一层变成红色,以上全作为黑色
3.元素新增策略
Put操作
这个操作没啥,就是遍历这颗树,左边小,右边大,遍历到合适的位置设置值,或者创建新的节点插入,并默认设置为黑色
重点在于后面的变动之后,如果进行红黑树的修复
针对红黑树的变动,可以参考以上总结的规则:https://www.cnblogs.com/cutter-point/p/10976416.html
针对于红黑树的操作主要就是左旋和右旋的操作
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
//如果根节点为空,然后这个比较其实是起一个类型检查作用
compare(key, key);
//创建root节点
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
//如果根节点存在
int cmp;
Entry<K,V> parent;
//判断是否设置了比较器
Comparator<? super K> cpr = comparator;
if (cpr != null) {
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 {
//如果没有设置
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);
}
//如果一直到t为空还没找到,那么就创建新值
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;
} //获取对应节点的父节点
private static <K,V> Entry<K,V> parentOf(Entry<K,V> p) {
return (p == null ? null: p.parent);
} //求当前节点的左右节点
private static <K,V> Entry<K,V> leftOf(Entry<K,V> p) {
return (p == null) ? null: p.left;
} private static <K,V> Entry<K,V> rightOf(Entry<K,V> p) {
return (p == null) ? null: p.right;
} //获取当前节点的颜色,为空即为黑
private static <K,V> boolean colorOf(Entry<K,V> p) {
return (p == null ? BLACK : p.color);
} private static <K,V> void setColor(Entry<K,V> p, boolean c) {
if (p != null)
p.color = c;
} 红黑树的修复操作,主要重点就是对从一个节点到叶子节点的黑色节点个数相同为基准 //进行红黑树修复
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 {
//如果是黑色,说明两边的分支走到叶子节点的不是相同数目的黑色节点
//如果x是右节点,那么就左旋,如果是左节点就右旋
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;
} 左旋右旋操作就是把以对应的节点为核心进行节点的上升和下降,然后要复合红黑树的规范
private void rotateLeft(Entry<K,V> p) {
//左旋操作
if (p != null) {
//如果节点不为空,进行左旋的时候,获取节点的右节点
Entry<K,V> r = p.right;
p.right = r.left;
if (r.left != null)
r.left.parent = p;
r.parent = p.parent;
if (p.parent == null)
root = r;
else if (p.parent.left == p)
p.parent.left = r;
else
p.parent.right = r;
r.left = p;
p.parent = r;
}
} /** From CLR */
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;
}
}
4.元素删除
进行元素删除的思想是:
如果我需要删除这个节点,那么首选判断右子树是否存在,如果存在那么就找到右边子树的最小值,也就是最小的叶子节点,用一个最靠近的节点的数据替换需要删除的节点的数据,这样就保障二叉索引树的特性,然后根据变动的节点颜色重新修复这颗红黑树的颜色
//节点删除操作
public V remove(Object key) {
//获取到这个节点
Entry<K,V> p = getEntry(key);
if (p == null)
return null; //获取旧值
V oldValue = p.value;
//删除节点,然后返回旧值
deleteEntry(p);
return oldValue;
} final Entry<K,V> getEntry(Object key) {
//如果有设置比较器,那么就优先使用比较器进行寻找
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
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;
} //进行节点的删除
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--; // 当前节点有两个子节点,找到右子树中最小元素作为后继节点;将后继节点信息替换到当前节点
if (p.left != null && p.right != null) {
//说白了就是找这个p元素相邻大小的元素,优先找大的,其次找小的
//1.找右子树的最小节点 因为上面有判断p的左右节点必须存在,所以结果肯定是右子树的最小值
Entry<K,V> s = successor2(p);
p.key = s.key;
p.value = s.value;
//吧引用指向s,吧p的值设置为s的值
//这个时候需要删除的那个节点的值变成了一个新的最靠近的值,这样就不会破坏索引树的条件,然后把那个用来替换的节点干掉即可
p = s;
//替换删除的元素
} // 开始修复,优先取这个节点的左边,否则取右边作为replacement节点对象
//除非是p.right和left有一个为空,不然一般肯定走的是p.right并且是个null对象
Entry<K,V> replacement = (p.left != null ? p.left : p.right); //如果要进行取代的节点为空,那么就不用操作
// 1、有两个儿子。这种情况比较复杂,但还是比较简单。上面提到过用子节点C替代代替待删除节点D,然后删除子节点C即可。
// 2、没有儿子,即为叶结点。直接把父结点的对应儿子指针设为NULL,删除儿子结点就OK了。
// 3、只有一个儿子。那么把父结点的相应儿子指针指向儿子的独生子,删除儿子结点也OK了。
if (replacement != null) {
//把需要替换的节点的父节点设置为p的父节点
replacement.parent = p.parent;
//判断p的父节点是否Wie空,或者判断p是否是作为左节点,否则判断是否是右节点
//说白了就是用replacement 取代P节点
if (p.parent == null)
root = replacement;
else if (p == p.parent.left)
p.parent.left = replacement;
else
p.parent.right = replacement; //设置p的左右和父节点为空,也就是吧p节点剥离开
p.left = p.right = p.parent = null; // 修复颜色分配,因为最后一个要删除的节点是黑色,那么删除这个节点之后,这条线的黑色节点个数肯定会减去1
if (p.color == BLACK)
//那么我们就需要对这个变动过的节点进行调整
fixAfterDeletion(replacement);
} else if (p.parent == null) { // return if we are the only node.
root = null;
} else {
//一般情况是这个,也就是说吧需要删除的节点移动到末尾叶子节点,然后把key,value替换掉,最后删除调最后一个叶子即可
//然后如果叶子的颜色正好是黑色的,那么要重新修复颜色
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;
}
}
} static <K,V> TestTreeMap.Entry<K,V> successor2(Entry<K,V> t) {
//找到右边节点的最小元素,也就是仅仅比T大一点的元素
Entry<K,V> p = t.right;
while (p.left != null)
p = p.left;
return p;
} private void fixAfterDeletion(Entry<K,V> x) {
//判断替换过的节点是否是黑色,删除节点需要一直迭代,直到 x 不是根节点,且 x 的颜色是黑色
while (x != root && colorOf(x) == BLACK) {
//如果这个节点是左节点
if (x == leftOf(parentOf(x))) {
//获取兄弟节点
Entry<K,V> sib = rightOf(parentOf(x)); if (colorOf(sib) == RED) {
//兄弟节点为红,设置为黑,父辈设置为红,然后左旋
setColor(sib, BLACK);
setColor(parentOf(x), RED);
//左旋
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
} if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
//右旋
rotateRight(sib);
sib = rightOf(parentOf(x));
}
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
} else { // symmetric
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);
}
5.元素修改和查找
修改可以参考put操作,对key无影响
查找因为是二叉索引树,所以查找方式和remove中的getEntry操作类似
6.特殊操作
6.1BuildFromSorted
这函数用来根据一级拍好顺序的map构建treemap
再这个函数之前还有一个computeRedLevel函数,这个用来计算当前节点所在的层数
//计算红色节点应该在红黑树哪一层,因为二叉树,因为每层二叉树要填满的话必须是2的倍数
//每层数据叠加是1,1+2,1+2+4,1+2+4+8.。。 基本就是每层就是每层/2
//sz指树中节点的个数,为了确保是一个红黑树,那么需要把前面几层全部看成黑色,最后一层设置为红色即可
//因为sz是节点的个数,所以最后一个节点所在的层数即是红色
public static int computeRedLevel(int sz) {
int level = 0;
//从0开始计算0,2,6,14
//可以看出m=(m+1)*2 前一个和后一个的递推关系 每一层计算
//那么反过来就是m/2-1就是上一层的位置,最后一个m>=0的时候还要计算一次
for (int m = sz - 1; m >= 0; m = m / 2 - 1)
level++;
return level;
} //通过迭代构造一个新的排序的map,递归将SortedMap中的元素逐个关联
//str: 如果不为空,则从流里读取key-value,defaultVal:见名知意,不为空,则value都用这个值
private void buildFromSorted(int size, Iterator<?> it,
java.io.ObjectInputStream str,
V defaultVal)
throws java.io.IOException, ClassNotFoundException {
this.size = size;
//递归,添加到root节点上
root = buildFromSorted(0, 0, size-1, computeRedLevel(size),
it, str, defaultVal);
} /**
* level: 当前树的层数,注意:是从0层开始
* lo: 子树第一个元素的索引
* hi: 子树最后一个元素的索引
* redLevel: 上述红节点所在层数
* 剩下的3个就不解释了,跟上面的一样
*/
private final Entry<K,V> buildFromSorted(int level, int lo, int hi,
int redLevel,
Iterator<?> it,
java.io.ObjectInputStream str,
V defaultVal)
throws java.io.IOException, ClassNotFoundException {
/*
* Strategy: The root is the middlemost element. To get to it, we
* have to first recursively construct the entire left subtree,
* so as to grab all of its elements. We can then proceed with right
* subtree.
*
* The lo and hi arguments are the minimum and maximum
* indices to pull out of the iterator or stream for current subtree.
* They are not actually indexed, we just proceed sequentially,
* ensuring that items are extracted in corresponding order.
*/
if (hi < lo) return null; //这相当于除以二,取中间位置,相当于除以2
int mid = (lo + hi) >>> 1; Entry<K,V> left = null;
//子树第一个元素的索引开始到中间的位置作为左子树,右边剩下递归又右子树
if (lo < mid) //递归左边部分节点
left = buildFromSorted(level+1, lo, mid - 1, redLevel,
it, str, defaultVal); K key;
V value;
//通过迭代器遍历所有节点
if (it != null) {
if (defaultVal==null) {
Map.Entry<?,?> entry = (Map.Entry<?,?>)it.next();
key = (K)entry.getKey();
value = (V)entry.getValue();
} else {
key = (K)it.next();
value = defaultVal;
}
} else { // use stream,通过流读取对象
key = (K) str.readObject();
value = (defaultVal != null ? defaultVal : (V) str.readObject());
} //创建中间节点
Entry<K,V> middle = new Entry<>(key, value, null); // color nodes in non-full bottommost level red
if (level == redLevel)
middle.color = RED; if (left != null) {
middle.left = left;
left.parent = middle;
} //递归右边节点
if (mid < hi) {
Entry<K,V> right = buildFromSorted(level+1, mid+1, hi, redLevel,
it, str, defaultVal);
middle.right = right;
right.parent = middle;
} return middle;
}
这里可以总结一点:左旋和右旋的判断主要依据是=》从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。因为我们每次新增的节点都是红色,所以这个红色的节点就会破坏原来的结构,会再红色的null节点新增一个黑色null节点,为了修复这种情况,那么就需要对父节点下的另外一个节点进行修复
7.扩容
不存在扩容问题,二叉树嘛,更类似链表的结构
8.总结
总结就是不论是新增还是删除,再修复颜色的时候,维持从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点这个原则
所以一般会校验这个节点的兄弟,以及父辈的兄弟节点,至于进行右旋还是左旋,这个参考我之前的博客的内容以及规则
参考:
https://blog.csdn.net/cyywxy/article/details/81151104
https://www.jianshu.com/p/e11fe1760a3d
【数据结构】11.java源码关于TreeMap的更多相关文章
- java源码之TreeMap
Map的单元是对键值对的处理,之前分析过的两种Map,HashMap和LinkedHashMap都是用哈希值去寻找我们想要的键值对,优点是理想情况下O(1)的查找速度. 那如果我们在一个对查找性能要求 ...
- java源码 -- TreeMap
简介 TreeMap 是一个有序的key-value集合,它是通过红黑树实现的.TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合.TreeMap 实现了N ...
- 如何阅读Java源码 阅读java的真实体会
刚才在论坛不经意间,看到有关源码阅读的帖子.回想自己前几年,阅读源码那种兴奋和成就感(1),不禁又有一种激动. 源码阅读,我觉得最核心有三点:技术基础+强烈的求知欲+耐心. 说到技术基础,我打个比 ...
- 如何阅读Java源码
刚才在论坛不经意间,看到有关源码阅读的帖子.回想自己前几年,阅读源码那种兴奋和成就感(1),不禁又有一种激动.源码阅读,我觉得最核心有三点:技术基础+强烈的求知欲+耐心. 说到技术基础,我打个比方吧, ...
- Java 源码学习线路————_先JDK工具包集合_再core包,也就是String、StringBuffer等_Java IO类库
http://www.iteye.com/topic/1113732 原则网址 Java源码初接触 如果你进行过一年左右的开发,喜欢用eclipse的debug功能.好了,你现在就有阅读源码的技术基础 ...
- [收藏] Java源码阅读的真实体会
收藏自http://www.iteye.com/topic/1113732 刚才在论坛不经意间,看到有关源码阅读的帖子.回想自己前几年,阅读源码那种兴奋和成就感(1),不禁又有一种激动. 源码阅读,我 ...
- 如何阅读Java源码?
阅读本文大概需要 3.6 分钟. 阅读Java源码的前提条件: 1.技术基础 在阅读源码之前,我们要有一定程度的技术基础的支持. 假如你从来都没有学过Java,也没有其它编程语言的基础,上来就啃< ...
- Java源码阅读的真实体会(一种学习思路)
Java源码阅读的真实体会(一种学习思路) 刚才在论坛不经意间,看到有关源码阅读的帖子.回想自己前几年,阅读源码那种兴奋和成就感(1),不禁又有一种激动. 源码阅读,我觉得最核心有三点:技术基础+强烈 ...
- Java源码阅读的真实体会(一种学习思路)【转】
Java源码阅读的真实体会(一种学习思路) 刚才在论坛不经意间,看到有关源码阅读的帖子.回想自己前几年,阅读源码那种兴奋和成就感(1),不禁又有一种激动. 源码阅读,我觉得最核心有三点:技术基础+ ...
随机推荐
- 原生js如何判断元素出现在可视区
元素出现在可视区 scorll滑动的距离>=当前元素距离浏览器最顶端的高度+当前元素自身的高度-当前可视区的高度 触底 scorll滑动的距离>=当前scroll总高度-当前可视区的高度
- 模拟测试20191017~18 lrd Day1& Day2
$Day1:$ $T1:位运算$ 从低位到高位分类讨论就好了 记得判$inf$ $T2:集合论$ 考场上差点就打线段树了 用一个数组维护,同时用一个变量代表当前总体$+$&&$-$的值 ...
- 大数定律(Law of Large Numbers)
大数定律:每次从总体中随机抽取1个样本,这样抽取很多次后,样本的均值会趋近于总体的期望.也可以理解为:从总体中抽取容量为n的样本,样本容量n越大,样本的均值越趋近于总体的期望.当样本容量极大时,样本均 ...
- spring注解式参数校验列表
校验注释列表: @AssertFalse Boolean,boolean 验证注解的元素值是false @AssertTrue Boolean,boolean 验证注解的元素值是true @NotNu ...
- smartnic
19年趋势: Intel® 2019网络技术研讨会圆满落幕 SANTOS: Flow and HQoS Acceleration Over DPDK Using Intel Programmable ...
- git 初始用法
Git global setup git config --global user.name "xiaoming" git config --global user.email & ...
- 第08组 Alpha冲刺(2/6)
队名:955 组长博客: 作业博客:https://edu.cnblogs.com/campus/fzu/SE_FZU_1917_K/homework/9939 组员情况 组员1(组长):庄锡荣 过去 ...
- 运行时异常RuntimeException捕获的小测试
public class ExceptionTest { public static void main(String[] args) throws InterruptedException { ne ...
- springboot(1)@SpringBootApplication
首先来看下Spring Boot项目中的运行类,基本上每个项目都会有该启动类: @SpringBootApplication public class Application { public sta ...
- 三句话看明白jdk收费吗
对于从oracle下载的jdk8:JDK8u200(含)以下版本不收费. 对于从oracle下载的jdk11:JDK 11.0.0不收费,JDK 11.0.1不收费. 对于openjdk:免费 ——— ...