对JAVA的TreeMap深入理解

今天我们来分析TreeMap的底层原理,深入了解TreeMap底层是如何实现的。在分析源码之前我们先来分析红黑树这种数据结构,这样我们呢才能看明白TreeMap的底层原理。

在讲红黑树之间我们先来讲一下二叉搜索树,因为红黑树就是二叉搜索树的一种形式。二叉搜索树就是该二叉树的每一个节点都大于它左子树的所有节点,小于它所有右子树的节点,画一个图来说明。

我们可已发现6大于它所有左子树的节点,小于它所有右子树的节点,且左右子树的节点依旧保持这个性质,这就是二叉搜索树。

我们继续来讲红黑树,首先先说明几个属于红黑树的概念每个叶子节点包含五个属性color 节点颜色,key 节点关键字,left 左孩子结点,right 右孩子节点,p 父节点,我们定义一个节点没有子节点或者父节点,则该相应的指针为NIL即空值。然后说明红黑树的五个规则:

1每个节点是红色或者黑色。

2根节点是黑色。

3每个NIL节点是黑色

4如果一个节点是红色,那它的孩子节点一定是黑色

5从根节点到每个叶子节点的路径上存在的黑色节点个数相同

对于NIL节点即空节点我们定义一个哨兵T.nil来表示所有的NIL节点,这要可以节省空间,红黑树的叶子节点是都是NIL节点这个要特别注意,然后说明黑高的概念由于从根节点到每个叶子节点的路径上存在的黑色节点个数相同,所以定义黑色节点数为这个红黑树的黑高。下面我画一颗红黑树方便大家理解。

现在你应该已经了解了什么是红黑树,所以我们接下来看Java的TreeMap源码,将红黑树结合着Java具体实现即TreeMap源码我们可以理解更清晰,我们先来看TreeMap的数据成员,

我们发现有四个数据成员comparatar,这个是用来比较键值因为红黑树中的节点具有顺序,所以我们需要用comparator对象来比较顺序,然后的第二个数组成员root,它显然就是红黑树的根节点,我们具体来看Entry的定义,非常的明显,它对应了我们上面讲的红黑树节点的定义,只是多了一个value,这是我们想存储的数据,第三个是数据成员size,明显是树的节点数量,modCount是java中用来保证快速失败机制的一个变量,详细解释看我这篇文章的补充知识。

补充知识icon-default.png?t=N7T8https://blog.csdn.net/weixin_61854715/article/details/140229431

这里我们结合TreeMap源码分析了红黑树的定义,接下来我们讲红黑树的操作。

第一个操作旋转,分为左旋和右旋,旋转操作是一种能保持二叉搜索树性质的操作画一张图来解释一下旋转操作。

x,y为两个节点,a,b,c分别为三颗子树,左旋就是选择一个子树,传入该子树的根节点让它的右节点变成该子树的根节点,自己变成左节点,右旋就是该子树根节点让它的左子树变成根节点,自己变成右节点,无论是左旋还是右旋都不能改变其二叉搜索树的性质即,

a的所有节点键值<x的键值<b的所有节点键值<y的键值<c的所有节点键值。

我么再来看TreeMap源码来看它是怎么实现这个操作的,

这里我们只分析右旋,左旋同理可得。首先判断传入子树的根节点是否为空,即p!=null,定义一个l节点来记录p的左节点,即图中的x,然后把p的左节点定义为l的右节点,即上图中b子树变成了y节点的左节点,,然后判断r的左节点是否为空,即图中的a子树是否为空,不为空我们就设置l的父节点为p的父节点,即x设置为该子树的根节点,然后判断p有没有父节点,没有的话就设置该红黑树的根节点为l,接下来两个判断是判断p是父节点的左节点,还是右节点,是右节点的话,就将p的父节点的右节点赋为l,左节点的话就将p的父节点的左节点赋为l,其实这整个判断就是对应图中x变成该子树新的根节点,后面两句将l的右节点变为p,对应图中y变为x的右节点,p的父节点为l,对应图中x变成了y的父节点。

