前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >算法原理系列:2-3查找树

算法原理系列:2-3查找树

作者头像
用户1147447
发布2019-05-26 09:58:11
8370
发布2019-05-26 09:58:11
举报
文章被收录于专栏:机器学习入门机器学习入门

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://cloud.tencent.com/developer/article/1434841

2-3查找树

第一次接触它是在刷数据结构那本书时,有它的介绍。而那时候只是单纯的理解它的节点是如何分裂,以及整个构建过程,并不清楚它的实际用处,所以看了也就忘了。而当看完《算法》查找章节时,顿时有种顿悟,喔,原来如此啊。所以,提出来的这些有趣的结构千万不能割裂来看,它的演变如此诱人,细节值得品味。

结构缘由

首先,搞清楚2-3查找树为什么会出来,它要解决什么样的问题?假设我们对它的基本已经有所了解了。先给它来个简单的定义:

2-3查找树:

  • 一种保持有序结构的查找树。
  • 可以维持动态平衡的有序查找树。

从上述定义就可以看出,它到底是为了解决什么问题,在上一篇文章中,介绍了【查找】的演变过程,详细请参看博文这里。其中最后优化到了BST这种树的结构。但我们都知道BST它对数据的输入是敏感的,如最坏情况下,每次put()key是有序的,那么构造出来的BST树,就相当于一个链表,那么对于每个元素的查找,它的性能就相当糟糕。而2-3树就是为了规避上述问题而设计发明出来的模型。现在请思考该如何设计它呢?

这里我们从BST遇到的实际问题出发,提出设计指标,再去思考利用些潜在的性质来构建2-3树。这部分内容,没有什么理论根据,而是我自己尝试去抓些字典的性质来构建,而2-3树的诞生过程并非真的如此,所以仅供参考。

构建2-3树

字典的两个主要操作为:查找和插入。而在前面一篇文章说到,作为有序表,查找性能和插入性能最理想的状态为O(lgn)O(\lg n),这点可以说明,BST作为树形结构,已经完全符合字典的设计了,而如果从一个全新的结构去构建字典显然已经没有多大的必要了。

BST最大的问题在于,它对输入敏感,针对有序的插入,它构建出来的结构相当于是链表。为什么会出现这种情况?

  • 作为有序插入,每当有新节点加入时,树没有选择【节点去向】的权力。(这好像是构建有序树的特质,树也无力改变,真惨!)
  • 树失去了分配【节点去向】的权力,自然就没办法动态改变它的高度。(出现极端情况的原因)

那么你会问了,难道就不能当输入到一定量时,发现树的深度太深,直接全局调整不行么?有了全局信息,不就能调控,分配每个节点了么。的确,我们要引出以下原因:

  • 调控可以,但为了拿到这些全局信息,我们需要遍历整个BST,而此时BST相当于链表,遍历一次的代价已经高于查找的效率,何必呢。
  • 在插入时动态调整是最佳的,而当树已经生成时,再去做树的大调整,显然实际有点难以操作。(这两条的认识都比较感性)

综上,字典key的有序性影响了【节点去向】,树失去了【分配权】,其次结构随插入时,树的【动态调整】优于【全局调整】。所以,我们需要设计一种结构能够符合:

  • 拥有分配权
  • 可以动态调整

指标提出来了,但真的要设计出这样的结构的确不是一般人能做的,好在,这世界有太多的大牛了,我们可以参考人家的思路。

分配权

为什么BST会失去分配权力?因为它没有可以权衡的信息,在BST中,每个节点只能存储了一个key,每当有新的节点插入时,进行比较后,就自动选择路径到它的子树中去了,它无法停留。节点的去向我们是无法改变的,已由有序性决定,但我们是否可以决定它的【去】和【留】,它到这节点就一定要构建新的节点?不能停留在旧的节点上么?

从宏观的角度来看这件事情的话,如果我么能做到key值插入节点的【停留】,是否能够利用它来做树结构调整呢?答案呼之欲出!

我就不卖关子了,直接给出2-3树的其中一个基本定义:

一棵2-3查找树或为一颗空树,或由以下节点组成:

  • 2-节点:含有一个键和两条链接,左链接指向的2-3树中的键都小于该节点,右链接指向的2-3树中的键都大于该节点。
  • 3-节点:含有两个键和三条链接,左链接指向的2-3树中的键都小于该节点,中链接指向的2-3树中的键都位于该节点的两个键之间,右链接指向的2-3树中的键都大于该节点。

!!!传统的树定义即为2-节点,但2-3树查找树的定义多了个3-节点,而3-节点,也就是为了让节点能够停留,而设计出来的新结构,它具有缓存能力?哈哈,可以这么理解。意思就是说,现在树多了一条权力,不再是节点说了算,你不是老大!树可以选择我把你【放在这】还是【找你的子树去】。对树来说是件好事,起码可以分配你了吧!所以分配这件事需要资源累积。

数据结构有了,我们先来看看它的查找,暂且忽略它是怎么构建的。我们只需要知道两个事实,每个节点最多可以存储两个键,三个分叉。比较选择子树和BST是一样的,对每个节点比较,然后选择合适的子树,进行下一步的递归比较。

左图是命中情况,右图是未命中,跟着图一步步走,就能理解整个查找过程了,这里我就不废话了。

动态平衡

要知道什么是动态平衡,就必须知道什么是平衡,这也是我第一次思考平衡这个概念,我们就拿树中对平衡的定义,粗略解释下。

树的平衡: 任何节点的左子树和右子树之间的高度差不能超过1。

所以很明显(a)图是平衡的,而(b)图是不平衡的。其实还要思考一个问题,平衡这个概念为何而出?定义树的平衡有它的必要性么?很显然,一个完全不平衡的树,在做查找时,它就是线性级别的性能,而平衡的二叉树,同样的数据量,但有效利用了平衡性,它的查找性能则能降到对数级别,这些都可以在数学上证明,此处只做感性认识。

