分享一个关于Avl树的迭代器算法

avl · 浏览次数 : 0

小编点评

**算法描述** 该算法使用双向堆栈实现对二叉树的静态迭代,包括查找、插入、删除和返回最接近的节点。 **双向堆栈**是一种线性数据结构,它允许在时间 O(logN) 中从堆栈顶弹出N个元素,其中N是堆栈的长度。 **算法步骤** 1. 如果节点存在左子节点,则将当前节点的索引存入双向堆栈的前面索引处,并执行一个循环。 2. 如果节点不存在左子节点,且后向索引的值不为39(栈的最大高度-1),则从后向索引所处的节点N中取出数据并更新迭代器高度。 3. 如果上述都不满足,则说明节点到达了limL(见上文),设置status=1,并返回空指针。 4. 如果当前status = 2,则说明当前位置在limR(见上文),此时进行previous操作,那么所指向的节点必定是二叉树的最右节点R。设置迭代器status = 0,表示状态正常,然后返回R所指数据。 5. 如果当前status = 1,那么说明在到达limL之后还在尝试previous操作,这种操作是无效的。只需返回空指针。 6. 如果当前status = 3,直接返回空指针3。 **算法时间复杂度** - 查找:O(logN),其中N是堆栈的长度。 - 插入:O(logN)。 - 删除:O(logN)。 - 最接近:O(logN)。

正文

1 研究过程

前段时间在研究avl树的迭代实现,在节点不使用parent指针的情况下,如何使用堆栈来实现双向地迭代。我参考了网络上的大部分迭代器实现,要么是使用了parent指针(就像c++的map容器中的迭代算法),要么就是前中后序遍历,没找到一种真正意义上可以双向迭代的算法,于是乎在我的不屑努力下,基于灵感想到了一个只使用很低层数的堆栈就可以完成双向迭代的算法。
我把它命名为“基于双向堆栈的avl树双向迭代算法”。

这个算法分为三个主要部分:初始化、前向迭代(next)、后向迭代(previous)。下面我将以文字的形式详细说明这三个算法。

2 算法步骤

2.1 初始化算法

1.定义需要使用的变量:一个长度为40的用于存放树节点的指针数组stack,模拟双向堆栈,存放前进或后退方向的节点。2个长度变量,分别是stack_next_len以及stack_prev_len,分别初始化为0和39(stack的长度-1)。以下将stack_next_len统称为前向索引,将stack_prev_len统称为后向索引。前者用于存放前进方向的节点指针,后者用于存放后退方向的节点指针。当前的高度cur_height,用于保存当前所处节点的高度,并用于更新在后续迭代过程中节点的高度。状态量status,表示当前迭代的边界状态。
2.根据用户所选的迭代起始位置(最小值节点或最大值节点),初始化stack。
2.1) 如果从最小值节点开始迭代,那么从树根节点开始,下潜到最左侧节点,并依次将下潜过程中遍历的所有节点从前向索引往后(从0到39的方向)依次存入stack。
2.2) 如果从最大值节点开始迭代,那么从树根节点开始,下潜到最右侧节点,并依次将下潜过程中遍历的所有节点从后向索引往前(从39到0的方向)依次存入stack。
3.根据下潜的方向,修改status,如果是左侧下潜,那么状态为1(表示当前的位置是处于比树的最左侧节点还要小的一个假象相邻节点),如果是右侧下潜,那么状态为2(表示当前的位置是处于比树的最右节点还要大的一个假象相邻节点)。若树为空,则将status设置为3。

2.2 前向迭代的算法

在完成了初始化之后,根据用户传入的迭代方向,调整双向堆栈的内容和迭代器的状态。

以下是执行next(前向)动作的迭代算法:

如果迭代器的status = 0,说明当前的迭代状态正常,没有处于边界条件。接下来判断迭代器所指向节点是否存在右子节点。
1.1) 如果存在,那么将当前节点存入双向堆栈的后向索引处,索引递减1(表现为向双向堆栈的尾部堆入一个节点)。随后将节点设置为其右子节点R,并执行一个循环。该循环的作用表现为:从R节点左向下潜到其最左侧节点LM,并依次更新下潜过程中的节点高度,将它们存入到双向堆栈的前向索引处,然后递增1(表现为向双向堆栈的首部堆入一个节点)。完成后返回LM所指数据。
1.2) 如果不存在,且前向索引的值不为0。那么取出前向索引所处的节点N。设当前节点的高度减去N节点的高度再减去1的值为m,然后将后向索引的值加上m(表现双向堆栈的尾部出栈m个节点)。然后更新迭代器高度为N的高度。完成后返回N所指数据。
1.3) 如果上述都不满足,那么说明节点达到了比二叉树的最右节点还要大的位置(设为limR)。设置status=2,并返回空指针。
如果当前status = 1,那么说明当前的位置处于二叉树最左节点还要小的位置(设为limL),此时进行next操作,那么所指向的节点必定是二叉树的最左节点L。设置迭代器status = 0,表示状态正常,然后返回L所指数据。
如果当前status = 2,那么说明在到达limR之后还在尝试next操作,这种操作是无效的。只需返回空指针。
如果status = 3,直接返回空指针

