【程序人生】数据结构杂记(七)

说在前面

个人读书笔记

查找

所谓的查找或搜索(search),指从一组数据对象中找出符合特定条件者,这是构建算法的一种基本而重要的操作。其中的数据对象,统一地表示和实现为词条(entry)的形式;不同词条之间,依照各自的关键码(key)彼此区分。根据身份证号查找特定公民,根据车牌号查找特定车辆,根据国际统一书号查找特定图书,均属于根据关键码查找特定词条的实例。

查找与数据对象的物理位置或逻辑次序均无关。实际上,查找的过程与结果,仅仅取决于目标对象的关键码,故这种方式亦称作循关键码访问(call-by-key)。

词条对象拥有成员变量key和value。前者作为特征,是词条之间比对和比较的依据;后者为实际的数据。

词条的判等与比较等操作可以通过关键码的判等与比较实现。当然,这里隐含地做了一个假定——所有词条构成一个全序关系,可以相互比对和比较。需指出的是,这一假定条件不见得总是满足。比如在人事数据库中,作为姓名的关键码之间并不具有天然的大小次序。另外,在任务相对单纯但更加讲求效率的某些场合,并不允许花费过多时间来维护全序关系,只能转而付出有限的代价维护一个偏序关系

搜索树

高效率的动态修改兼顾高效率的静态查找

二叉搜索树

在所谓的二叉搜索树(binary search tree)中,处处都满足顺序性:
任一节点r的左(右)子树中,所有节点(若存在)均不大于(不小于)r
在这里插入图片描述
任何一棵二叉树是二叉搜索树,当且仅当其中序遍历序列单调非降
在这里插入图片描述

查找

二叉搜索树的查找算法,亦采用了减而治之的思路与策略,其执行过程可描述为:
从树根出发,逐步地缩小查找范围,直到发现目标(成功)或缩小至空树(失败)

在这里插入图片描述
如上图查找关键码22的过程:
首先,经与根节点16比较确认目标关键码更大,故深入右子树25递归查找;经比较发现目标关键码更小,故继续深入左子树19递归查找;经再次比较确认目标关键码更大后,深入右子树22递归查找;最终在节点22处匹配,查找成功。

一般地,在上述查找过程中,一旦发现当前节点为NULL,即说明查找范围已经缩小至空,查找失败;否则,视关键码比较结果,向左(更小)或向右(更大)深入,或者报告成功(相等)。

在这里插入图片描述

上面是在子树vv中查找关键码ee的算法。节点的插入和删除操作,都需要首先调用查找算法,并根据查找结果确定后续的处理方式。因此,这里以引用方式传递(子)树根节点,以为后续操作提供必要的信息。所谓引用传递是指在调用函数时将实际参数的地址传递到函数中那么在函数中对参数所进行的修改,将影响到实际参数

当二叉搜索树退化为(接近于)一条单链时,此时的单次查找可能需要线性时间,因为实际上这样的一棵“二分”搜索树,已经退化成了一个不折不扣的一维有序列表,而此时的查找则等效于顺序查找。
由此我们可得到启示:
若要控制单次查找在最坏情况下的运行时间,须从控制二叉搜索树的高度入手

插入

在这里插入图片描述
以如上图中(a)(a)所示的二叉搜索树为例。若欲插入关键码40,则在执行search(40)之后,如图(b)(b)所示,_hot将指向比较过的最后一个节点46,同时返回其左孩子(此时为空)的位置。于是接下来如图(c)(c)所示,只需创建新节点40,并将其作为46的左孩子接入,拓扑意义上的节点插入即告完成。

不过,为保持二叉搜索树作为数据结构的完整性和一致性,还需从节点_hot(46)出发,自底而上地逐个更新新节点40历代祖先的高度

接下来若欲插入关键码55,则在执行search(55)之后如图(c)(c)所示,_hot将指向比较过的最后一个节点53,同时返回其右孩子(此时为空)的位置。于是如图(d)(d)所示,创建新节点55,并将其作为53的右孩子接入。当然,此后同样需从节点_hot出发,逐代更新祖先的高度。

在这里插入图片描述
注意,按照以上实现方式,无论插入操作成功与否,都会返回一个非空位置,且该处的节点与拟插入的节点相等。如此可以确保一致性,以简化后续的操作。

删除