接着我们来介绍红黑树第二个操作插入元素,介绍这个操作我们直接结合着源码来看:

这个函数非常长我们分块来看,第一块比较简单,首先使用t记录根节点,假如t为空,即该红黑树是一个空树,调用compare函数进行键值比较,这里并不是为了比较大小,而是为了检查comparator数据成员是否已经初始化了。接着将数据封装进节点,并将该红黑树的根节点root定义为这个新封装的节点。TreeMap容量+1即size=1,modCount++这句不在解释,看上面链接的补充知识,返回null。这就是对插入时是空树的时候的操作。后面定义了一个int变量cmp,定义了一个parent节点,定义了一个比较器cpr初始化为数据成员comparator。

我们继续分析第二块代码。首先判断cpr是否为空,即comparator是否被赋值,不为空就是开始一个循环,其目的就是找到插入节点的一个位置,其方法很简单就是从根节点开始比较键值大于就走左边路径,小于就走右边路径,直到找到一个空节点,就是该节点插入位置,假如在查找过程中发现键值已经存在,就更新里面的值并返回旧值。后面的else分支目的其实是一样的,只是键值的比较方法不同,因为没有定义comparator数据成员,所以使用的是comparable接口里的compareTo方法。接下来定义一个插入节点e我们通过判断cmp可以判断该节点时左节点还是右节点,<0就是左节点,大于0就是右节点,这样e就被插入到对应位置,但我们可以想象插入一个节点e是会破坏红黑树的性质的,所以我们之后调用了一个fixAfterInsertion方法来修复红黑树,即使它的颜色性质依然符合红黑树。我们来看一下这个方法是如何修复红黑树的性质的。

该方法首先将传入x节点的颜色设置为红色。在继续看之前我们先思考插入一个红节点会破坏红黑树的五个性质中的哪些性质。

1每个节点是红色或者黑色。

2根节点是黑色。

3每个NIL节点是黑色

4如果一个节点是红色,那它的孩子节点一定是黑色

5从根节点到每个叶子节点的路径上存在的黑色节点个数相同

性质1很明显不会被破坏,性质2很明显会被破坏,假如正好插入到根节点那么就不满足第二个性质了,性质3很明显不会被破坏,性质4很明显会被破坏,假如插入位置的父节点是一个红节点就破坏了性质4,性质5不会被破坏,因为我们插入的是一个红色节点,这个红色节点仅仅是取代了一个NIL节点然后增加了两个NIL节点,相当于新增了两条路径,黑色节点数显然没有增加。所以说当插入节点时只可能破坏性质2和性质4。

我们继续看源码首先是一个while循环前两跳出循环的条件不太重要,就是排除红黑树是一颗空树对的情况已经传入进来的节点不是空节点。后面跳出循环的条件才最为重要,它是只有在x父节点的颜色是红的情况下才跳出循环。,这个循环修改的是性质4被破坏的情况,接下来比较复杂,需要仔细思考才能理解红黑树是怎么被修复的。

这个循环首先将情况分成两个大类,即最外层的判断是哪两种情况呢,为表述更清晰,我们设插入节点为x,x的父节点为y,y的父节点z,第一种情况就是y是z左节点,,第二种情况是y是z的右节点节点,为什么我们需要考虑三代节点,原因很简单,由于性质4被破坏,即x和y都是红色,我们假如只通过x,y,变色一定是解决不了问题的,无论是把xy变成红黑或者是黑红或者是黑黑,都不能使其恢复红黑树的性质。所以我们一定要考虑三代节点,保证以z节点的子树恢复成红黑树,然后在考虑z的祖父节点为红黑树的子树能否满足性质,直到整棵红黑树性质完全恢复,所以大情况分为y是z左节点,,y是z的右节点节这两个情况是对称的。

我们来看判断语句的第一个分支,首先定义了一个y节点,y是x的父节点的父节点的右孩子节点

判断y是不是红色,这就是情况1,我来画个图:

4是x节点即插入节点,8是y节点且是红色节点,我们来看这种情况代码是怎么处理的。

首先设置x的父节点为黑色即5节点,y节点为黑色即8节点,然后在设置x父节点的父节点即7节点为红色,最后赋值x为x父节点的父节点即7号节点。我们来看树经过变化后变成了什么样子。

很明显现在以x为根节点的子树已经恢复了红黑的性质,但2,7再次破坏了红黑树的性质,我们进行第二次循环首先定义y为x节点父节点的父节点,即14号节点,很明显14为黑节点,所以进入了else分支,这个分支里有进行了一个判断x节点是父节点的右节点,如我们下面画的情况二。

对情况二的处理明显是先将x变为x的父节点,然后对x节点进行左旋,变成情况三进行处理我们来画一下情况三:

然后看情况三是源码是如何处理的:

先设置x父节点为黑色即7号节点,接着设置x父节点的父节点为红色即11号节点,然后对x的父节点的父节点进行右旋,画个图看一下变化后的树是怎样的。

很明显树已经恢复了红黑树的性质,且我们要知道只要执行了情况二和三就代表着红黑树即将恢复性质,因为x的我们设置了x的父节点为黑色,这样保证了不会再出现两个红色节点连在一起的情况。这个分支我么就分析完了。第二个大的情况同理可得,留给读者自行分析,检验自己是否已经理解了红黑树是如何修复的。

到这我们讲完,了红黑树的插入,我们继续来讲红黑树的删除,我们来看TreeMap的源码,

首先传入一个Object的对象,定义p节点使用getEntry获取该键值的节点,getEntry方法源码如下

获取方法很简单就是从根节点开始比较传入的键值,小于就往左节点走,否则往右节点走,直到找到了该节点,找不到就返回空节点null,上面源码功能就是这个意思,比较简单这里就不在分析。我们回到remove方法。

获取到p节点,判断为不为空,为空的话就直接返回空值,不为空的话就使用oldValue记录找到的值,然后使用deleteEntry来删除该节点。我们来看该方法的源码。这也是一个非常长的方法我们一块一块的去看。

modCount++使实现快速失败机制的变量,了解原理看上面的链接文章,size--即减少一个红黑树的一个容量,接着判断删除的p节点左右节点是不是都不为空。两者都不为空,就定义获取p节点的后继节点,所为后继节点就是大于p节点关键字最接近p节点的节点。

这段代码就是TreeMap中实现寻找后继节点的代码,代码解释如下:

参数检查:首先检查传入的节点t是否为null。如果是,那么就没有后继节点,直接返回null。
右子树存在的情况:
如果节点t有右子节点,那么后继节点一定在t的右子树中。在右子树中,从根节点开始,不断向左子节点移动,直到没有左子节点为止。这个过程会找到右子树中的最小节点,也就是t的中序后继。
右子树不存在的情况:
如果节点t没有右子节点,那么后继节点可能是t的某个祖先节点。从t的父节点开始向上遍历,直到找到一个节点p,使得t是p的左子节点。这个p节点就是t的中序后继。如果一直遍历到根节点都没有找到这样的p(即t是整棵树的最大节点),那么t没有后继节点,但在这个实现中,由于p是从t.parent开始赋值的,并且会在循环中更新,当p为null时循环结束,此时p本身就是null,所以可以直接返回p(即null)。

我们回到deleteEntry方法第一块:

由于p节点有右子树所以后继节点一定在p的右子树中,在s获得p的后继节点位置后,直接将p的key值赋值为s的key值,p的value值赋值为s的value值。然后将p节点赋为s节点。

我们继续来看第二段代码块首先定义了一个replacement节点,判断p的左节点是否为空,不为空就初始化replacement为p.left,否则为p.right。replacement指向可能的情况是删除节点p只有一个子节点或者没有子节点的时候,replacement指向的是p的子节点或者是null,假如删除节点p有两个子节点的时候,经过上面的一个判断后,删除节点里的值已经变成了后继节点里的的值,无论是键值还是存储值,p已经指向了它的后继节点,后继节点只有可能有右节点,因为它是该子树的最小值,所以此时replacment只会初始p.right或null。

