python数据结构 树_Python数据结构——AVL树的实现

既然,我们已经证明,保持 AVL 树的平衡将会使性能得到很大的提升,那我们看看如何在程序中向树插入一个新的键值。因为所有的新键是作为叶节点插入树的,而新叶子的平衡因子为零,所以我们对新插入的节点不作调整。不过一旦有新叶子的插入我们必须更新其父节点的平衡因子。新叶子会如何影响父节点的平衡因子取决于叶节点是左子节点还是右子节点。如果新节点是右子节点,父节点的平衡因子减 1。如果新节点是左子节点,父节点的平衡因子将加 1。这种关系可以递归地应用于新节点的前两个节点,并有可能影响到之前的每一个甚至是根节点。由于这是一个递归的过程,我们看看更新平衡因子的两个基本条件:

递归调用已到达树的根。

父节点的平衡因子已调整为零。一旦子树平衡因子为零,那么父节点的平衡因子不会发生改变。

我们将实现 AVL 树的子类BinarySearchTree。首先,我们将重写_put方法,并写一个新的辅助方法updateBalance。这些方法如Listing 1 所示。除了第 7 行和第 13 行对 updateBalance的调用,你会注意到_put和简单的二叉搜索树的定义完全相同。

Listing 1

def _put(self,key,val,currentNode):

if key < currentNode.key:

if currentNode.hasLeftChild():

self._put(key,val,currentNode.leftChild)

else:

currentNode.leftChild = TreeNode(key,val,parent=currentNode)

self.updateBalance(currentNode.leftChild)

else:

if currentNode.hasRightChild():

self._put(key,val,currentNode.rightChild)

else:

currentNode.rightChild = TreeNode(key,val,parent=currentNode)

self.updateBalance(currentNode.rightChild)

def updateBalance(self,node):

if node.balanceFactor > 1 or node.balanceFactor < -1:

self.rebalance(node)

return

if node.parent != None:

if node.isLeftChild():

node.parent.balanceFactor += 1

elif node.isRightChild():

node.parent.balanceFactor -= 1

if node.parent.balanceFactor != 0:

self.updateBalance(node.parent)

updateBalance方法完成了大部分功能,实现了我们刚提到的递归过程。这个再平衡方法首先检查当前节点是否完全不平衡,以至于需要重新平衡(第 16 行)。如果当前节点需要再平衡,那么只需要对当前节点进行再平衡,而不需要进一步更新父节点。如果当前节点不需要再平衡,那么父节点的平衡因子就需要调整。如果父节点的平衡因子不为零, 算法通过父节点递归调用updateBalance方法继续递归到树的根。

当对一棵树进行再平衡是必要的,我们该怎么做呢?高效的再平衡是使 AVL 树能够很好地执行而不牺牲性能的关键。为了让 AVL 树恢复平衡,我们会在树上执行一个或多个“旋转”

(rotation)。

为了了解什么是旋转,让我们看一个很简单的例子。思考一下图 3 的左边的树。这棵树是不平衡的,平衡因子为 -2。为了让这棵树平衡我们将根的子树节点 A 进行左旋转。

bVDDLr?w=442&h=221

图 3:使用左旋转变换不平衡树

执行左旋转我们需要做到以下几点:

使右节点(B)成为子树的根。

移动旧的根节点(A)到新根的左节点。

如果新根(B)原来有左节点,那么让原来B的左节点成为新根左节点(A)的右节点。

注:由于新根(B)是 A 的右节点,在这种情况下,移动后的 A 的右节点一定是空的。我们不用多想就可以给移动后的 A 直接添加右节点。

虽然这个过程概念上看起来简单,但实现时的细节有点棘手,因为要保持二叉搜索树的所有性质,必须以绝对正确的顺序把节点移来移去。此外,我们需要确保更新了所有的父节点。

让我们看一个稍微复杂的树来说明右旋转。图 4 的左侧展现了一棵“左重”的树,根的平衡因子为 2。执行一个正确的右旋转,我们需要做到以下几点:

使左节点(C)成为子树的根。

移动旧根(E)到新根的右节点。

如果新根(C)原来有右节点(D),那么让 D 成为新根右节点(E)的左节点。

注:由于新根(C)是 E 的左节点,移动后的 E 的左节点一定为空。这时可以直接给移动后的 E 添加左节点。

bVDDLs?w=554&h=281

图 4:使用右旋转变换不平衡树

现在你已经明白了旋转的过程,了解了旋转的方法,让我们看看代码。Listing 2 同时显示了右旋转和左旋转的代码。在第 2 行,我们创建一个临时变量来跟踪新的子树的根。正如我们之前所说的新的根是旧根的右节点。现在,右节点已经存储在这个临时变量中。我们将旧根的右节点替换为新根的左节点。

