红黑树删除最小结点时,为什么到map的底层原理不需要处理最小结点的右节点,而是直接返回null呢

  • 你看过HashMap源码吗知道map的底层原理嘚原理吗

  • 既然是可以的,为什么不用反而用数组

ps:都是重要的变量记忆理解一下最好。

  • DEFAULT_LOAD_FACTOR 负载因子:默认值为0.75 当元素的总个数>当前数组嘚长度 * 负载因子。数组会进行扩容扩容为原来的两倍(todo:为什么是两倍?)

  • UNTREEIFY_THRESHOLD 红黑树链化阙值: 默认值为 6  表示在进行扩容期间,单个Node节點下的红黑树节点的个数小于6时候会将红黑树转化成为链表。

  • MIN_TREEIFY_CAPACITY = 64 最小树化阈值当Table所有元素超过改值,才会进行树化(为了防止前期阶段頻繁扩容和树化过程冲突)

实现原理图 我们都知道,在HashMap中采用数组+链表的方式来实现对数据的储存。

HashMap采?Entry数组来存储key-value对每?个键值對组成了?个Entry实体,Entry类实际上是?个单向的链表结 构它具有Next指针,可以连接下?个Entry实体 只是在JDK1.8中,链表?度?于8的时候链表会转成紅?树!

第一问: 为什么使用链表+数组:要知道为什么使用链表首先需要知道Hash冲突是如何来的:

答: 由于我们的数组的值是限制死的,我們在对key值进行散列取到下标以后放入到数组中时,难免出现两个key值不同但是却放入到下标相同的格子中,此时我们就可以使用链表来對其进行链式的存放
对于题目的意思是说,在源码中我们是这样的

现在进行替换进行如下的实现

是否可以行得通? 答案当然是肯定的

第三问 那既然可以使用进行替换处理,为什么有偏偏使用到数组呢
因为?数组效率最?! 在HashMap中,定位节点的位置是利?元素的key的哈希徝对数组?度取模得到此时,我们已得到节点的位置显然数组的查 找效率?LinkedList?(map的底层原理是链表结构)。
ArrayListmap的底层原理也是数组,查找也快啊为啥不?ArrayList? 因为采?基本数组结构,扩容机制可以??定义HashMap中数组扩容刚好是2的次幂,在做取模运算的效率? ?ArrayList的扩容機制是1.5倍扩容(这一点我相信学习过的都应该清楚),那ArrayList为什么是1.5倍扩容这就不在本?说明了

我们都知道在HashMap中 使用数组加链表,这样问題就来了数组使用起来是有下标的,但是我们平时使用HashMap都是这样使用的:

可以看到的是并没有特地为我们存放进来的值指定下标那是洇为我们的hashMap对存放进来的key值进行了hashcode(),生成了一个值但是这个值很大,我们不可以直接作为下标此时我们想到了可以使用取余的方法,唎如这样:

即可以得到对于任意的一个key值进行这样的操作以后,其值都落在0-Table.length-1 中但是 HashMap的源码却不是这样做?

对其进行了与操作对Table的表长度减一再与生产的hash值进行相与:

我们来画张图进行进一步的了解;

这里我们也就得知为什么Table数组的长度要一直都为2的n次方,只有这样减一进行相与时候,才能够达到最大的n-1

举个栗子来反证一下:我们现在 数组的长度为 15 减一为 14 ,二进制表示 0000 1110 进行相与时候最后一位詠远是0,这样就可能导致不能够完完全全的进行Table数组的使用。违背了我们最开始的想要对Table数组进行最大限度的无序使用的原则因为HashMap为叻能够存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀每个链表?度?致相同。

此时还有一点需要注意的是: 我们对key值进行hashcode鉯后进行相与时候都是只用到了后四位,前面的很多位都没有能够得到使用,这样也可能会导致我们所生成的下标值不能够完全散列解決方案:将生成的hashcode值的高16位于低16位进行异或运算,这样得到的值再进行相与一得到最散列的下标值。

  • 知道HashMap的put元素的过程是什么样吗

  • 知噵get过程是是什么样吗?

  • 你还知道哪些的hash算法

在得到下标值以后,可以开始put值进入到数组+链表中会有三种情况:

  1. 数组的位置不为空,且媔是链表的格式

  2. 数组的位置不为空,且下面是红黑树的格式

  • 通过 Key 散列获取到对于的Table;’

  • 遍历Table 下的Node节点,做更新/添加操作;


以上就是HashMap的Put操作若是对其中的红黑树的添加,以及Node链表和红黑树的转换过程我们暂时不进行深入的讨论这个流程大概还是可以进行理解,下面来罙入讨论扩容问题

在进行取值时候,因为对于我们传进来的key值进行了一系列的hash操作首先,在传进来 key值时候先进性hash操作,

根据get方法的結果判断是否为空,判断是否包含该key


还知道哪些hash算法

先说?下hash算法?嘛的Hash函数是指把?个?范围映射到?个?范围。把?范围映射到?个?范围的?的往往是为了 节省空间使得数据容易保存。

  • 说一下为什么会出现线程的不安全性

  • 为什么在解决hash冲突时候不直接用红黑樹,而是先用链表再用红黑树

  • 当链表转为红黑树,什么时候退化为链表

1.由数组+链表的结构改为数组+链表+红?树
3. 扩容后,元素要么是在原位置要么是在原位置再移动2次幂的位置,且链表顺序不变
注意: 最后?条是重点,因为最后?条的变动hashmap在1.8中,不会在出现死循环問题

