2-3树
2-3树是一棵自平衡的多路查找树,它并不是一棵二叉树,具有如下性质:
(1)每个节点有1个或2个key,对应的子节点为2个子节点或3个子节点;
(2)所有叶子节点到根节点的长度一致;
(3)每个节点的key从左到右保持了从小到大的顺序,两个key之间的子树中所有的key一定大于它的父节点的左key,小于父节点的右key。
如下图所示,
为什么会有2-3树这种数据结构呢?是因为他的查询复杂度比平衡二叉树还高吗?
其实不是的,实际上2-3树的查询时间复杂度也是为 O(logN) ,而出现这种多路查找树,主要是跟内存与磁盘交互有关。我们知道在内存IO的速度比磁盘IO要快的多的多,但是同样空间大小的内存比硬盘要贵的多的多,像TB级别的数据库不可能全部读出来放到内存中去,太过昂贵,而且也没必要,大部分数据是不经常用的,所以就需要内存与外存互相结合,而如果用平衡二叉树这种数据结构,在大数据量的情况下,树肯定会很高,此时查个数据对磁盘读个几千上万次那肯定是不行的(有人可能说把数据的索引文件全部放到内存中,然后把源数据放在硬盘中,这样在内存中定位到源数据Id,然后去外存中取源数据,这样肯定是不行的,不要以为索引文件很小,像搜索引擎的倒排索引文件比源文件还要大),所以用多路查找树这种数据结构,高阶的情况下,树不用很高就可以标识很大的数据量了,检索次数就大大减少了,用这种数据结构去磁盘中存取数据,磁盘IO次数的次数也会很少。
因为2-3树是一棵自平衡的多路查找树,所以构建跟维系一棵2-3树,就比二叉平衡树要复杂的多了。
2-3树的插入操作,首先一定是在叶子节点,另外如果2-3树中已存在当前插入的key,则插入失败, 下面就在这两点的前提下,进行2-3树插入流程的分析:
(1)如果待插入的节点只有1个key,则直接插入即可;
(2)如果待插入的节点有2个key,则对节点进行分裂,即2个key加上待插入的key,这3个key分裂成1个key跟两个子节点,然后将分裂之后的3个key中的父节点看作向上层插入的key,然后重复(1)、(2)步骤,直到满足2-3树的定义性质。
如下图所示,插入“7”,而此时节点“5”只有一个key,则直接插入即可,形成节点“5 7”。
此时如果再插入“6”,而节点“5 7”已经有2个key了,所以需要先进行分裂。
“5 7”节点与新插入的“6”分裂之后,如下图所示,
此时需要将“6”向父节点插入,而父节点“13 30”又包含2个key,则需要再次分裂,即如下图所示,“13 30”与“6”分裂成父节点为“13”,子节点为“6”跟“30”
再将节点“13”看作向父节点插入,而此时父节点“50”只有一个key,则将“13”与“50”直接合并即可,如下图所示,完成节点的插入调整,如下图所示
2-3树节点的插入应该还算简单,最好是自己再去找几个2-3树的图自己手画一下插入过程,加深下印象。下面再讲2-3树节点的删除。
关于2-3树节点的删除,首先,如果删除的key不存在,则删除失败,然后在此前提下来分析节点的删除。跟讲平衡二叉树的节点删除类似,总结也是两个判断:①删除的是什么节点?②删除了节点之后是否符合满足2-3树的性质?
2-3树有4种节点:1.仅1个key的叶子节点;2.有 2个key的叶子节点;3.仅1个key的非叶子节点;4.有2个key的非叶子节点。即 1个key与2个key的节点 和 是否为叶子节点 的组合。下面就从简单到复杂的情况开始分析:
(1)当删除的节点是2个key的叶子节点,则将要删除的目标key删除即可,此时原来待删除的2个key的叶子节点,变成1个key的叶子节点,但是符合2-3树;
(2)当删除的节点是2个key的非叶子节点,则此时使用中序遍历找到待删除节点的后继节点,然后将后继节点与待删除节点位置互换,此时就将问题转化为删除节点为叶子节点(平衡树的非叶子节点中序遍历后继节点肯定叶子节点),如果该叶子是2个key,则跟情况(1)一样,如果该节点是只有1个key,则跟后面的情况(4)一样;
(3)当删除的节点是1个key的非叶子节点,实际上操作跟情况(2)是一样的,即使用中序遍历找到待删除节点的后继节点,然后将后继节点与待删除节点位置互换,此时问题转化为删除节点为叶子节点;
(4)当删除的节点是1个key的叶子节点,则将节点删除,此时树肯定不满足2-3树的性质,也即肯定需要调整,但要分情况来进行调整,而总结起来就是当前待删除的1个key的叶子节点,兄弟节点与父节点,分别是1个key还是2个key,即:
a.当父节点是1个key(即此时仅有一个兄弟节点),兄弟节点是2个key,则将兄弟节点的一个key上移成父节点,而父节点下移成子节点,也即跟2个key中插入新节点类似,拆成一父两子,此时树满足2-3树,完成调整。
b.当父节点是1个key,兄弟节点也是1个key,则此时将父节点与兄弟节点合并,将合并后的节点看成当前节点,然后重复(4)的判断,即判断合并后的当前节点的兄弟节点与父节点的情况,然后走对应的a.b.c处理,直到满足2-3树,完成调整。
c.当父节点是2个key,即此时有两个兄弟节点,而兄弟节点又可能有多种情况,穷举起来有:删除节点的位置左中右3个,以及另外两个兄弟节点是否为1个key或2个key的4种情况,总共3*4=12种。即,
i.若删除的是左或右节点,且中间节点只有1个key,则此时父节点的一个key下移,与中间节点合并,此时父节点为1个key,两个子节点,树满足2-3树,完成调整;
ii.若删除的是左或右节点,且中间节点有2个key,则此时父节点的一个key下移,中间节点的一个key上移与父节点合并,此时父节点为2个key,3个子节点,树满足2-3树,完成调整;
iii.若删除的是中间节点,且右节点只有1个key,则此时父节点的一个key下移,与右节点合并,此时父节点为1个key,两个子节点,树满足2-3树,完成调整;
iv.若删除的是中间节点,且右节点有2个key,则此时父节点的一个key下移,右节点的一个key上移与父节点合并,此时父节点为2个key,3个子节点,树满足2-3树,完成调整。
计:i与ii删除左或右节点两种情况,中间节点1个key或2个key两种情况,兄弟节点1个key或2个key两种情况,总共 2x2x2=8 种;删除中间节点一种情况,iii与iv右节点1个key或2个key两种情况,左节点1个或2个key两种情况,总共 1x2x2=4 种; 4+8=12 种全齐,虽然场景有12种,但是处理的方式只有2种,一种是父节点下移与子节点合并,另一种是父节点下移成单独一个子节点,然后2个key的子节点上移一个key与父节点合并。
还是画几个图演示一下吧,如下图所示,最简单的删除情况(1),待删除的节点是2个key,直接对节点的key “5” 删除即可,
若删除节点是情况(2),如下图所示,删除“100”,而且此时“100”是非叶子节点且2个key,则找到后继节点“120”与“100”互换位置,然后删除“100”
结果如下图所示,将问题转化为删除一个key的叶子节点,且父节点为2个key,即为情况(4),删除的节点为右节点,且中间节点为一个key,也即为情况(4)中c的i,所以此时需要将父节点的一个key下移与中间节点合并
结果如下图所示,将父节点的一个key “120”下移,与中间节点“80”合并,最后如下右图所示,2-3树调整完成。
再讲另外一种,情况(4)中c的iv,如下图所示,删除节点“22”,而右兄弟节点是2个key,则需要将父节点的“30”下移成中间节点,然后右兄弟的一个key“40”上移与父节点合并,
此时情况(4)中c的iv调整结果如下右图所示,
最后再讲一种节点删除的情况,就是满二叉树的情况,根据定义的性质,满二叉树也符合2-3树,如果当满二叉树要删除叶子节点时,是符合情况(4)中的b的,即将父节点与兄弟节点合并,此时树的层数显然不平衡,即,将合并后的节点看作被删除的当前节点,而当前节点的兄弟节点与父节点依然是都是一个key,符合情况(4)的b,将父节点与兄弟节点合并,直至树平衡。
另外,实际上节点删除的情况中(2)(3)是可以整合到一起去处理的,即,删除节点是非叶子节点,无论待删除节点的key数是多少,都用中序排序找到后继节点,然后把问题转化为删除一个key的叶子节点去处理。
备注:对于节点删除中的(4)的 b 可能没讲明白,再补充说明一下,如下图删除节点“10”,符合(4)的 b 情况,则父节点“13”与兄弟节点“18”合并,
合并之后如下图所示,此时符合(4)中 c 的 ii 情况,则父节点的key“22”下移,中间节点的key“30”上移,
变换结果如下图所示,此时2-3树已经调整完成。这里需要注意的点是,由于之前说父子节点key的上下移对于叶子节点来说并没有子节点,但对于非叶子节点的变换是对应左旋与右旋的,所以上一步的变换,是以节点“22”做左旋操作,由父节点“降级”为子节点,而原本子节点“30”晋升为父节点,并将“30”的左子节点出让给“22”作为右子节点。
2-3-4树
2-3-4树只是在2-3树的基础上进行了扩展。2-3-4树也是一棵自平衡的多路查找树,具有如下性质:
(1)任一节点只能是1个或2个或3个key,对应的子节点为2个子节点或3个子节点或4个子节点;
(2)所有叶子节点到根节点的长度一致;
(3)每个节点的key从左到右保持了从小到大的顺序,两个key之间的子树中所有的key一定大于它的父节点的左key,小于父节点的右key,对于3个key的节点,两两key之间也是如此。
如下图所示,
2-3-4树插入节点跟删除节点的处理,实际上跟2-3树很像,特别是插入节点,基本上跟2-3树是一模一样,只是分裂的条件由2个key变成了3个key而已,即,
(1)如果待插入的节点不是3个key,则直接插入即可;
(2)如果待插入的节点有3个key,则对节点进行分裂,即3个key加上待插入的key,这4个key分裂成1个key跟2个子节点,然后将分裂之后的4个key中的父节点看作向上层插入的key,然后重复(1)、(2)步骤,直到满足2-3-4树的定义性质。
如下图所示,插入“125”,而此时待插入节点有3个key,需要对节点进行分裂,
“100 125 130”节点分裂之后,如下图所示,分裂成父节点“120”与两个子节点“100”与“125 130”,此时将父节点“120”看作向上层插入的key,
而又由于“120”的上层节点是“60 70 80”是3个key的节点,则需要对3个key节点进行分裂,如下图所示,分裂成父节点”70”与子节点“60”与“80 120”,
将父节点“70”看作向上层插入的key,此时上层节点“22 50”是2个key,则直接插入即可,结果如下图所示,此时满足2-3-4树,完成调整。
2-3-4树节点的插入就差不多这样了,也比较简单的,其实从前面到这里可以看出一些规律,就是不管是二叉查找树也好,平衡二叉树,以及2-3树的节点插入,相对来说都算简单,但是对于一棵树节点的删除却比较复杂,有的甚至需要不断的回溯到根节点才能把树调整平衡。
所以,关于2-3-4树节点的删除也不简单,至少比节点的插入要复杂麻烦的多,但这里就讲个大概,类比2-3树节点删除去推就可以推出来,思路是一致的。
2-3-4树节点的删除,首先,如果删除的key不存在,则删除失败。类比2-3树总结也是两个判断:①删除的是什么节点?②删除了节点之后是否符合满足2-3-4树的性质?
2-3-4树有4种节点,1个key与非1个key的节点 和 是否为叶子节点 的组合,即:1.非1个key的叶子节点;2.仅1个key的叶子节点;3.非1个key的非叶子节点;4.仅1个key的非叶子节点。
2-3-4节点删除操作:
(1)当删除的节点是非1个key的叶子节点,则将要删除的目标key删除即可;
(2)当删除的节点是非叶子节点,无论待删除节点的key是多少个,先使用中序遍历找到待删除节点的后继节点,然后将后继节点与待删除节点位置互换,此时就将问题转化为删除节点为叶子节点(平衡树的非叶子节点中序遍历后继节点肯定叶子节点),如果该叶子是非1个key,则跟情况(1)一样,如果该节点是只有1个key,则跟后面的情况(3)一样;
(3)当删除的节点是1个key的叶子节点,则将节点删除,此时树肯定需要调整,即:
a.当父节点是1个key(即此时仅有一个兄弟节点),兄弟节点是非1个key,则将兄弟节点的一个key上移成父节点,而父节点下移成子节点,此时树满足2-3-4树,完成调整。
b.当父节点是1个key,兄弟节点也是1个key,则此时将父节点与兄弟节点合并,将合并后的节点看成当前节点,然后重复(3)的判断,即判断合并后的当前节点的兄弟节点与父节点的情况,然后走对应的a.b.c处理,直到满足2-3-4树,完成调整。
c.当父节点是非1个key,即此时有两个或三个兄弟节点,此时看相邻兄弟节点是否“丰满”,也即是否为3个key,如下,
i.若删除节点的相邻兄弟节点为非3个key,则父节点的一个key下移,与相邻兄弟节点合并,此时树满足2-3树,完成调整;
ii.若删除节点的相邻兄弟节点为3个key,则父节点的一个key下移成1个key的节点,相邻兄弟节点的一个key上移与父节点合并,此时树满足2-3树,完成调整;
下面画几个图演示一下吧,如下图所示,符合(3)中的 b 情况,即对兄弟节点“18”与父节点“13”合并,
合并之后,如下图所示,此时符合(3)中 c 的 ii 情况,即对节点“22”做左旋操作(参考2-3树文章最后的备注部分),
左旋结果如下图所示,此时2-3-4树调整完成。
最后再重复一点,关于2-3-4树节点删除情况(3)中的 b :“将合并后的节点看成当前节点,然后重复(3)的判断,即判断合并后的当前节点的兄弟节点与父节点的情况” 这句话,由于此时合并后的当前节点,其兄弟节点,是带有子节点的,所以此时重复(3)的判断之后,如果是 c 中的 i 或 ii 情况,对于(兄弟)key的上移与(父)key的下移,对应的子节点是需要出让的,即此时的变换,实际上为左旋或右旋,具体是左旋还是右旋,看对应的场景。