下一步是调整两个节点的父指针。如果newRoot原来有左节点,左节点的新父节点变成旧根。新根的父节点将成为旧根的父节点。如果旧根是整个树的根,那么我们必须让整棵树的根指向这个新的根。如果旧根是左节点,那么我们改变左节点的父节点到一个新的根;否则,我们改变右节点的父节点到一个新的根(第 10-13 行)。最后我们设置的旧根的父节点成为新的根。这里有很多复杂的中间过程,所以建议你一边看函数的代码,一边看图 3。rotateRight方法和rotateLeft是对称的,所以请自行研究rotateRight的代码。

Listing 2

def rotateLeft(self,rotRoot):

newRoot = rotRoot.rightChild

rotRoot.rightChild = newRoot.leftChild

if newRoot.leftChild != None:

newRoot.leftChild.parent = rotRoot

newRoot.parent = rotRoot.parent

if rotRoot.isRoot():

self.root = newRoot

else:

if rotRoot.isLeftChild():

rotRoot.parent.leftChild = newRoot

else:

rotRoot.parent.rightChild = newRoot

newRoot.leftChild = rotRoot

rotRoot.parent = newRoot

rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(newRoot.balanceFactor, 0)

newRoot.balanceFactor = newRoot.balanceFactor + 1 + max(rotRoot.balanceFactor, 0)

最后,第 16-17 行需要解释一下。这两行我们更新了旧根和新根的平衡因子。因为其他操作都是移动整个子树,被移动的子树内的节点的平衡因子不受旋转的影响。但我们如何在没有重新计算新的子树的高度的情况下更新平衡因子?下面的推导将让你明白,这些代码都是正确的。

bVDDLz?w=449&h=189

图 5:左旋转

图5显示了一个左旋转。B 和 D 是中心节点,A,C,E 是其子树。让 hX 表示以X为根节点的子树的高度。通过定义我们知道:

$$newBal(B) = h_A - h_C \\

oldBal(B) = h_A - h_D$$

但我们知道,D 的高度也可以通过 1 + max(hC,hE) 给定,也就是说,D 的高度为两子树高度中较大者加 1。记住,hC 和 hE 没有改变。所以,把上式代入第二个方程,可以得到:

$$oldBal(B) = h_A - (1 + max(h_C,h_E))$$

然后两方程作差。下面是作差的步骤,newBal(B) 使用了一些代数方法简化方程。

$$begin{split}newBal(B) - oldBal(B) = h_A - h_C - (h_A - (1 + max(h_C,h_E))) \\

newBal(B) - oldBal(B) = h_A - h_C - h_A + (1 + max(h_C,h_E)) \\

newBal(B) - oldBal(B) = h_A - h_A + 1 + max(h_C,h_E) - h_C \\

newBal(B) - oldBal(B) = 1 + max(h_C,h_E) - h_C$$

接下来我们移动 oldBal(B) 到方程的右端并利用 max(a,b)−c = max(a−c,b−c)。

$$newBal(B) = oldBal(B) + 1 + max(h_C - h_C ,h_E - h_C)$$

但 hE − hC 等同于 −oldBal(D)。所以我们说:max(−a,−b) = −min(a,b),可以通过以下步骤完成对 newBal(B) 的推导:

$$newBal(B) = oldBal(B) + 1 + max(0 , -oldBal(D)) \\

newBal(B) = oldBal(B) + 1 - min(0 , oldBal(D))$$

现在方程所有的项都是已知数。如果我们记得 B 是rotRoot,D 是newRoot,可以看出这正好符合第 16 行的语句:

rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(0,newRoot.balanceFactor)

更新节点 D,以及右旋转后的平衡因子的方程推导与此类似。

现在你可能认为步骤都完全了解了。我们知道如何并且什么时候进行左右旋转,但看看图 6。由于节点 A 的平衡因子是 -2,我们应该做一个左旋转。但是,当我们在左旋转时会发生什么?

bVDDLH?w=110&h=222

图 6:一棵更难平衡的不平衡树

bVDDLL?w=111&h=237

图 7:显示的树左旋转后,仍然不平衡。如果我们要做一个右旋转来试图再平衡,又回到了开始的状态。

要解决这个问题,我们必须使用以下规则:

如果子树需要左旋转使之平衡,首先检查右节点的平衡因子。如果右节点左重则右节点右旋转,然后原节点左旋转。

如果子树需要右旋转使之平衡,首先检查左节点的平衡因子。如果左节点右重则左节点左旋转,然后原节点右旋转。

图 8 显示了这些规则如何解决了我们在图 6 和图 7 中遇到的问题。首先,以 C 为中心右旋转,树变成一个较好的形状;然后,以 A 为中心左旋转,整个子树恢复平衡。

bVDDLN?w=533&h=210

图 8:右旋转后左旋转