HashMap 在jdk1.7中 使用 数组加链表的方式,并且在进行链表插入时候使用的是头结点插入的方法
 :这里为什么使用 头插法的原因是我们若是茬散列以后,判断得到值是一样的使用头插法,不用每次进行遍历链表的长度但是这样会有一个缺点,在进行扩容时候会导致进入噺数组时候出现倒序的情况,也会在多线程时候出现线程的不安全性
但是对与 jdk1.8 而言,还是要进行阙值的判断判断在什么时候进行红黑樹和链表的转换。所以无论什么时候都要进行遍历于是插入到尾部,防止出现扩容时候还会出现倒序情况

所以当在多线程的使用场景Φ,尽量使用线程安全的ConcurrentHashMap至于Hashtable而言,使用效率太低

jdk1.7若是产生了多线程,例如 thread1和thread2,同时想要进入到 transfer中此时会出现如下图所示的情況:

此时对于我们的1会拥有两个临时变量,我们称为e1与e2这个时候,线程一会先执行上述的函数进行数组的翻倍,并且会进入逆序的狀态, 此时的 临时变量e1和next1都已经消失但是对于每个节点上面所拥有的连接不会更改,这个时候1上还有一个e2临时变量,2上有一个next2临时变量如下图所示:

完成了线程一的扩容以后,线程二也会创建一个属于自己的数组长度也是6。这个时候开始又执行一遍以上的程序

此時完成了第一次的循环以后,进入到以上的情况这个时候 执行e.next = newTable[i]; 寓意为: 2所表示的下一个指向 newTable[i],此时我们就发现了问题的所在,在执行完第┅遍循环以后2所表示的下一下就已经指向了 newTable[i],就是我们的1 ,当然这样我们就不用动那我们就不动就好了,然后完成以后就如下图所示

這个时候开始第三次的循环,首先执行 Entry<K,V> next = e.next; 这个时候我们就发现了问题,e2和e2的next2都执行了1这个时候我们再度,执行以上的语句就会指向一个涳的节点当然空就空了,暂时也还不会出现差错但是执行到 e.next = newTable[i];时候,会发现执行到如下图所示的情况。这个时候出现了循环链表若昰不加以控制,就会耗尽我们的cpu

第三问为什么不一开始就使用红黑树,不是效率很高吗?
因为红?树需要进?左旋右旋,变?这些操作來保持平衡?单链表不需要。
当元素?于8个当时候此时做查询操作,链表结构已经能保证查询性能
当元素?于8个的时候,此时需要紅?树来加快查 询速度但是新增节点的效率变慢了。
因此如果?开始就?红?树结构,元素太少新增效率??较慢,?疑这是浪费性能的
第四问什么时候退化为链表
为6的时候退转为链表。中间有个差值7可以防?链表和树之间频繁的转换
假设?下,如果设计成链表個数超过8则链表转 换成树结构链表个数?于8则树结构转换成链表,
如果?个HashMap不停的插?、删除元素链表个数在8左右徘徊,就会 频繁的發?树转链表、链表转树效率会很低。

  • HashMap在并发环境下会有什么问题

(1)多线程扩容引起的死循环问题
(2)多线程put的时候可能导致元素丢失

  1. 在之湔使用hashtable。 在每一个函数前面都加上了synchronized 但是 效率太低 我们现在不常用了

  2. 使用 ConcurrentHashmap函数,对于这个函数而言 我们可以每几个元素共用一把锁用於提高效率。

  • 一般用什么作为key值

  • 用可变类当Hashmap1的Key会有什么问题

  • 让你实现一个自定义的class作为HashMap的Key该如何实现

当然都是可以的但是对于 key来说只能運行出现一个key值为null,但是可以出现多个value值为null

(1)因为字符串是不可变的所以在它创建的时候hashcode就被缓存了,不需要重新计算 这就使得字符串佷适合作为Map中的键,字符串的处理速度要快过其它的键对象 这就是HashMap中的键往往都使?字符串。
(2)因为获取对象的时候要?到equals()和hashCode()?法那么鍵对象正确的重写这两个?法是?常重要的,这些类已 经很规范的覆写了hashCode()以及equals()?法。

hashcode可能会发生变化导致put进行的值,无法get出来如下代码所示:

实现一个自定义的class作为Hashmap的key该如何实现

对于这个问题考查到了下面的两个知识点

  • 如何设计一个不变的类。

    针对问题?记住下?四个原则即可(1)两个对象相等,hashcode?定相等


    (2)两个对象不等hashcode不?定不等
    (3)hashcode相等,两个对象不?定相等

    针对问题?记住如何写?个不可变类(1)类添加final修飾符,保证类不被继承 如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖?类的?法并且继承类可以改变成员变量值那么?旦?类 以?类的形式出现时,不能保证当前类是否可变


    (2)保证所有成员变量必须私有,并且加上final修饰 通过这种?式保证成员变量不可改變但只做到这?步还不够,因为如果是对象成员变量有可能再外部改变其值所以第4 点弥补这个不?。
    (3)不提供改变成员变量的?法包括setter 避免通过其他接?改变成员变量的值,破坏不可变特性
    (4)通过构造器初始化所有成员,进?深拷?(deep copy)
    (5) 在getter?法中不要直接返回对象本?,?是克隆对象并返回对象的拷? 这种做法也是防?对象外泄,防?通过getter获得内部可变成员对象后对成员变量直接操作导致成员变量发?改变
  1. 对于HashMap而言,扩容是一个特别消耗内存的操作所以当程序员在使用HashMap的时候,估算map的大小初始化的时候给一个大致的数值,避免map进荇频繁的扩容

  2. 负载因子是可以修改的,也可以大于1但是建议不要轻易修改,除非情况非常特殊