在这里插入图片描述
以上图中(a)(a)所示二叉搜索树为例。若欲删除节点69,需首先通过search(69)定位待删除节点(69)。因该节点的右子树为空,故只需如图(b)(b)所示,将其替换为左孩子(64),则拓扑意义上的节点删除即告完成。
当然,为保持二叉搜索树作为数据结构的完整性和一致性,还需更新全树的规模记录,释放被摘除的节点(69),并自下而上地逐个更新替代节点(64)历代祖先的高度

注意,首个需要更新高度的祖先(58),恰好由_hot指示。

不难理解,对于没有左孩子的目标节点,也可以对称地予以处理。当然,以上同时也已涵盖了左、右孩子均不存在(即目标节点为叶节点)的情况。

那么,当目标节点的左、右孩子双全时,删除操作又该如何实施呢?

继续上例,设拟再删除二度节点36。如图(b)(b)所示,首先找到该节点的直接后继(40)。然后,只需如图(c)(c)所示交换二者的数据项,则可将后继节点等效地视作待删除的目标节点。不难验证,该后继节点必无左孩子,从而相当于转化为此前相对简单的情况。于是最后可如图(d)(d)所示,将新的目标节点(36)替换为其右孩子(46)。

请注意,在中途互换数据项之后,这一局部如图(c)(c)所示曾经一度并不满足顺序性。但这并不要紧——不难验证,在按照上述方法完成整个删除操作之后,全树的顺序性必然又将恢复。

同样地,除了更新全树规模记录和释放被摘除节点,此时也要更新一系列祖先节点的高度。
不难验证,此时首个需要更新高度的祖先(53),依然恰好由_hot指示。

在这里插入图片描述
在这里插入图片描述

平衡二叉树

既然二叉搜索树的性能主要取决于高度,节点数目固定的前提下,应尽可能地降低高度。

相应地,应尽可能地使兄弟子树的高度彼此接近,即全树尽可能地平衡。当然,包含n个节点的二叉树,高度不可能小于log2nlog _2n若树高恰好为log2nlog_2n,则称作理想平衡树

在渐进意义下适当放松标准之后的平衡性,称作适度平衡。

若将树高限制为“渐进地不超过O(logn)O(logn)”,则AVL树、伸展树、红黑树、kd-树等,都属于适度平衡。这些变种,因此也都可归入平衡二叉搜索树(balanced binary search tree,BBST)之列。

等价二叉搜索树

若两棵二叉搜索树的中序遍历序列相同,则称它们彼此等价;反之亦然。

在这里插入图片描述
由该图也不难看出,虽然等价二叉搜索树中各节点的垂直高度可能有所不同,但水平次序完全一致。这一特点可概括为“上下可变,左右不乱”

平衡二叉搜索树的适度平衡性,都是通过对树中每一局部增加某种限制条件来保证的。比如,在红黑树中,从树根到叶节点的通路,总是包含一样多的黑节点;在AVL树中,兄弟节点的高度相差不过1。事实上,这些限制条件设定得非常精妙,除了适度平衡性,还具有如下局部性:

  1. 经过单次动态修改操作后,至多只有O(1)O(1)处局部不再满足限制条件
  2. 总可在O(logn)O(logn)时间内,使这O(1)O(1)处局部(以至全树)重新满足限制条件

这就意味着:
刚刚失去平衡的二叉搜索树,必然可以迅速转换为一棵等价的平衡二叉搜索树

等价二叉搜索树之间的上述转换过程,也称作等价变换。

最基本的修复失衡二叉搜索树手段,就是通过围绕特定节点的旋转,实现等价前提下的局部拓扑调整

zig和zag

在这里插入图片描述
如上图(a)(a)所示,设ccZZvv的左孩子、右子树,XXYYcc的左、右子树。所谓以vv为轴的zig旋转,即如上图(b)(b)所示,重新调整这两个节点与三棵子树的联接关系:
XXvv作为cc的左子树、右孩子,YYZZ分别作为vv的左、右子树。

可见,尽管局部结构以及子树根均有变化,中序遍历序列仍是...,X,c,Y,v,Z,...{ ..., X, c, Y, v, Z, ... },故zig旋转属于等价变换。
在这里插入图片描述
对称地如上图(a)(a)所示,设XXccvv的左子树、右孩子,YYZZ分别是cc的左、右子树。所谓以vv为轴的zag旋转,即如上图(b)(b)所示,重新调整这两个节点与三棵子树的联接关系:
vvZZ作为cc的左孩子、右子树,XXYY分别作为vv的左、右子树。