实现这些规则的代码可以从我们“再平衡”(rebalance)的方法中找到,如Listing 3 所示。上面的第一条规则从第二行if语句中实现。第二条规则是由第 8 行elif语句实现。

Listing 3

def rebalance(self,node):

if node.balanceFactor < 0:

if node.rightChild.balanceFactor > 0:

self.rotateRight(node.rightChild)

self.rotateLeft(node)

else:

self.rotateLeft(node)

elif node.balanceFactor > 0:

if node.leftChild.balanceFactor < 0:

self.rotateLeft(node.leftChild)

self.rotateRight(node)

else:

self.rotateRight(node)

通过保持树的平衡,我们可以确保get方法运行的时间复杂度为 O(log2n)。但问题是put方法的时间复杂度是多少?我们把put操作进行分解。由于每一个新节点都是作为叶节点插入的,每一轮更新所有父节点的平衡因子最多只需要 log2n 次操作,每层执行一次。如果子树是不平衡的最多需要两个旋转把子树恢复平衡。但是,每个旋转的操作的复杂度为 O(1) ,所以即使我们进行put操作最终的复杂度仍然是 O(log2n)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/567020.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python安装常见问题_Python常见问题

问题描述: 在linux环境下部署Python项目时常常报错无法找到自己编写的模块解决方案export PYTHONPATH项目路径问题描述:Scrapy防封之settings文件设置解决方案设置动态USER-AGENT安装scrapy-fake-useragent模块在settings.py中添加配置DOWNLOADER_MIDDLEWARES {scrapy.downloa…

设无向图g如图所示_阿里重磅发布大规模图神经网络平台 AliGraph,架构算法解读...

图神经网络 (GNN) 主要是利用神经网络处理复杂的图数据&#xff0c;它将图数据转换到低维空间&#xff0c;同时最大限度保留结构和属性信息&#xff0c;并构造一个用于训练和推理的神经网络。在实际应用中&#xff0c;为了加速 GNN 训练和新算法的快速迭代&#xff0c;设计一套…

mysql5.0.19_CentOS下升级MySQL5.0.19到5.5

系统环境&#xff1a;CentOS 5.5CentOS 5.5的源mysql目前还停留在5.0.19上&#xff0c;要做数据库主从的时候&#xff0c;必须升级到5.1以上。索性&#xff0c;直接到5.5吧1系统环境&#xff1a;CentOS 5.5CentOS 5.5的源mysql目前还停留在5.0.19上&#xff0c;要做数据库主从的…

基于点云的三维重建_香港科技大学王煜教授:深度学习在物体三维重建中的应用...

基于单幅图像的物体三维重建是计算机视觉领域的一个重要问题, 近十年来得到了广泛地关注. 随着深度学习的不断发展, 近年来逐渐成为一个新的学术研究热点问题.计算机视觉研究的主要目标之一是从二维图像复原三维结构. 二维图像是当今时代极易获取的数据形式, 互联网上每时每刻都…

ranger管mysql_添加Kafka的Ranger访问权限策略

设置Kafka管理员权限在首页中单击“Kafka”区域的组件插件名称&#xff0c;例如“Kafka”。选择“Policy Name”为“all - topic”的策略&#xff0c;单击按钮编辑策略。在“Allow Conditions”区域&#xff0c;单击“Select User”下选择框选择用户。单击“Add Permissions”&…

ubuntu安装python3.6_Ubuntu上安装python3.6以及多版本python管理 | SQN

这篇文章记录了如何在Ubuntu上安装python3.6以及将其设置为系统默认。 查看Ubuntu版本 由于python3.6在不同的Ubuntu版本中的安装是不一样的&#xff0c;这里我们先来看一下的Ubuntu的版本&#xff0c;在terminal中输入lsb_release -a即可查看&#xff0c;例如我的Ubuntu版本为…

vue @click 多个事件_VUE学习记录3

v-on的基本使用​ 在前面的计数器案例中使用了v-on:click监听单击事件。这里在回顾一下&#xff1a;<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-w…

cas登录成功后不跳转成功页面_SpringSecurity用户登录成功后页面跳转原理剖析

Spring Security 框架在用户成功登录后的处理逻辑&#xff0c;相对来说比较复杂&#xff0c;比较绕&#xff0c;下面我们就关键逻辑&#xff0c;进行相关分析。首先&#xff0c;框架默认的 AuthenticationSuccessHandler 为 SavedRequestAwareAuthenticationSuccessHandler。判…

android pdf阅读器开发_如何在 Windows 10 中将 Firefox 设置为默认 PDF 阅读器

PDF 作为办公一族中的必备文档&#xff0c;很多人都会编辑或者创建 PDF 文档&#xff0c;而在 Windows 系统中并没有默认的 PDF 阅读器。而在新版的 Microsoft Edge微软已在开始提供有真正的高级 PDF 控件&#xff0c;从而使用户可以正常使用文档而无需其他专用 PDF 阅读器应用…

shell脚本和python脚本和go脚本的区别_Nodejs中调用系统命令、Shell脚本和Python脚本的方法和实例-Go语言中文社区...

NodeJS 子进程提供了与系统交互的重要接口&#xff0c;其主要 API 有&#xff1a;1)标准输入、标准输出及标准错误输出的接口&#xff1b;child.stdin 获取标准输入child.stdout 获取标准输出child.stderr 获取标准错误输出2)获取子进程的PID&#xff1a;child.pid提供生成子进…

js文件中怎么使用thymeleaf标签_007、Spring Boot集成Thymeleaf模板引擎

1. Thymeleaf 介绍Thymeleaf 是适用于 Web 和独立环境的现代服务器端 Java 模板引擎。Thymeleaf 的主要目标是为您的开发工作流程带来优雅的自然模板 - 可以在浏览器中正确显示的HTML&#xff0c;也可以用作静态原型&#xff0c;从而在开发团队中实现更强大的协作。 以上翻译自…

python语言中包含的标准数据类型_python标准数据类型(笔记一)

关于python&#xff0c;它是一种解释型&#xff0c;面对对象&#xff0c;带有动态语义的高级程序设计语言。 之前学习python的时候&#xff0c;简单的将python的基础内容过了一遍&#xff0c;然后在工作中需要用到什么就相应的去加深某一模块的需求以及应用&#xff0c;总觉得基…

python怎么输入一个数字并调用_python如何直接输入上一句话,如何快速打出上一句话...

如何快速打出上一句话 用搜狗或者QQ拼音打字法&#xff0c;里面有设置快捷键输入语就行了。 Python&#xff0c;怎么在输入非数字时&#xff0c;输出一句话 Python提供2113了两个内置函数从标准输入5261读入一行文本&#xff0c;默4102认的标准输入是键盘。如下&#xff1a;165…

python 打卡程序_如何用python实现腾讯文档自动打卡并定时执行

最近学了些Web&#xff0c;了解了一些selenum包内函数使用&#xff0c;就写了下自动健康打卡&#xff0c;并用windows任务计划程序定时执行&#xff0c;健康打卡这个针对特定网站&#xff0c;所以对于通用化使用倒没有太大用处&#xff0c;但关于腾讯文档如何填写&#xff0c;因…

springboot test_精益求精!Spring Boot 知识点全面回顾,带你重新细读源码!

作者&#xff1a;cyd_0619原文&#xff1a;https://blog.csdn.net/cyd_0619约定优于配置Build Anything with Spring Boot&#xff1a;Spring Boot is the starting point for building all Spring-based applications. Spring Boot is designed to get you up and running as …

事物日志恢复 mysql_浅谈SQL Server中的事务日志(五)----日志在高可用和灾难恢复中的作用...

本篇文章是系列文章中的第五篇&#xff0c;是对前一个日志系列的补充篇。如果您对日志的基本概念还没有一个比较系统的了解&#xff0c;可以参看本系列之前的文章&#xff1a; 浅谈SQL Server中的事务日志(一)----事务日志的物理和逻辑构架 浅谈SQL Server中的事务日志(二)----…

select count(*) from返回的类型_数据分析面试题类型汇总

1 简单查询- 基本的查询语句&#xff1a;select <列名> from 表名 where 条件- where 后的条件需要标量&#xff0c;不可以使用集合&#xff0c;若与关联子查询连用可用&#xff1b;可使用逻辑运算符连接多个条件&#xff1a;between、and、or、in&#xff08;&#xff0…

configurationproperties_【Springboot】注解@ConfigurationProperties让配置整齐而简单

1 简介前面我们用一篇文章《【Spring】只想用一篇文章记录Value的使用&#xff0c;不想再找其它了(附思维导图)》详细讲解了在Spring中如何使用Value来实现我们对配置的需求&#xff0c;它功能强大、使用方便。但它也是有它的局限性的&#xff0c;比如对于邮件服务&#xff0c;…

dmo Java_java DMO及增删改查代码的自动生成

在web开发过程中&#xff0c;尤其是后台管理系统的开发中&#xff0c;少不了增删改成的基础操作&#xff0c;原来我自己的做法是一份一份的拷贝粘贴&#xff0c;然后修改其中的不同&#xff0c;然而这样既枯燥无味又浪费了大量的时间&#xff0c;所以根据自己项目结构的特点写了…

if __name__ == __main___一文带你弄懂python中if __name__ == #39;__main__#39;

我们在python模块那章节的学习&#xff0c;有所接触到if __name__ __main__这个概念。当时我们只是大概描述了一番&#xff0c;不少伙伴还是有所困惑&#xff0c;今天就让我们通过实际例子去讲解这条语句到底有何含义。一个python文件有两种用途&#xff0c;一种被当主程序、脚…