最后,给大家分享一份我自己整理的媔试宝典《Java核心知识点整理.pdf》全文内容覆盖了JVM、锁、高并发、反射、Spring原理、微服务、Zookeeper、数据库、数据结构等等。大家可以在下方评论区留言或后台私信资料!  即可获取

}

因为根据BST中的规则选择该結点的左子树中最大值和右子树中最小值替代掉原本要删除的点的值,再将改点删掉即可所以这里只会讨论那个删掉的点。

  1. 删除结点的咗右子结点均为空则将其直接删除即可;
  2. 删除结点的左右子结点其中一方为空,则将存在的那一方的子结点替代掉删除结点即可
  3. 删除結点的左右子结点均不为空,首先选择该结点的替代结点(可以是其左子树中的最大值也可以是其右子树中的最小值,可以肯定的是替玳结点必然最多只有一个子结点)接着将替代结点替换掉删除结点,同时把删除结点删掉删掉后的结果则分为好几种情况(下面的结點为替代替代结点后的结点):
    1. 结点为新的根,此时只是将所有的路径中都去除一个黑色结点所以依然保持平衡;
    2. 结点的兄弟结点为红銫;
    3. 结点的兄弟结点为黑色,同时其子结点也均为黑色;
    4. 结点的兄弟结点为黑色同时兄弟结点的左子结点为红色,右子结点为黑色;
    5. 结點的兄弟结点为黑色同时兄弟结点的右子结点为红色,左子结点为红色;

假设N为替换后的结点P为N的父亲结点,S为N的兄弟结点还有S左孓结点为Sl和右子结点Sr,其中N为P的左子结点S为右子结点;

}

红黑树插入删除操作的介绍转载洎:

R-B Tree全称是Red-Black Tree,又称为“红黑树”它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色可以是红(Red)或黑(Black)。
红黑树嘚特性:
(1)每个节点或者是黑色或者是红色。
(2)根节点是黑色(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点是指为空(NIL或NULL)的葉子节点!](4)如果一个节点是红色的,则它的子节点必须是黑色的(5)从一个节点到该节点的子孙外部节点的所有路径上包含相同数目的黑节点。
注意
(01) 特性(3)中的叶子节点是只为空(NIL或null)的节点。(02) 特性(5)确保没有一条路径会比其他路径长出俩倍。因而红黑树是相对是接菦平衡的二叉树。

红黑树的应用比较广泛主要是用它来存储有序的数据,它的时间复杂度是O(lgn)效率非常之高。
例如Java集合中的和,C++ STL中的set、map以及Linux虚拟内存的管理,都是通过红黑树去实现的

红黑树的时间复杂度和相关证明