同样地,旋转之后中序遍历序列依然不变,故zag旋转亦属等价变换。

zig和zag旋转均属局部操作,仅涉及常数个节点及其之间的联接关系,故均可在常数时间内完成。正因如此,在实现各种二叉搜索树平衡化算法时,它们都是支撑性的基本操作。

就与树相关的指标而言,经一次zig或zag旋转之后,节点vv的深度加一,节点cc的深度减一;这一局部子树(乃至全树)的高度可能发生变化,但上、下幅度均不超过一层

AVL树

通过合理设定适度平衡的标准,并借助以上等价变换,AVL树(AVL tree)可以实现近乎理想的平衡。在渐进意义下,AVL树可始终将其高度控制在O(logn)O(logn)以内,从而保证每次查找、插入或删除操作,均可在O(logn)O(logn)的时间内完成。

任一节点vv的平衡因子(balance factor)定义为“其左、右子树的高度差”,即:
balFac(v)=height(lc(v))height(rc(v))balFac(v)=height(lc(v)) - height(rc(v))
请注意,空树高度取1-1,单节点子树(叶节点)高度取00,与以上定义没有冲突。

所谓AVL树,即平衡因子受限的二叉搜索树——其中各节点平衡因子的绝对值均不超过1

在完全二叉树中各节点的平衡因子非0即1,故完全二叉树必是AVL树

失衡与重平衡

AVL树与常规的二叉搜索树一样,也应支持插入、删除等动态修改操作。但经过这类操作之后,节点的高度可能发生变化,以致于不再满足AVL树的条件。
在这里插入图片描述
以插入操作为例,考查上图(b)(b)中的AVL树,其中的关键码为字符类型。现插入关键码M'M',于是如图(c)(c)所示,节点N'N'R'R'G'G'都将失衡。类似地,摘除关键码Y'Y'之后,也会如图(a)(a)所示导致节点R'R'的失衡。

如此因节点xx的插入或删除而暂时失衡的节点,构成失衡节点集,记作UT(x)UT(x)。请注意,xx为被摘除的节点,则UT(x)UT(x)仅含单个节点;但若xx为被引入的节点,则UT(x)UT(x)可能包含多个节点

节点插入

不难看出,新引入节点xx后,UT(x)UT(x)中的节点都是xx的祖先,且高度不低于xx的祖父。以下,将其中的最深者记作g(x)g(x)。在xxg(x)g(x)之间的通路上,设ppg(x)g(x)的孩子,vvpp的孩子。注意,既然g(x)g(x)不低于xx的祖父,则pp必是xx的真祖先。

首先,需要找到如上定义的g(x)g(x)。为此,可从xx出发沿parentparent指针逐层上行并核对平衡因子,首次遇到的失衡祖先即为g(x)g(x)。既然原树是平衡的,故这一过程只需O(logn)O(logn)时间。

根据节点g(x)g(x)ppvv之间具体的联接方向,将采用不同的局部调整方案

在这里插入图片描述
如上图(a)(a)所示,设vvpp的右孩子,且ppgg的右孩子。这种情况下,必是由于在子树vv中刚插入某节点xx,而使g(x)g(x)不再平衡。图中以虚线联接的每一对灰色方块中,其一对应于节点xx,另一为空。
此时,可做逆时针旋转zag(g(x)g(x)),得到如图(b)(b)所示的另一棵等价二叉搜索树。
可见,经如此调整之后,g(x)g(x)必将恢复平衡。不难验证,通过zig(g(x)g(x))可以处理对称的失衡。

在这里插入图片描述
如上图(a)(a)所示,设节点vvpp的左孩子,而ppg(x)g(x)的右孩子。这种情况,也必是由于在子树vv中插入了新节点xx,而致使g(x)g(x)不再平衡。同样地,在图中以虚线联接的每一对灰色方块中,其一对应于新节点xx,另一为空。
此时,可先做顺时针旋转zig(pp),得到如图(b)(b)所示的一棵等价二叉搜索树。再做逆时针旋转zag(g(x)g(x)),得到如图(c)(c)所示的另一棵等价二叉搜索树。
此类分别以父子节点为轴、方向互逆的连续两次旋转,合称“双旋调整”。可见,经如此调整之后,g(x)g(x)亦必将重新平衡。不难验证,通过zag(pp)和zig(g(x)g(x))可以处理对称的情况。