我们来看第一个判断,这个判断是四种情况,即删除节点只有一个孩子节点和两个孩子节点。画图说明:

情况一的replacement是r节点,p节点是z节点,情况二的replacement是l节点p节点是z节点,情况三的replacement是x节点,p节点是y节点,情况四的replacement是x节点,p节点是y节点,我们发现这四种情况都是首先将replacement的父节点设置为p的父节点,在这句话情况1,2已经完成了连接,然后判断p是不是p的父节点的左节点对应情况4,然后将p的父节点设置为replacement,最后一个分支对应情况3,将p的父节点的右孩子节点设置为replacement。这段代码就完成了四种情况的连接。

接下来是p节点的左节点,右节点,父节点都为空,即将p点剔除红黑树。然后假如p点的颜色是黑色就要使用fixAfterDeletion方法修复红黑树的红黑性质。我们来看如何修复红黑树的红黑性质。这个方法非常长,我们也分块来看,

这个也是一个循环,循环条件是x不为根节点且x的颜色为黑色。循环里首先分了两个类,即补充删除节点的节点的位置是第一个分类标准,第一个if是补充节点是父节点的左节点,第二个是else分支补充节点是父节点的右节点,我们也只分析第一种情况,第二种情况留给读者自己分析,检验自己是否理解了如何在删除节点后修复红黑树的性质。我们来看第一种大类情况源码:

首先定义了一个sib节点为x父节点的右节点,然后第一个判断sib的节点是红色的情况:画图来表示这种情况:

对这种情况如何处理呢,我们来看源码,首先将sib节点设置为黑色,即D节点为设为黑色节点,设置x的父节点为红色即B节点为红色,然后左旋x的父节点即B节点。最后将sib节点赋值为x的父节点的右节点即c节点,我们画出变化后的情况,+

我们再来看第二种情况的源码

第二种情况是判断sib节点的左节点的颜色是不是黑色和sib有节点是不是黑色,我们来画图说明这种情况。

我们看这种情况是怎么处理的:

将sib节点设置为红色,将x节点设置为x的父节点,我们画出变化后的情况:

显然x节点不为红色,所以继续循环。我们再来看情况三即else的第一个判断:

对情况三的处理就是将其转换为情况四,如何转换呢?首先设置sib的左节点为黑色,sib节点为红色,右旋sib节点,设置sib节点为x父节点的右节点。画图来看一下变化后的情况。

对情况四的处理方法是设置sib节点的颜色为x父节点的颜色,设置x的父节点为红色,设置sib右节点为黑色,然后将x的父节点进行左旋。然后将x设为根节点。

现在来总结一下四种情况,分类条件是通过sib节点即x的兄弟节点的颜色和sib子节点的颜色来分类。

第一种请况是sib节点是红色,由于x的兄弟节点sib为红色

第二种情况是sib节点是黑色的,且sib的两个子节点都是黑色的

第三种情况是sib节点是黑色,sib左节点是红的,右节点是黑色

第四种情况是sib节点是黑色,sib左节点是黑色,右节点是红色

然后我们回到deleteEntry方法

很明显只有在删除节点是黑色的时候我们才需要调用修复函数fixAfterDeletion函数,因为只有删除黑色节点才会导致路径节点减少一个黑色节点破坏性质5或者两个红色节点相联破坏性质4,删除红色节点不会破坏红黑树的性质的。

我们看deleteEntry方法剩余的两个判断分支,第二个判断分支时p父节点为空,代表这颗红黑树只剩下一个节点p,所以直接设置root节点为空即可。

第三个判断分支代表的是删除节点无孩子节点,先判断删除节点是不是黑节点,是的话就调用修复函数,然后将p从红黑树中分离出来完成删除。

到这我们讲完了TreeMap的源码即底层原理就是对红黑树的实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值