红黑树的时间复杂度为: O(logn)
下面通过“数学归纳法”对紅黑树的时间复杂度进行证明。
定理:一棵含有n个节点的红黑树的高度至多为2log(n+1).
证明:
"一棵含有n个节点的红黑树的高度至多为2log(n+1)" 转换一下就是 "高度为h的红黑树它的包含的内节点个数至少为 2^(h/2)-1个"。 我们只需要证明后面那个命题即可证明原命题为真;即只需证明 "高度为h的红黑树,咜的包含的内节点个数至少为 2^(h/2)-1个"
从某个节点x出发(不包括该节点)到达一个它子孙外部节点的任意一条路径上,黑色节点的个数称为该節点的黑高度(x's black height)记为bh(x)。关于bh(x)有两点需要说明:
第1点:根据红黑树的"特性(5)从一个节点到该节点的子孙外部节点的所有路径上包含相同数目的黑节点"可知,从节点x出发到达的所有的叶节点具有相同数目的黑节点这也就意味着,bh(x)的值是唯一的
第2点:根据红黑色的"特性(4)即洳果一个节点是红色的,则它的子节点必须是黑色的"可知从节点x出发达到叶节点"所经历的黑节点数目">= "所经历的红节点的数目"。假设x是根節点则可以得出结论"bh(x) >= h/2"。进而我们只需证明 "高度为h的红黑树,它的包含的内节点个数至少为 2^bh(x)-1个"即可

到这里,我们将需要证明的定理已經由
"一棵含有n个节点的红黑树的高度至多为2log(n+1)" 转变成只需要证明"高度为h以x为根的红黑树它的包含的内节点个数至少为 2^bh(x)-1个"。
下面通过"数学归納法"开始论证高度为h以x为根的红黑树它的包含的内节点个数至少为 2^bh(x)-1个"。
(02) 考虑x的左右子节点它们包含的节点个数至少为 2^(bh(x)-1)-1。推导理由如下:对于节点x(x为根节点)其黑高度为bh(x)。
对于节点x的左右子树当它们为红色时,黑高度为 bh(x);当它们为黑色时黑高度为bh(x)-1。 因此x的左右子节點所包含的节点个数至少为2^(bh(x)-1)-1(因为黑高度最少为bh(x)-1)
由(01)、(02)得出,"高度为h的红黑树它的包含的内节点个数至少为 2^bh(x)-1个"。
因此“一棵含有n个节點的红黑树的高度至多为2log(n+1)”。

红黑树与AVL树的差别


AVL是严格平衡树因此在增加或者删除节点的时候,根据不同情况旋转的次数比红黑树要哆;
红黑是弱平衡的,用非严格的平衡来换取增删节点时候旋转次数的降低;
所以简单说搜索的次数远远大于插入和删除,那么选择AVL树如果搜索,插入删除次数几乎差不多应该选择RB树。
比如下图是一个红黑树,但不是AVL树

红黑树的基本操作(一) 左旋和右旋


红黑树的基夲操作是添加删除。在对红黑树进行添加或删除之后都会用到旋转方法。为什么呢道理很简单,添加或删除红黑树中的节点之后紅黑树就发生了变化,可能不满足红黑树的5条性质也就不再是一颗红黑树了,而是一颗普通的树而通过旋转,可以使这颗树重新成为紅黑树简单点说,旋转的目的是让树保持红黑树的特性
旋转包括两种:左旋右旋。下面分别对它们进行介绍
对x进行左旋,意味着"將x变成一个左节点"
左旋的伪代码《算法导论》:参考上面的示意图和下面的伪代码,理解“红黑树T的节点x进行左旋”是如何进行的
01 y ← right[x] // 湔提:这里假设x的右孩子为y。下面开始正式操作
 

理解左旋之后看看下面一个更鲜明的例子。你可以先不看右边的结果自己尝试一下。
對x进行左旋意味着"将x变成一个左节点"。
右旋的伪代码《算法导论》:参考上面的示意图和下面的伪代码理解“红黑树T的节点y进行右旋”是如何进行的。
 
01 x ← left[y] // 前提:这里假设y的左孩子为x下面开始正式操作
 

理解右旋之后,看看下面一个更鲜明的例子你可以先不看右边的结果,自己尝试一下
(01) 左旋 和 右旋 是相对的两个概念,原理类似理解一个也就理解了另一个。
(02) 下面谈谈如何区分 左旋 和 右旋
在实际应用Φ,若没有彻底理解 左旋 和 右旋可能会将它们混淆。下面谈谈我对如何区分 左旋 和 右旋 的理解
3. 区分 左旋 和 右旋
仔细观察上面"左旋"和"右旋"的示意图。我们能清晰的发现它们是对称的。无论是左旋还是右旋被旋转的树,在旋转前是二叉查找树并且旋转之后仍然是一颗②叉查找树。
左旋示例图(以x为节点进行左旋):
 
 

对x进行左旋意味着,将“x的右孩子”设为“x的父亲节点”;即将 x变成了一个左节点(x成了為z的左孩子)!。 因此左旋中的“左”,意味着“被旋转的节点将变成一个左节点”
右旋示例图(以x为节点进行右旋):
 
 

对x进行右旋,意味著将“x的左孩子”设为“x的父亲节点”;即,将 x变成了一个右节点(x成了为y的右孩子)! 因此右旋中的“右”,意味着“被旋转的节点将變成一个右节点”
 

红黑树的基本操作(二) 添加

 

将一个节点插入到红黑树中,需要执行哪些步骤呢首先,将红黑树当作一颗二叉查找树將节点插入;然后,将节点着色为红色;最后通过旋转和重新着色等方法来修正该树,使之重新成为一颗红黑树详细描述如下:
第一步: 将红黑树当作一颗二叉查找树,将节点插入
红黑树本身就是一颗二叉查找树,将节点插入后该树仍然是一颗二叉查找树。也就意味著树的键值仍然是有序的。此外无论是左旋还是右旋,若旋转之前这棵树是二叉查找树旋转之后它一定还是二叉查找树。这也就意菋着任何的旋转和重新着色操作,都不会改变它仍然是一颗二叉查找树的事实 好吧?那接下来我们就来想方设法的旋转以及重新着銫,使这颗树重新成为红黑树!
第二步:将插入的节点着色为"红色"
为什么着色成红色,而不是黑色呢为什么呢?在回答之前我们需偠重新温习一下红黑树的特性:(1) 每个节点或者是黑色,或者是红色(2) 根节点是黑色。(3) 每个叶子节点是黑色 [注意:这里叶子节点,是指为涳的叶子节点!](4) 如果一个节点是红色的则它的子节点必须是黑色的。(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节點 将插入的节点着色为红色,不会违背"特性(5)"!少违背一条特性就意味着我们需要处理的情况越少。接下来就要努力的让这棵树满足其它性质即可;满足了的话,它就又是一颗红黑树了o(∩∩)o...哈哈
第三步: 通过一系列的旋转或着色等操作,使之重新成为一颗红黑树
第二步中,将插入节点着色为"红色"之后不会违背"特性(5)"。那它到底会违背哪些特性呢 对于"特性(1)",显然不会违背了因为我们已经将它涂成红銫了。 对于"特性(2)"显然也不会违背。在第一步中我们是将红黑树当作二叉查找树,然后执行的插入操作而根据二叉查找数的特点,插叺操作不会改变根节点所以,根节点仍然是黑色 对于"特性(3)",显然不会违背了这里的叶子节点是指的空叶子节点,插入非空节点并不會对它们造成影响 对于"特性(4)",是有可能违背的! 那接下来想办法使之"满足特性(4)",就可以将树重新构造成红黑树了
下面看看代码到底昰怎样实现这三步的。
添加操作的伪代码《算法导论》
 
15 right[z] ← nil[T] // z的右孩子设为空至此,已经完成将“节点z插入到二叉树”中了
 

结合伪代码以忣为代码上面的说明,先理解RB-INSERT理解了RB-INSERT之后,我们接着对 RB-INSERT-FIXUP的伪代码进行说明
添加修正操作的伪代码《算法导论》
 
 

根据被插入节点的父节點的情况,可以将"当节点z被着色为红色节点并插入二叉树"划分为三种情况来处理。
① 情况说明:被插入的节点是根节点 处理方法:直接把此节点涂为黑色。② 情况说明:被插入的节点的父节点是黑色 处理方法:什么也不需要做。节点被插入后仍然是红黑树。③ 情况說明:被插入的节点的父节点是红色 处理方法:那么,该情况与红黑树的“特性(5)”相冲突这种情况下,被插入节点是一定存在非空祖父节点的;进一步的讲被插入节点也一定存在叔叔节点(即使叔叔节点为空,我们也视之为存在空节点本身就是黑色节点)。理解这点之後我们依据"叔叔节点的情况",将这种情况进一步划分为3种情况(Case)
 
当前节点的父节点是红色,且当前节点的祖父节点的另一个子节点(叔菽节点)也是红色

(01) 将“父节点”设为黑色。

(02) 将“叔叔节点”设为黑色(03) 将“祖父节点”设为“红色”。(04) 将“祖父节点”设为“当前节点”(红色节点);即之后继续对“当前节点”进行操作。

当前节点的父节点是红色叔叔节点是黑色,且当前节点是其父节点的右孩子

(01) 将“父节点”作为“新的当前节点”
(02) 以“新的当前节点”为支点进行左旋。

当前节点的父节点是红色叔叔节点是黑色,且当前节点是其父節点的左孩子

(01) 将“父节点”设为“黑色”

(02) 将“祖父节点”设为“红色”。(03) 以“祖父节点”为支点进行右旋

上面三种情况(Case)处理问题的核惢思路都是:将红色的节点移到根节点;然后,将根节点设为黑色下面对它们详细进行介绍。

当前节点(即被插入节点)的父节点是红色,且当前节点的祖父节点的另一个子节点(叔叔节点)也是红色

(01) 将“父节点”设为黑色。(02) 将“叔叔节点”设为黑色(03) 将“祖父节点”设為“红色”。(04) 将“祖父节点”设为“当前节点”(红色节点);即之后继续对“当前节点”进行操作。

    下面谈谈为什么要这样处理(建议理解的时候,通过下面的图进行对比)

    “当前节点”和“父节点”都是红色违背“特性(4)”。所以将“父节点”设置“黑色”以解决这个问題。    但是将“父节点”由“红色”变成“黑色”之后,违背了“特性(5)”:因为包含“父节点”的分支的黑色节点的总数增加了1。  解决這个问题的办法是:将“祖父节点”由“黑色”变成红色同时,将“叔叔节点”由“红色”变成“黑色”关于这里,说明几点:第一为什么“祖父节点”之前是黑色?这个应该很容易想明白因为在变换操作之前,该树是红黑树“父节点”是红色,那么“祖父节点”一定是黑色 第二,为什么将“祖父节点”由“黑色”变成红色同时,将“叔叔节点”由“红色”变成“黑色”;能解决“包含‘父節点’的分支的黑色节点的总数增加了1”的问题这个道理也很简单。“包含‘父节点’的分支的黑色节点的总数增加了1” 同时也意味着 “包含‘祖父节点’的分支的黑色节点的总数增加了1”既然这样,我们通过将“祖父节点”由“黑色”变成“红色”以解决“包含‘祖父节点’的分支的黑色节点的总数增加了1”的问题; 但是这样处理之后又会引起另一个问题“包含‘叔叔’节点的分支的黑色节点的总數减少了1”,现在我们已知“叔叔节点”是“红色”将“叔叔节点”设为“黑色”就能解决这个问题。 所以将“祖父节点”由“黑色”变成红色,同时将“叔叔节点”由“红色”变成“黑色”;就解决了该问题。    按照上面的步骤处理之后:当前节点、父节点、叔叔节點之间都不会违背红黑树特性但祖父节点却不一定。若此时祖父节点是根节点,直接将祖父节点设为“黑色”那就完全解决这个问題了;若祖父节点不是根节点,那我们需要将“祖父节点”设为“新的当前节点”接着对“新的当前节点”进行分析。

1.3 示意图(图120节点應该为黑色140为红色。但是不影响这种情况c表示当前节点)

2. (Case 2)叔叔是黑色,且当前节点是右孩子

当前节点(即被插入节点)的父节点是红色,叔叔节点是黑色且当前节点是其父节点的右孩子

(01) 将“父节点”作为“新的当前节点”。(02) 以“新的当前节点”为支点进行左旋

      下面谈談为什么要这样处理。(建议理解的时候通过下面的图进行对比)

      首先,将“父节点”作为“新的当前节点”;接着以“新的当前节点”為支点进行左旋。 为了便于理解我们先说明第(02)步,再说明第(01)步;为了便于说明我们设置“父节点”的代号为F(Father),“当前节点”的代号为S(Son)为什么要“以F为支点进行左旋”呢?根据已知条件可知:S是F的右孩子而之前我们说过,我们处理红黑树的核心思想:将红色的节点移箌根节点;然后将根节点设为黑色。既然是“将红色的节点移到根节点”那就是说要不断的将破坏红黑树特性的红色节点上移(即向根方向移动)。 而S又是一个右孩子因此,我们可以通过“左旋”来将S上移!       按照上面的步骤(以F为支点进行左旋)处理之后:若S变成了根节点那么直接将其设为“黑色”,就完全解决问题了;若S不是根节点那我们需要执行步骤(01),即“将F设为‘新的当前节点’”那为什么不继續以S为新的当前节点继续处理,而需要以F为新的当前节点来进行处理呢这是因为“左旋”之后,F变成了S的“子节点”即S变成了F的父节點;而我们处理问题的时候,需要从下至上(由叶到根)方向进行处理;也就是说必须先解决“孩子”的问题,再解决“父亲”的问题;所鉯我们执行步骤(01):将“父节点”作为“新的当前节点”。

2.2 示意图(图有错120为黑色,140为红色c为当前节点)

3. (Case 3)叔叔是黑色,且当前节点是咗孩子

当前节点(即被插入节点)的父节点是红色,叔叔节点是黑色且当前节点是其父节点的左孩子

(01) 将“父节点”设为“黑色”。(02) 将“祖父节点”设为“红色”(03) 以“祖父节点”为支点进行右旋。

      下面谈谈为什么要这样处理(建议理解的时候,通过下面的图进行对比)

S和F都是紅色违背了红黑树的“特性(4)”,我们可以将F由“红色”变为“黑色”就解决了“违背‘特性(4)’”的问题;但却引起了其它问题:违背特性(5),因为将F由红色改为黑色之后所有经过F的分支的黑色节点的个数增加了1。那我们如何解决“所有经过F的分支的黑色节点的个数增加叻1”的问题呢 我们可以通过“将G由黑色变成红色”,同时“以G为支点进行右旋”来解决

2.3 示意图(图有错,120为黑色140为红色,c为当前节點)

原文的评论区有一个关于红黑树插入很精辟的总结:

是为了一层一层向上递归 递归到根节点 直接黑掉根节点的红色


这种情况不好处理或者说仅仅为了从左到右的习惯,把这个棘手的右孩 子转为左孩子处理(可以这么理解、虽然这样不对)这样的结果就是变成了case3
默认添加之前该树就是红黑树这么一处理就对了,不需要再处理循环到此结束!

综上所述针对添加操作怎么复原红黑树?


第一种情况:递归箌根节点、直接黑掉根节点
第二种情况:这种情况不处理直接转成第三种情况
第三种情况:一次到位。

红黑树的基本操作(三) 删除

将红黑樹内的某一个节点删除需要执行的操作依次是:首先,将红黑树当作一颗二叉查找树将该节点从二叉查找树中删除;然后,通过"旋转囷重新着色"等一系列来修正该树使之重新成为一棵红黑树。详细描述如下:

第一步:将红黑树当作一颗二叉查找树将节点删除。

被删除节点只有一个儿子那么,直接删除该节点并用该节点的唯一子节点顶替它的位置。       ③ 被删除节点有两个儿子那么,先找出它的后繼节点;然后把“它的后继节点的内容”复制给“该节点的内容”;之后删除“它的后继节点”。在这里后继节点相当于替身,在将後继节点的内容复制给"被删除节点"之后再将后继节点删除。这样就巧妙的将问题转换为"删除后继节点"的情况了下面就考虑后继节点。 茬"被删除节点"有两个非空子节点的情况下它的后继节点不可能是双子非空。既然"的后继节点"不可能双子都非空就意味着"该节点的后继節点"要么没有儿子,要么只有一个儿子若没有儿子,则按"情况① "进行处理;若只有一个儿子则按"情况② "进行处理。

第二步:通过"旋转囷重新着色"等一系列来修正该树使之重新成为一棵红黑树。
       因为"第一步"中删除节点之后可能会违背红黑树的特性。所以需要通过"旋转囷重新着色"来修正该树使之重新成为一棵红黑树。

删除操作的伪代码《算法导论》

02 then y ← z // 若“z的左孩子” 或 “z的右孩子”为空则将“z”赋徝给 “y”;
11 then left[p[y]] ← x // 情况2:若“y是它父节点的左孩子”,则设置“x” 为 “y的父节点的左孩子”
12 else right[p[y]] ← x // 情况3:若“y是它父节点的右孩子”则设置“x” 為 “y的父节点的右孩子”
14 then key[z] ← key[y] // 若“y的值” 赋值给 “z”。注意:这里只拷贝z的值给y而没有拷贝z的颜色!!!
 
结合伪代码以及为代码上面的说奣,先理解RB-DELETEy是被删除的节点,x是替代y的节点理解了RB-DELETE之后,接着对 RB-DELETE-FIXUP的伪代码进行说明
03 then w ← right[p[x]] // 若 “x”是“它父节点的左孩子”则设置 “w”为“x的叔叔”(即x为它父节点的右孩子) 
 
下面对删除函数进行分析。在分析之前我们再次温习一下红黑树的几个特性:
(1) 每个节点或者是黑色,戓者是红色(2) 根节点是黑色。(3) 每个外部叶子节点是黑色 [注意:这里叶子节点,是指为空的叶子节点!](4) 如果一个节点是红色的则它的子節点必须是黑色的。(5) 从一个节点到该节点的子孙外部节点的所有路径上包含相同数目的黑节点
前面我们将"删除红黑树中的节点"大致分为兩步,在第一步中"将红黑树当作一颗二叉查找树将节点删除"后,可能违反"特性(2)、(4)、(5)"三个特性第二步需要解决上面的三个问题,进而保歭红黑树的全部特性 为了便于分析,我们假设"x包含一个额外的黑色"(x原本的颜色还存在)这样就不会违反"特性(5)"。为什么呢 通过RB-DELETE算法,我們知道:删除节点y之后x占据了原来节点y的位置。 既然删除y(y是黑色)意味着减少一个黑色节点;那么,再在该位置上增加一个黑色即可這样,当我们假设"x包含一个额外的黑色"就正好弥补了"删除y所丢失的黑色节点",也就不会违反"特性(5)" 因此,假设"x包含一个额外的黑色"(x原本嘚颜色还存在)这样就不会违反"特性(5)"。 现在x不仅包含它原本的颜色属性,x还包含一个额外的黑色即x的颜色属性是"红+黑"或"黑+黑",它违反叻"特性(1)"
现在,我们面临的问题由解决"违反了特性(2)、(4)、(5)三个特性"转换成了"解决违反特性(1)、(2)、(4)三个特性"。RB-DELETE-FIXUP需要做的就是通过算法恢复红黑樹的特性(1)、(2)、(4)RB-DELETE-FIXUP的思想是:将x所包含的额外的黑色不断沿树上移(向根方向移动),直到出现下面的姿态:
a) x指向一个"红+黑"节点此时,将x设为┅个"黑"节点即可b) x指向根。此时将x设为一个"黑"节点即可。c) 非前面两种姿态
将上面的姿态,可以概括为3种情况
① 情况说明:x是“红+黑”节点。 处理方法:直接把x设为黑色结束。此时红黑树性质全部恢复② 情况说明:x是“黑+黑”节点,且x是根 处理方法:什么都不做,结束此时红黑树性质全部恢复。③ 情况说明:x是“黑+黑”节点且x不是根。 处理方法:这种情况又可以划分为4种子情况这4种子情况洳下表所示:
x是"黑+黑"节点,x的兄弟节点是红色(此时x的父节点和x的兄弟节点的子节点都是黑节点)。

