红黑树详细讲解(结合JavaTreeMap)
1:红黑树简介
红黑树又称红-黑二叉树,它首先是一颗二叉树,它具体二叉树所有的特性。同时红黑树更是一颗自平衡的排序二叉树。根据二叉查找树的概念可以得出正常情况下查找的时间复杂度为O(log n),但是可能会出现一种极端的情况使得这颗二叉树变为线性的则查找的时间复杂度直接降到(O(n)),为了避免这种情况红黑树应运而生,红黑树是一颗自平衡的二叉树,是解决二叉查找树变为线性结构的一种实现方式。
1.1:红黑树概念:
1、每个节点都只能是红色或者黑色
2、根节点是黑色
3、每个叶节点(NIL节点,空节点)是黑色的。
4、如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
5、从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。所以红黑树它是复杂而高效的,其检索效率O(log n)。
1.2:红黑树自平衡的关键操作
1:左旋转(如下图:图片来源网络)
2:右旋转:(如下图:图片来源网络)
3:变色:通过改变节点的颜色达到满足红黑树的概念。
1.3:红黑树的应用
红黑树的应用比较广泛,主要是用它来存储有序的数据,它的时间复杂度是O(lgn),效率非常之高。 例如,Java集合中的TreeSet,C++ STL中的set、map,以及Linux虚拟内存的管理,都是通过红黑树去实现的。
2:红黑树的插入操作
2.1插入
插入总共可以分为五种情况,其中前两种情况是最简单的,而后面的情况又是可以相互转换的,下面具体分析。为了使插入之后尽量满足红黑树更多的规则,所以我们规定每个新插入的节点为红色。
(第三四五种情况我们按照在如下前提下进行:插入的节点为S(son),S的父亲节点为F(father),F的父亲节点为G(grandfather),而F的兄弟节点为U(uncle)。并且F为G的左儿子) 当F为G的右儿子的时候对称操作即可。
2.1.1:为跟节点
若新插入的节点N没有父节点,则直接当做根据节点插入即可,同时将颜色设置为黑色。
2.1.2:父节点为黑色
这种情况新节点N同样是直接插入,同时颜色为红色,由于根据规则四它会存在两个黑色的叶子节点,值为null。同时由于新增节点N为红色,所以通过它的子节点的路径依然会保存着相同的黑色节点数,同样满足规则5。
2.1.3:若父节点F和F的兄弟节点U都为红色
操作:将F以及U设置为黑,G设为红,并将G看成新插入的节点(即下一轮的S),递归操作。
原因:这个操作实际是想将红色往根处移动。将红色往上移了一层,并不会打破红黑树的特性,不断的把红色往上移动,当移动到根时,直接将根设置为黑色,就完全符合红黑树的性质了。
2.1.4:若父节点F为红色,叔节点U为黑色或者缺少,且新增节点S为F节点的右孩子
操作:将F左旋,并把F看成新加入的节点(即下一轮的S),继续后面的判断。
这里所产生的结果其实并没有完成,还不是平衡的(违反了规则四),这是我们需要进行情况5的操作。
2.1.5:父节点F为红色,叔父节点U为黑色或者缺少,新增节点S为父节点F左孩子
操作:先将F设为黑,G设为红,然后G右旋。这样操作后,就完全符合红黑树的性质了
3:Java中TreeMap插入操作实现分析
本来想自己手动实现一下红黑树,奈何JDK源码写的太优秀了,下面就结合Java中TreeMap的put方法实现分析上面的操作。
3.1:插入操作
public V put(K key, V value) {
Entry<K,V> t = root;
//第一种情况也就是上满的2.1.1
if (t == null) {
//本来感觉这句代码没有用,看到源码注释才知道校验空值用的,真是的。。。。。。。
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
//cmp表示key比较大小的返回结果
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
//如果cpr不为空,则采用既定的排序算法进行创建TreeMap集合
if (cpr != null) {
//该循环是根据给定的排序算法根据key是否能找到对应的键值对,如果能找到的话则覆盖旧值并且返回旧值
do {
//parent为找到的符合插入的父节点
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);
}
//如果cpr为空,则采用默认的排序算法进行创建TreeMap集合
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
//该循环是根据默认的排序算法根据key是否能找到对应的键值对,如果能找到的话则覆盖旧值并且返回旧值
do {
//parent为找到的符合插入的父节点
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不再此树种,则新建节点,插入的parent节点下
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
//当前key小于父节点key,插入左子树
parent.left = e;
else
//当前key大于父节点key,插入右子树
parent.right = e;
//插入节点之后调整,使其满足红黑树的性质,其中情况2.1.2是不需要调整的,所以调整操作里面没有情况2.1.2
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
。3.2:调整操作
调整红黑树节点,使其满足红黑树的五条性质。
private void fixAfterInsertion(Entry<K,V> x) {
//使插入的节点为红色
x.color = RED;
//循环 直到 x不是根节点,且x的父节点不为红色
while (x != null && x != root && x.parent.color == RED) {
//如果插入的节点的父节点是他的父节点的左节点,也就是我们在2.1上面假设的F为G的左儿子
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//获取x的叔叔节点
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
//如果X的叔节点(U) 为红色(情况2.1.3)
if (colorOf(y) == RED) {
//将X的父节点设置为黑色
setColor(parentOf(x), BLACK);
//将X的叔叔节点设置为黑色
setColor(y, BLACK);
//将X的祖父节点设置为红色
setColor(parentOf(parentOf(x)), RED);
//把祖父节点当为当前节点循环递归
x = parentOf(parentOf(x));
}
//如果X的叔节点,这里会存在两种情况(2.1.4和2.1.5)
else {
//如果X节点为其父节点的右子树,则进行左旋转(情况2.1.4)
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
//左旋转代码不再展示,不明白怎么实现左旋转代码的可以自己参考一下源码
rotateLeft(x);
}
//情况(2.1.5)
//将X的父节点设置为黑色
setColor(parentOf(x), BLACK);
//将X的祖父节点设置为黑色
setColor(parentOf(parentOf(x)), RED);
//右旋转
rotateRight(parentOf(parentOf(x)));
}
}
//如果插入的节点的父节点是他的父节点的右节点,也就是我们在2.1上面假设的F为G的右儿子
下面的操作均为上面操作的对称操作,不再多余解释
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节点变为红色,不满足红黑树性质,所有设置root节点为黑色
root.color = BLACK;
}
4:红黑树的删除操作
相对于红黑树的增加节点而言,删除更加复杂,因为删除节点的时候相对于插入多了很多情况,实际上只要我们慢慢的捋清楚所有的情况,你就会发现他也不难,然后自己完全搞明白之后说不会还会感叹,就这!就这!哈哈哈!
由于红黑树的叶节点都是nil节点,所以为了表述方便,我们下面nil节点不再考虑。
4.1:删除
红黑树的删除主要分为一下几种情况,我们下面一一讨论,然后再结合Java中TreeMap中的删除操作分析一下。
4.1.1:情况1
待删除节点为红色,且没有子节点,这这种情况是最简单的,因为删除子节点之后不会影响红黑树的限制条件,所以直接删除即可。
4.1.2:情况2
待删除有一个子节点,由于红黑树限制,X1只能为红色,不理解的可以在重温一下概念。
这种情况处理也是非常简单的,用子节点替代待删除节点,然后删除子节点即可。
4.1.3:情况3
待删除节点有两个子节点,这种情况说明待删除节点不是叶节点,那么我们就需要找到他的后继节点,然后用后继节点的key和value覆盖待删除节点,然后再去删除后继节点,这样就将待删除节点转换到了叶节点上或者叶节点的父节点上,有些情况就转换成情况1和情况2,剩下的情况就是待删除节点为黑色,且没有子节点这一种情况。然后这一种情况又可以分为四种情况,哈哈哈,是不是有点惊喜,有点意外。
顺便说一下前驱和后继的概念
前驱:小于当前节点的最大节点
后继:大于当前节点的最小节点
4.1.4:情况4
待删除节点的兄弟节点为红色
B为红色,那么其子节点BL、BR必定全部为黑色,父节点P也为黑色。处理策略是:改变B、P的颜色,然后进行一次左旋转。这样处理就可以使得红黑性质得以继续保持。这样处理后将情况情况4、转变为情况5、6、7中的一种。
4.1.5:情况5
s的兄弟B是黑色的,且B的俩个孩子都是黑色的
这种情况其父节点P可红可黑(图上画的为黑色,不要误解),由于B为黑色,这样导致S子树相对于其兄弟B子树少一个黑色节点,这时我们可以将B置为红色。这样,S子树与B子树黑色节点一致,保持了平衡。
将B由黑转变为红,这样就会导致新节点S(也就是图上的节点P)相对于它的兄弟节点会少一个黑色节点。但是如果P为红色,我们直接将P转变为黑色,保持整棵树的平衡。否则情况情况5 会转变为情况4、6、7的一种。
4.1.6:情况6
S的兄弟B是黑色的,B的左孩子是红色,B的右孩子是黑色。
这种情况是将节点B和其左子节点进行颜色交换,然后对B进行右旋转处理。于是情况6转化为情况7
4.1.7:情况7
S的兄弟B是黑色的,且B的右孩子时红色的
交换B和父节点P的颜色,同时对P进行左旋转操作。这样就把左边缺失的黑色节点给补回来了。同时将B的右子节点BR置黑。这样左右都达到了平衡。
后面的这四种情况比较难理解,首先他们都不是单一的某种情况,他们之间是可以进行互转的。相对于其他的几种情况,情况5比较好理解,仅仅只是一个颜色的转变,通过减少右子树的一个黑色节点使之保持平衡,同时将不平衡点上移至S与B的父节点,然后进行下一轮迭代。情况4,是将B旋转将其转成情况5、6、7情况进行处理。而情况6通过转变后可以化成情况7来进行处理,从这里可以看出情况7应该最终结。情况7、右子节点为红色节点,那么将缺失的黑色节点交由给右子节点,通过旋转达到平衡。
5:Java中TreeMap的删除操作实现分析
5.1:删除操作
public V remove(Object key) {
//根据查找节点
Entry<K,V> p = getEntry(key);
//查找不到,直接返回
if (p == null)
return null;
//查找到,删除节点P并返回P节点的value
V oldValue = p.value;
deleteEntry(p);
return oldValue;
}
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;
// If strictly internal, copy successor's element to p and then make p
// point to successor.
//情况3,左右节点都不为空,查找后继,用后继的key和value覆盖待删除的节点
if (p.left != null && p.right != null) {
//寻找后继节点
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
} // p has 2 children
// Start fixup at replacement node, if it exists.
//replacement为替代节点,如果P的左子树存在那么就用左子树替代,否则用右子树替代
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
//如果replacement不为空,则说明有子节点,则满足情况二,直接删除此节点,并用子节点替代皆即可
if (replacement != null) {
// Link replacement to parent
replacement.parent = p.parent;
//删除节点为root节点
if (p.parent == null)
root = replacement;
else if (p == p.parent.left)
p.parent.left = replacement;
else
p.parent.right = replacement;
// Null out links so they are OK to use by fixAfterDeletion.
p.left = p.right = p.parent = null;
// Fix replacement
//情况2:如果待删除节点P为黑色,则调整P节点的子节点为黑色
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { // return if we are the only node.
//p没有父节点,表示为P根节点,直接删除即可
root = null;
} else { // No children. Use self as phantom replacement and unlink.
//如果P节点的颜色为黑色,则需要按照情况4、5、6、7对红黑树进行调整
if (p.color == BLACK)
fixAfterDeletion(p);
//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;
}
}
}
5.2:调整操作
private void fixAfterDeletion(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
//如果X的父亲节点是其祖父节点的左孩子
if (x == leftOf(parentOf(x))) {
//获取兄弟节点
Entry<K,V> sib = rightOf(parentOf(x));
//情况4:兄弟节点为红色,改变B、P的颜色,然后进行一次左旋转
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}
//情况5:若兄弟节点的两个子节点都为黑色,将兄弟节点变成红色
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
//情况6:如果兄弟节点只有右子树为黑色,将兄弟节点与其左子树进行颜色互换然后进行右转
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
//情况7:交换兄弟节点和父节点的颜色,同时将兄弟节点右子树设置为黑色,最后左旋转
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
}
//如果X的父亲节点是其祖父节点的右孩子,与上面的操作对称即可,不再做多余解释
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);
}
这是红黑树在删除节点后,对树的平衡性进行调整的过程,其实现过程与上面四种复杂的情况一一对应,所以在这个源码的时候一定要对着上面提到的四种情况看。
小结:本文主要结合Java中TreeMap的实现详细的说明红黑树的插入和删除操作以及调整过程,红黑树的主要操作是插入和删除,插入和删除的时候难免会破坏红黑树的结构,所有我们需要变色,左旋转,右旋转来调整,使其满足红黑树的限制条件。
红黑树详细讲解(结合JavaTreeMap)的更多相关文章
- B树 B+树 红黑树
B-Tree(B树) 具体讲解之前,有一点,再次强调下:B-树,即为B树.因为B树的原英文名称为B-tree,而国内很多人喜欢把B-tree译作B-树,其实,这是个非常不好的直译,很容易让人产生误解. ...
- 红黑树原理详解及golang实现
目录 红黑树原理详解及golang实现 二叉查找树 性质 红黑树 性质 operation 红黑树的插入 golang实现 类型定义 leftRotate RightRotate Item Inter ...
- 【深入理解Java集合框架】红黑树讲解(上)
来源:史上最清晰的红黑树讲解(上) - CarpenterLee 作者:CarpenterLee(转载已获得作者许可,如需转载请与原作者联系) 文中所有图片点击之后均可查看大图! 史上最清晰的红黑树讲 ...
- 红黑树之 原理和算法详细介绍(阿里面试-treemap使用了红黑树) 红黑树的时间复杂度是O(lgn) 高度<=2log(n+1)1、X节点左旋-将X右边的子节点变成 父节点 2、X节点右旋-将X左边的子节点变成父节点
红黑树插入删除 具体参考:红黑树原理以及插入.删除算法 附图例说明 (阿里的高德一直追着问) 或者插入的情况参考:红黑树原理以及插入.删除算法 附图例说明 红黑树与AVL树 红黑树 的时间复杂度 ...
- 数据结构与算法(十):红黑树与TreeMap详细解析
本文目录 一.为什么要创建红黑树这种数据结构 在上篇我们了解了AVL树,既然已经有了AVL这种平衡的二叉排序树,为什么还要有红黑树呢? AVL树通过定义我们知道要求树中每一个结点的左右子树高度差的绝对 ...
- 【Java入门提高篇】Day25 史上最详细的HashMap红黑树解析
当当当当当当当,好久不见,最近又是换工作,又是换房子,忙的不可开交,断更了一小段时间,最重要的一篇迟迟出不来,每次都犹抱琵琶半遮面,想要把它用通俗易懂的方式进行说明,确实有一定的难度,可愁煞我也,但自 ...
- 史上最详细的HashMap红黑树解析
简介:请允许我当一回标题党.好了,言归正传,本篇主要内容便是介绍HashMap的男二号——TreeNode(男一号还是给Node吧,毕竟是TreeNode的爷爷,而且普通节点一般来说也比TreeN ...
- 【红黑树】的详细实现(C++)
红黑树的介绍 红黑树(Red-Black Tree,简称R-B Tree),它一种特殊的二叉查找树.红黑树是特殊的二叉查找树,意味着它满足二叉查找树的特征:任意一个节点所包含的键值,大于等于左孩子的键 ...
- 浅谈算法和数据结构: 七 二叉查找树 八 平衡查找树之2-3树 九 平衡查找树之红黑树 十 平衡查找树之B树
http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html 前文介绍了符号表的两种实现,无序链表和有序数组,无序链表在插入的 ...
- [转]SGI STL 红黑树(Red-Black Tree)源代码分析
STL提供了许多好用的数据结构与算法,使我们不必为做许许多多的重复劳动.STL里实现了一个树结构-Red-Black Tree,它也是STL里唯一实现的一个树状数据结构,并且它是map, multim ...
随机推荐
- 2019-3-15-uwp-ScrollViewer-content-out-of-panel-when-set-the-long-width
title author date CreateTime categories uwp ScrollViewer content out of panel when set the long widt ...
- 什么是IPD项目管理模式?聊聊IPD下的产品研发流程
IPD(集成产品开发)涵盖了产品从创意提出到研发.生产.运营等,包含了产品开发到营销运营的整个过程.围绕产品(或项目)生命周期的过程的管理模式,是一套生产流程,更是时下国际先进的管理体系.IPD(集成 ...
- ChatGPT开源项目精选合集
大家好,我是 Java陈序员. 2023年,ChatGPT 的爆火无疑是最值得关注的事件之一,AI对话.AI绘图等工具层出不穷. 今天给大家介绍几个 ChatGPT 的开源项目! 关注微信公众号:[J ...
- Mybatis逆向工程的2种方法,一键高效快速生成Pojo、Mapper、XML,摆脱大量重复开发
一.写在开头 最近一直在更新<Java成长计划>这个专栏,主要是Java全流程学习的一个记录,目前已经更新到Java并发多线程部分,后续会继续更新:而今天准备开设一个全新的专栏 <E ...
- Zookeeper中的角色
在zookeeper集群中,节点也有不同的角色,承担着不同角色. zookeeper有三种角色: 老大:Leader (领导者) : 客户端提供读服务和写服务. 老二:Follower(跟随者 ...
- NumPy 数组创建方法与索引访问详解
NumPy 创建数组 NumPy 中的核心数据结构是 ndarray,它代表多维数组.NumPy 提供了多种方法来创建 ndarray 对象,包括: 使用 array() 函数 array() 函数是 ...
- Typora+免费图床,构建随处可用的Markdown文档
Typora+PicGo+Gitee自动上传图片 视频教程: https://www.bilibili.com/video/BV1hT4y1f7Mf?from=search&seid=1546 ...
- C数据结构:循环队列的顺序存储结构
顺序队列目录 队列的定义 定义 假溢出 空间浪费的缺点 如何解决 循环队列的缺点 *==主要的算法思想(重要)==* 如何理解循环队列(必看) 结构体代码 两种实现方法 **①循环队列,队头和队尾指针 ...
- .NET周刊【5月第2期 2024-05-12】
国内文章 C#在工业数字孪生中的开发路线实践 https://mp.weixin.qq.com/s/b_Pjt2oii0Xa_sZp_9wYWg 这篇文章探讨了C#在工业数字孪生技术中的应用,介绍了三 ...
- PageOffice 6 给SaveFilePage指向的保存地址传参
PageOffice给保存方法传递参数的方式有两种: 通过设置保存地址的url中的?传递参数.例如: poCtrl.setSaveFilePage("/save?p1=1") 通过 ...