无论单旋或双旋,经局部调整之后,不仅g(x)g(x)能够重获平衡,而且局部子树的高度也必将复原。这就意味着,g(x)g(x)以上所有祖先的平衡因子亦将统一地复原——换而言之,在AVL树中插入新节点后,仅需不超过两次旋转,即可使整树恢复平衡

AVL树的节点插入操作可以在O(logn)O(logn)时间内完成

节点删除

与插入操作十分不同,在摘除节点xx后,以及随后的调整过程中,失衡节点集UT(x)UT(x)始终至多只含一个节点。而且若该节点g(x)g(x)存在,其高度必与失衡前相同
另外还有一点重要的差异是,g(x)g(x)有可能就是xx的父亲

与插入操作同理,从_hot节点出发沿parentparent指针上行,经过O(logn)O(logn)时间即可确定g(x)g(x)位置作为失衡节点的g(x)g(x),在不包含xx的一侧,必有一个非空孩子pp,且pp的高度至少为1。于是,可按以下规则从pp的两个孩子(其一可能为空)中选出节点vv
若两个孩子不等高,则vv取作其中的更高者;否则,优先取vvpp同向者(亦即,vvpp同为左孩子,或者同为右孩子)

以下不妨假定失衡后g(x)g(x)的平衡因子为+2(为-2的情况完全对称)。根据祖孙三代节点g(x)g(x)ppvv的位置关系,通过以g(x)g(x)pp为轴的适当旋转,同样可以使得这一局部恢复平衡。

在这里插入图片描述
如上图(a)(a)所示,由于在T3T3中删除了节点而致使g(x)g(x)不再平衡,pp的平衡因子非负时,通过以g(x)g(x)为轴顺时针旋转一次即可恢复局部的平衡。平衡后的局部子树如图(b)(b)所示。
同样地这里约定,图中以虚线联接的灰色方块所对应的节点,不能同时为空;T2T2底部的灰色方块所对应的节点,可能为空,也可能非空。

在这里插入图片描述
如上图(a)(a)所示,g(x)g(x)失衡时若pp的平衡因子为-1,则经过以pp为轴的一次逆时针旋转之后(图(b)(b)),接着再以g(x)g(x)为轴顺时针旋转,即可恢复局部平衡(图(c)(c))。

对于删除节点操作,g(x)g(x)恢复平衡之后,局部子树的高度可能降低。对于插入节点操作,重平衡后不仅能恢复子树的平衡性,也同时能恢复子树的高度

与插入操作不同,在删除节点之后,尽管也可通过单旋或双旋调整使局部子树恢复平衡,但就全局而言,依然可能再次失衡。对于删除节点操作,若g(x)g(x)原本属于某一更高祖先的更短分支,则因为该分支现在又进一步缩短,从而会致使该祖先失衡。在摘除节点之后的调整过程中,这种由于低层失衡节点的重平衡而致使其更高层祖先失衡的现象,称作“失衡传播”

失衡传播的方向必然自底而上,而不致于影响到后代节点。在此过程中的任一时刻,至多只有一个失衡的节点;高层的某一节点由平衡转为失衡,只可能发生在下层失衡节点恢复平衡之后。因此,可沿parentparent指针逐层遍历所有祖先,每找到一个失衡的祖先节点,即可套用以上方法使之恢复平衡。

统一重平衡算法——“3 + 4”重构

在这里插入图片描述
无论对于插入或删除操作,从刚发生修改的位置xx出发逆行而上,直至遇到最低的失衡节点g(x)g(x)。于是在g(x)g(x)更高一侧的子树内,其孩子节点pp和孙子节点vv必然存在,而且这一局部必然可以g(x)g(x)ppvv为界,分解为四棵子树——将它们按中序遍历次序重命名为T0T0T3T3

若同样按照中序遍历次序,重新排列g(x)g(x)ppvv,并将其命名为aabbcc,则这一局部的中序遍历序列应为:
{T0,a,T1,b,T2,c,T3T0 , a, T1 , b, T2 , c, T3}

将这三个节点与四棵子树重新如上“组装”起来,恰好即是一棵AVL树

结语

如果您有修改意见或问题,欢迎留言或者通过邮箱和我联系。
手打很辛苦,如果我的文章对您有帮助,转载请注明出处。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章