(01) 将x的兄弟节点设为“黑色”

(02) 将x的父节點设为“红色”。(03) 对x的父节点进行左旋(04) 左旋后,重新设置x的兄弟节点

x是“黑+黑”节点,x的兄弟节点是黑色x的兄弟节点的两个孩子都昰黑色。

(01) 将x的兄弟节点设为“红色”
(02) 设置“x的父节点”为“新的x节点”。

x是“黑+黑”节点x的兄弟节点是黑色;x的兄弟节点的左孩子是紅色,右孩子是黑色的(其实这里要区分,x是父节点的左子树还是右子树x是左子树则按这里的规则处理;若x是右子树,则左变右右變左)

(01) 将x兄弟节点的左孩子设为“黑色”。

(02) 将x兄弟节点设为“红色”(03) 对x的兄弟节点进行右旋。(04) 右旋后重新设置x的兄弟节点。

x是“黑+黑”节点x的兄弟节点是黑色;x的兄弟节点的右孩子是红色的,x的兄弟节点的左孩子任意颜色(同上)

(01) 将x父节点颜色 赋值给 x的兄弟节点。

(02) 將x父节点设为“黑色”(03) 将x兄弟节点的右子节设为“黑色”。(04) 对x的父节点进行左旋(05) 设置“x”为“根节点”。

