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树的迭代器算法的更多相关文章

  1. AVL树的平衡算法(JAVA实现)

      1.概念: AVL树本质上还是一个二叉搜索树,不过比二叉搜索树多了一个平衡条件:每个节点的左右子树的高度差不大于1. 二叉树的应用是为了弥补链表的查询效率问题,但是极端情况下,二叉搜索树会无限接近 ...

  2. AVL树的JAVA实现及AVL树的旋转算法

    1,AVL树又称平衡二叉树,它首先是一颗二叉查找树,但在二叉查找树中,某个结点的左右子树高度之差的绝对值可能会超过1,称之为不平衡.而在平衡二叉树中,任何结点的左右子树高度之差的绝对值会小于等于 1. ...

  3. 分享一个我改进过的ocr算法

    https://github.com/zhangbo2008/chineseOCR-jingjianban 欢迎大家前来拍砖

  4. 数据结构和算法(Golang实现)(28)查找算法-AVL树

    AVL树 二叉查找树的树高度影响了查找的效率,需要尽量减小树的高度,AVL树正是这样的树. 一.AVL树介绍 AVL树是一棵严格自平衡的二叉查找树,1962年,发明者Adelson-Velsky和La ...

  5. 数据结构图文解析之:AVL树详解及C++模板实现

    0. 数据结构图文解析系列 数据结构系列文章 数据结构图文解析之:数组.单链表.双链表介绍及C++模板实现 数据结构图文解析之:栈的简介及C++模板实现 数据结构图文解析之:队列详解与C++模板实现 ...

  6. 纸上谈兵:AVL树

    作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 二叉搜索树的深度与搜索效率 我们在树, 二叉树, 二叉搜索树中提到,一个有n个节点 ...

  7. 纸上谈兵: AVL树[转]

    作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 二叉搜索树的深度与搜索效率 我们在树, 二叉树, 二叉搜索树中提到,一个有n个节点 ...

  8. 使用C编程语言实现AVL树

    本文将介绍AVL树及其插入.删除操作,最后使用C编程语言实现基于平衡因子(balance factor)的AVL树. 什么是AVL树? AVL树(AVL tree)是前苏联计算机科学家Adelson- ...

  9. PAT 1066 Root of AVL Tree[AVL树][难]

    1066 Root of AVL Tree (25)(25 分) An AVL tree is a self-balancing binary search tree. In an AVL tree, ...

  10. AVL树,红黑树,B-B+树,Trie树原理和应用

    前言:本文章来源于我在知乎上回答的一个问题 AVL树,红黑树,B树,B+树,Trie树都分别应用在哪些现实场景中? 看完后您可能会了解到这些数据结构大致的原理及为什么用在这些场景,文章并不涉及具体操作 ...

随机推荐

  1. 草之王qsnctfwp

    文件内容(举例): 林间小路旁有一条小溪 草之王许下三个诺言 无人知晓神诏背后的真相 草之王许下三个诺言 === 林间小路旁有一条小溪 草之王许下三个诺言 林间小路旁有一条小溪 无人知晓神诏背后的真相 ...

  2. Excel 分组后计算

    分组后的计算都类似,仍然采用 groups 函数,分组并同时计算出各洲的 GDP 总量,然后再求一遍各洲的 GDP 总量占全球 GDP 的百分比值. SPL 代码如下:   A B 1 =clipbo ...

  3. github 小技巧

    前言 简单记一下github 小技巧,因为经常忘. 正文 就是如何快速搜索到自己想找的项目. 如果自己知道项目名,那么直接输入就可以搜索到. 如果不是,那么一般要通过高级搜索. https://git ...

  4. Django3.0连接数据库注意点

    需先在应用下的__Init__.py文件中配置 import pymysqlpymysql.version_info=(1, 3, 13, 'final', 0) # 3.0时需要pymysql.in ...

  5. WPF开发随笔收录-查看PDF文件

    一.前言 在项目的开发过程中,涉及到查看服务器生成的pdf报告文件的查看,起初的方案是通过spire.pdf这个库来将pdf文件转换成图片,然后在进行查看.但是经常被吐槽预览不清晰,后面上网发现了一个 ...

  6. docker安装mysql8.0.20并远程连接

    前言 今天docker安装mysql8.0.20捯饬了半天,主要是挂载问题和连接问题,索性记录一下.网上很多千篇一律,还有很多就是过时了,那还是我自己上场吧.大家看的时候,请睁大眼睛,按步骤来. Do ...

  7. 力扣21(java&python)-合并两个有序链表(简单)

    题目: 将两个升序链表合并为一个新的 升序 链表并返回.新链表是通过拼接给定的两个链表的所有节点组成的. 示例 1: 输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3 ...

  8. Fluid给数据弹性一双隐形的翅膀 (1) -- 自定义弹性伸缩

    简介: 弹性伸缩作为Kubernetes的核心能力之一,但它一直是围绕这无状态的应用负载展开.而Fluid提供了分布式缓存的弹性伸缩能力,可以灵活扩充和收缩数据缓存. 它基于Runtime提供了缓存空 ...

  9. 技术干货 | 使用 mPaaS 配置 SM2 国密加密指南

    ​简介:随着移动智能终端的广泛应用,敏感信息极易被监控或盗取,给国家.企事业及个人带来极大政治.经济损失.金融和重要领域的各个企业正在逐步落实并完成国产密码改造工作.为解决客户侧因更换加密算法造成的种 ...

  10. 【产品动态】解读Dataphin流批一体的实时研发

    ​简介: Dataphin作为一款企业级智能数据构建与管理产品,具备全链路实时研发能力,从2019年开始就支撑可集团天猫双11的实时计算需求,文章将详细介绍Dataphin实时计算的能力. 背景 每当 ...