2.3 后向迭代的算法

后向迭代的算法与前向迭代大致相同,只需要将对应的方向和索引进行交换即可。

以下是执行previous(后向)动作的迭代算法:

如果迭代器的status = 0,说明当前的迭代状态正常,没有处于边界条件。接下来判断迭代器所指向节点是否存在左子节点。
1.1) 如果存在,那么将当前节点存入双向堆栈的前向索引处,索引递减1(表现为向双向堆栈的首部堆入一个节点)。随后将节点设置为其左子节点L,并执行一个循环。该循环的作用表现为:从L节点右向下潜到其最右侧节点RM,并依次更新下潜过程中的节点高度,将它们存入到双向堆栈的后向索引处,然后递减1(表现为向双向堆栈的尾部堆入一个节点)。完成后返回RM所指数据。
1.2) 如果不存在,且后向索引的值不为39(栈的最大高度-1)。那么取出后向索引所处的节点N。设当前节点的高度减去N节点的高度再减去1的值为m,然后将前向索引的值减去m(表现双向堆栈的首部出栈m个节点)。然后更新迭代器高度为N的高度。完成后返回N所指数据。
1.3) 如果上述都不满足,那么说明节点到达了limL(见上文)。设置status=1,并返回空指针。
如果当前status = 2,那么说明当前的位置在limR(见上文),此时进行previous操作,那么所指向的节点必定是二叉树的最右节点R。设置迭代器status = 0,表示状态正常,然后返回R所指数据。
如果当前status = 1,那么说明在到达limL之后还在尝试previous操作,这种操作是无效的。只需返回空指针。
如果status = 3,直接返回空指针

3 算法原理分析

基于堆栈的双向迭代算法,实现难点在于如何保证在双向任意次数迭代的情况下而不会产生节点的丢失以及失序的问题,解决这个问题的核心在于,如何根据前后迭代状态合理地更新两个堆栈的高度。

根据二叉树迭代过程中的下潜方向,我们分别用两个堆栈保存左向下潜和右向下潜迭代过程的所有节点,并且在无法通过下潜获取符合迭代方向的节点时,我们取出自身堆栈顶部的节点。这个节点代表需要回退的位置。如图1所示,假设当前节点是C,那么根据二叉树性质可知,B节点一定位于C节点之后迭代,且A节点一定位于C节点之前迭代。若此时执行previous操作,而节点B位于节点A和节点C之间,那么B节点应该失效。如果当前所处的节点是D,在执行next操作后,应该回退到节点R,并且在R与D之间的节点A和B都应该失效。而根据这一个过程,可以得知,若发生回退,则一定只可能且只有同一个下潜方向的节点需要被消除。而不同下潜方向的节点被保存在了不同堆栈,所以只需更新与自身相对的另一个堆栈的高度即可。且这两个堆栈的高度总和不会超过树高-1的长度,因此可以采用双向堆栈进一步节省空间。


图1 AVL树实例

4 算法扩展

基于上述的算法流程,可以延伸出其他算法的迭代器版本(不考虑多线程)。

首先是查找算法,在需要获取某一个节点并对其附近的节点进行迭代时,只需要把查找路径上的节点,按照下潜的方向依次存入双向堆栈即可。

其次是插入算法,在插入某一个节点之后,只需自顶向下,再次使用迭代器版本的查找算法即可重新构建查找路径。时间复杂度为:

\[O\left ( 2logN \right ) \]

最后是删除算法,如果删除的节点是迭代器所指的节点,那么在执行删除时,只需要将节点高度从小到大,进行合并,然后使用自底向上的删除算法,对树结构进行重构,并且将于删除的节点值最接近的节点更新到迭代器所指节点,然后在回退至根节点的过程中,如果发生旋转操作,只需要根据旋转的类型,调整双向堆栈的结构即可。时间复杂度为:

\[O\left ( logN \right ) \]

5 多线程可行性

以下将对多个线程使用同一个avl树对象进行迭代的情况做讨论。

经过理论分析可以得到,在多线程环境下,使用parent指针与使用双向堆栈的迭代算法在进行静态迭代时都不会失效,而使用parent指针的迭代算法,在迭代过程中如果需要进行插入操作,那么只需要加上互斥语句即可。而使用parent指针的迭代算法在删除操作的情况下,有非常低的概率会其他线程的迭代器失效,这种情况是所删除的节点正好被其他迭代器访问。

使用双向堆栈迭代的迭代算法,在进行删除操作以及插入操作时,都会使其他线程的迭代器失效,原因是树结构的改变不会实时更新到其他线程的堆栈上,并且各个迭代器的堆栈状态是不一样的,这样的同步代价就很高。解决方法是,在任意一个线程使用插入或删除操作之后,通知其余正在使用迭代器的进程,从avl树的根节点开始,将迭代器的当前所指数据指针作为查找依据,进行一个模糊查找,查找到最接近的那一个节点,从而完成双向堆栈的重构。使用这种方法时,在删除操作上同样存在与使用parent指针时一样的问题。如果使用的是指针类型来做查找依据,那么在查找时需确保备份了自身数据指针所指的数据,避免出现悬挂指针。这两种操作在有效的情况下,同步的时间复杂度如下:

\[O\left ( \sum_{i=1}^{N}log(Ni) \right ) \]

其中是第i个线程的节点数量。

还可以使用读写锁的方式进一步简化时间复杂度,简化后的时间复杂度如下

\[O\left ( 1 \right ) + O\left ( \max_i{log(N_i) } \right ) \]

其中O(1)表示获取读写锁的时间,后部表示节点数量最大的线程查找一次某个节点的时间。

6 其他探讨

也许会存在其他能够进一步缩短迭代器重新调整时间的实现,也欢迎各位能把自己的看法分享出来,希望能够互相探讨。

源码的链接:https://gitee.com/luqi_866813670/MyLib

与分享一个关于Avl树的迭代器算法相似的内容:

分享一个关于Avl树的迭代器算法

1 研究过程 前段时间在研究avl树的迭代实现,在节点不使用parent指针的情况下,如何使用堆栈来实现双向地迭代。我参考了网络上的大部分迭代器实现,要么是使用了parent指针(就像c++的map容器中的迭代算法),要么就是前中后序遍历,没找到一种真正意义上可以双向迭代的算法,于是乎在我的不屑努力

状态机的技术选型看这篇就够了,最后一个直叫好!!!

今天跟大家分享一个关于“状态机”的话题。给你讲清楚什么是状态机、为什么需要状态机、适用场景、有哪些具体的实现方案以及各个方案对比(附带github源码地址)

Zabbix Timeout 设置不当导致的问题

哈喽大家好,我是咸鱼 今天跟大家分享一个关于 zabbix Timeout 值设置不当导致的问题,这个问题不知道大家有没有碰到过 ## 问题 事情经过是这样的: 把某一台 zabbix agent 的模板由原来的 `Template OS Windows by Zabbix agent` 换成了 `

systemctl 命令设置开机自启动失败

哈喽大家好,我是咸鱼。今天跟大家分享一个关于 Linux 服务(service)相关的案例 案例现象 我在 3 月 31日的时候发表了一篇《shell 脚本之一键部署安装 Nginx》,介绍了如何通过 shell 脚本一键安装 Nginx 我脚本中执行了 Nginx 开机自启动的命令,当我使用 sy

bash shell 无法使用 perl 正则

哈喽大家好,我是咸鱼。今天跟大家分享一个关于正则表达式的案例,希望能够对你有所帮助 案例现象 前几天有一个小伙伴在群里求助,说他这个 shell 脚本有问题,让大家帮忙看看 可以看到,这个脚本首先将目标文本文件的名字当作该脚本的第一个参数($1)传递进去,然后查看这个文本文件的内容(cat $1),

为什么访问同一个网址却返回不同的内容

哈喽大家好,我是咸鱼。今天给大家分享一个关于 HTTP 有趣的现象 链接:https://csvbase.com/meripaterson/stock-exchanges 我们用浏览器访问这个链接,可以看到下面的网页 但如果我们使用 curl 命令去访问这个链接呢? 可以看到返回的是一个 csv 文

可重入锁思想,设计MQ迁移方案

如果你的MQ消息要从Kafka切换到RocketMQ且不停机,怎么做?在让这个MQ消息调用第三方发奖接口,但无幂等字段又怎么处理?今天小傅哥就给大家分享一个关于MQ消息在这样的场景中的处理手段。 这是一种比较特例的场景,需要保证切换的MQ消息不被两端同时消费,并且还需要在一段消费失败后的MQ还可以继

有意思!一个关于 Spring 历史的在线小游戏

发现 Spring One 的官网上有个好玩的彩蛋,分享给大家! 进到Spring One的官网,可以看到右下角有个类似马里奥游戏中的金币图标。 点击该金币之后,会打开一个新的页面,进入下面这样一个名为:The History Of Spring 的在线小游戏 你可以使用上下左右的方向键来控制Spr

玩转GaussDB 中的SET操作符

摘要:关系数据库中提供了一个关于集合的运算符SET操作符,其中包括以下操作:UNION/UNION ALL 并集、INTERSECT 交集、MINUS 差集。 本文分享自华为云社区《GaussDB 中的SET操作符 (UNION, INTERSECT, MINUS)【玩转PB级数仓GaussDB(D

算法训练优化的经验:深入任务与数据的力量

引言 在算法优化的世界中,理解所面对的任务不仅是起点,也是整个优化过程的核心。在这篇博客中,我将分享我在算法训练和优化中的一些经验,以及一个关于场景流估计的项目中应用的案例。我希望这些经验能帮助你在未来的项目中取得更好的成绩。 1. 深入理解任务和数据 理解算法项目的独特目标和挑战是优化的第一步。明