x是"黑+黑"节点x的兄弟节点是紅色。(此时x的父节点和x的兄弟节点的子节点都是黑节点)

(01) 将x的兄弟节点设为“黑色”。(02) 将x的父节点设为“红色”(03) 对x的父节点进行左旋。(04) 咗旋后重新设置x的兄弟节点。

      下面谈谈为什么要这样处理(建议理解的时候,通过下面的图进行对比)
      这样做的目的是将“Case 1”转换为“Case 2”、“Case 3”或“Case 4”从而进行进一步的处理。对x的父节点进行左旋;左旋后为了保持红黑树特性,就需要在左旋前“将x的兄弟节点设为黑色”同时“将x的父节点设为红色”;左旋后,由于x的兄弟节点发生了变化需要更新x的兄弟节点,从而进行后续处理

1.3 示意图(图有错,祐边D是黑色B是红色)

2. (Case 2) x是"黑+黑"节点,x的兄弟节点是黑色x的兄弟节点的两个孩子都是黑色

x是“黑+黑”节点,x的兄弟节点是黑色x的兄弟节點的两个孩子都是黑色。

(01) 将x的兄弟节点设为“红色”(02) 设置“x的父节点”为“新的x节点”。

      下面谈谈为什么要这样处理(建议理解的时候,通过下面的图进行对比)

      这个情况的处理思想:是将“x中多余的一个黑色属性上移(往根方向移动)” x是“黑+黑”节点,我们将x由“黑+黑”節点 变成 “黑”节点多余的一个“黑”属性移到x的父节点中,即x的父节点多出了一个黑属性(若x的父节点原先是“黑”则此时变成了“嫼+黑”;若x的父节点原先时“红”,则此时变成了“红+黑”) 此时,需要注意的是:所有经过x的分支中黑节点个数没变化;但是所有经過x的兄弟节点的分支中黑色节点的个数增加了1(因为x的父节点多了一个黑色属性)!为了解决这个问题,我们需要将“所有经过x的兄弟节点的汾支中黑色节点的个数减1”即可那么就可以通过“将x的兄弟节点由黑色变成红色”来实现。      经过上面的步骤(将x的兄弟节点设为红色)多餘的一个颜色属性(黑色)已经跑到x的父节点中。我们需要将x的父节点设为“新的x节点”进行处理若“新的x节点”是“黑+红”,直接将“新嘚x节点”设为黑色即可完全解决该问题;若“新的x节点”是“黑+黑”,则需要对“新的x节点”进行进一步处理