那什么是动态平衡呢?定义如下:

树的动态平衡: 在对树进行插入操作时,每个动态的状态都能满足静态的平衡条件。

动态平衡是时时刻刻的,在新数据插入前,它是平衡的,而一旦当数据插入导致树结构不平衡时则立马进行调整。这思想很重要,因为后续的平衡二叉树算法都是基于这个原则实现的。原因也说了,如果不去时刻维护,要获得全局信息代价高昂且全局调整难度大于局部调整。

有上述性质,我们不难判断BST不是一个能够自平衡的结构,在最坏情况下它的缺陷很明显,对于有序key的插入,树的深度+1。那么问题来了,假设我现在要插入三个有序的key值如A E S

BST的做法已经很明显了,生成如下结构A -> E -> S。我们来看看2-3树,刚才定义了3节点,我们就尝试性的让最开始的两个节点停留在根节点,于是有如下所示:

很明显,在插入第三个节点时,我们就只剩下一个选择了,让它去子树上找位置去,这意味着它和BST的插入本质上是一样的,并没有利用缓存的能力。但其实这缓存有个很好的性质,它有了两个节点的信息(大于1节点的局部信息),可以对三个key值在插入时刻进行比较,而一旦能达到这能力,此树就可以做自我调整了。如:我找三个树的中间值,把它变成三个节点的BST树!相比于直接把下一节点插入到子树中去,它利用了两个元素的信息做了些调整,而调整后的树,是个平衡的二叉树。

所以接下来的事情,就是当有更多元素插入时,如何让这个2-3树在做调整时,时刻保持动态平衡。唉,令人遗憾的是这想法直接就由上面那种最简单的情况得到了,如上,我们没理由把节点往下插。用个形象的比喻,树根在生长时,有它的随意性,因为扎根没给它任何限制。而现在我们做了一件可怕的事情,我们在树根生长的土壤中给它加了一层隔板,限制它的向下发展,而不去约束它的向上势头,但我们都知道,不管向上怎么发展,它始终是头部为一个根节点,而底部为大量叶子节点的终极形态。

是不是很形象,所以2-3树就形成了一个基本插入原则,每当有新的元素插入时,追根溯源到最底层(也就是那层隔板),当有存放它的位置时,2-节点还尚有一个存储空间,它就存放。而当没有存放位置时,3-节点都被塞满了,那它开始【分裂】,分裂操作是不能破环【不准向下插入】原则的,所以它只能向上影响【父结点】。

所以有了上述原则,也就有了书中的对一些插入情况的讨论。

  1. 向一棵只含有一个3-节点的树中插入新键。(树的初始态)
  2. 向一个父节点为2-节点的3-节点中插入新键。(子树的分裂1)
  3. 向一个父节点为3-节点的3-节点中插入新建。(子树的分类2)
  4. 分解根节点。(树的向上生长态)

在前文中,我们已经图解了树的初始态,此处就不在解释了。操作2和操作3是在子树中最基本的两个操作,它们唯一的区别在于父结点一种是【2节点状态】而操作3的父结点是【3节点状态】。

父节点:2-节点,子节点:3-节点

很明显,元素一定是先沉底的,此处元素沉底在最左边,但由于超过了3-节点的存储能力,所以它必须分裂,不能向下分裂,所以只能往上了,影响它的父节点,但父节点可以再容纳一个元素,所以只需要把X元素放入父节点即可。

父节点:3-节点,子节点:3-节点

此处和操作2唯一的区别在于,子节点分裂后,把一个元素加入到了它的父节点,但也超过了父节点的存储能力,所以还要继续向上分裂,直到有容下它的父节点。它和操作2本质上是一回事,但是为了表示分裂的传递性,所以被拎出来重点讨论了下。

接着就剩下最后一个问题了,上述两操作是不会影响树的深度的,不信你自己模拟操作一遍,而真正影响树的深度在于操作4,只有当根节点为3-节点时,此时有元素插入沉底后,不断向上裂变,很不幸如果影响到根节点,那么就执行操作4,我冒个头出来,哈哈,是不是形象。

我们再来看看一个23树的整体构建轨迹加深理解。

好了,整体的23树的构建已经阐述完毕了,原本想看看书上是怎么实现的,让我继续加深理解,结果却在书中找到这样一段话,也是让我很无语,但它所提出的思想值得学习。

《算法》

但是,我们和真正实现还有一段距离。尽管我们可以用不同的数据类型表示2-节点和3-节点并写出变换所需的代码,但用这种直白的表示方法实现大多数操作并不方便,因为需要处理的情况实在太多。我们需要维护两种不同类型的节点,将被查找的键和节点中的每个键进行比较,将链接和其他信息从一种节点复制到另一种节点,将节点从一种数据类型转换到另一种数据类型,等等。实现这些不仅需要大量的代码,而且它们所产生的额外开销可能会使算法比标准的二叉查找树更慢。平衡一棵树的初衷是为了消除最坏情况,但我们希望这种保障所需的代码能够越来越好。幸运的是你将看到,我们只需要一点点代价就能用一种统一的方式完成所有变换。

欲言又止,但很有爱,起码它有进一步的实现版本了,具体是什么,我也卖个关子,下回分解。

参考文献

  1. Robert Sedgewick. 算法 第四版M. 北京:人民邮电出版社,2012.10
  2. Cormen. 算法导论M.北京:机械工业出版社,2013
  3. 算法原理系列:查找
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017年03月28日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 2-3查找树
    • 结构缘由
      • 构建2-3树
        • 分配权
        • 动态平衡
    • 参考文献
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档