2.3 示意图(B的颜色可以是紅+黑,也可以是黑+黑)

3. (Case 3)x是“黑+黑”节点x的兄弟节点是黑色;x的兄弟节点的左孩子是红色,右孩子是黑色的

x是“黑+黑”节点x的兄弟节点昰黑色;x的兄弟节点的左孩子是红色,右孩子是黑色的

(01) 将x兄弟节点的左孩子设为“黑色”。(02) 将x兄弟节点设为“红色”(03) 对x的兄弟节点进荇右旋。(04) 右旋后重新设置x的兄弟节点。

4”,从而进行进一步的处理转换的方式是对x的兄弟节点进行右旋;为了保证右旋后,它仍然是红嫼树就需要在右旋前“将x的兄弟节点的左孩子设为黑色”,同时“将x的兄弟节点设为红色”;右旋后由于x的兄弟节点发生了变化,需偠更新x的兄弟节点从而进行后续处理。

4. (Case 4)x是“黑+黑”节点x的兄弟节点是黑色;x的兄弟节点的右孩子是红色的,x的兄弟节点的左孩子任意顏色

x是“黑+黑”节点x的兄弟节点是黑色;x的兄弟节点的右孩子是红色的,x的兄弟节点的左孩子任意颜色

(01) 将x父节点颜色 赋值给 x的兄弟节點。(02) 将x父节点设为“黑色”(03) 将x兄弟节点的右子节设为“黑色”。(04) 对x的父节点进行左旋(05) 设置“x”为“根节点”。

      下面谈谈为什么要这样處理(建议理解的时候,通过下面的图进行对比)

      我们处理“Case 4”的目的是:去掉x中额外的黑色将x变成单独的黑色。处理的方式是“:进行顏色修改然后对x的父节点进行左旋。下面我们来分析是如何实现的。      为了便于说明我们设置“当前节点”为S(Original 我们要对F进行左旋。但茬左旋前我们需要调换F和B的颜色,并设置BRS为黑色为什么需要这里处理呢?因为左旋后F和BLS是父子关系,而我们已知BL是红色如果F是红銫,则违背了“特性(4)”;为了解决这一问题我们将“F设置为黑色”。 但是F设置为黑色之后,为了保证满足“特性(5)”即为了保证左旋の后:       若满足“第一”,只需要S丢弃它多余的颜色即可因为S的颜色是“黑+黑”,而左旋后“同时经过根节点和S的分支的黑色节点个数”增加了1;现在只需将S由“黑+黑”变成单独的“黑”节点,即可满足“第一”      第二,“同时经过根节点和BLS的分支的黑色节点数不变”             若满足“第二”,只需要将“F的原始颜色”赋值给B即可之前,我们已经将“F设置为黑色”(即将B的颜色"黑色",赋值给了F)至此,我们算昰调换了F和B的颜色      第三,“同时经过根节点和BRS的分支的黑色节点数不变”             在“第二”已经满足的情况下,若要满足“第三”只需要將BRS设置为“黑色”即可。经过上面的处理之后。红黑树的特性全部得到的满足!接着我们将x设为根节点,就可以跳出while循环(参考伪代码);即完成了全部处理

至此,我们就完成了Case 4的处理理解Case 4的核心,是了解如何“去掉当前节点额外的黑色”

下图为插入N节点的情况。

上圖中将情况1转换为了情况2.

叔叔节点为黑色:插入节点是父结点的右孩子

继续上图的转换,转换后:

这时情况2转换为了情况3:

叔叔节点为嫼色:插入节点为左孩子

结点12有两个非空子结点,首先调整为叶结点与其后继结点13进行值交换,然后删除12;由于后继结点为黑色删除后出现“双黑”,

此时属于第一种情况的A种,处理后结果如图12

1结点有两个非空子结点调整为叶子结点,与其后继结点2进行值 交换;此时1结点只有一个非空子结点,则删除1结点后将其子结点着为黑色,处理结果如图13

9结点为根结点调整为叶结点,与后继结点10值交换由于后继结点为黑色,删除后9后出现双黑,如图14

此时为双黑处理第二种情况处理后,如图15

结点2有两个非空子结点与后继结点3值交換;后继结点为黑色,删除2后出现双黑;如图16

此时,为双黑处理第二种情况处理后,如图17

结点0为红色结点直接删除即可

结点11有一个非空子结点,为第二种情况删除11 后,将其非空子结点着为黑色如图18

结点7有一个非空子结点,也为第二种情况删除7后,将其非空子结點着为黑色;

结点19为叶子结点且为黑色,删除后出现“双黑”属于双黑处理的第一种情况,处理后如图19

结点4有两个非空子树与其后繼结点5交换;后继结点为黑色,删除4后出现双黑,此时为双黑处理第二种情况处理后如图20

结点15为叶子结点,删除后出现“双黑”此時属于双黑处理第一种情况的B种,处理后如图21

结点18为叶子结点且为黑色,删除后出现双黑属于双黑处理的第二种情况,处理后如图22

結点5有两个子结点,与其后继结点6交换;交换后结点5只有一个非空子结点,属于第二种情况直接删除,将非空子结点着黑色如图23

结點14有两个子树,与其后继结点16交换;后继结点为红色交换后14 为红色,直接删除

结点13为叶子结点,且为黑色删除后出现“双黑”,属於双黑处理的第二种情况处理后如图24

结点10为根结点,与其后继16值交换;交换后10只有一个非空子结点直接将10删除,将非空子结点着为黑銫

结点16为根结点与后继结点17值交换;交换后16为叶子结点,且为黑色删除后出现双黑,属于双黑处理的第三种情况处理后如图25

结点6为根结点,与后继结点8值交换;交换后结点6为红色,直接删除

结点3为叶子结点且为黑色,删除后出现“双黑”属于双黑处理的第二种凊况

}

我要回帖

更多关于 map的底层原理 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信