Python数据结构——AVL树的实现

简介:


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

  • 递归调用已到达树的根。
  • 父节点的平衡因子已调整为零。一旦子树平衡因子为零,那么父节点的平衡因子不会发生改变。

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

Listing 1


 
 
  1. def _put(self,key,val,currentNode): 
  2.     if key < currentNode.key
  3.         if currentNode.hasLeftChild(): 
  4.                 self._put(key,val,currentNode.leftChild) 
  5.         else
  6.                 currentNode.leftChild = TreeNode(key,val,parent=currentNode) 
  7.                 self.updateBalance(currentNode.leftChild) 
  8.     else
  9.         if currentNode.hasRightChild(): 
  10.                 self._put(key,val,currentNode.rightChild) 
  11.         else
  12.                 currentNode.rightChild = TreeNode(key,val,parent=currentNode) 
  13.                 self.updateBalance(currentNode.rightChild) 
  14.  
  15. def updateBalance(self,node): 
  16.     if node.balanceFactor > 1 or node.balanceFactor < -1: 
  17.         self.rebalance(node) 
  18.         return 
  19.     if node.parent != None: 
  20.         if node.isLeftChild(): 
  21.                 node.parent.balanceFactor += 1 
  22.         elif node.isRightChild(): 
  23.                 node.parent.balanceFactor -= 1 
  24.  
  25.         if node.parent.balanceFactor != 0: 
  26.                 self.updateBalance(node.parent) 

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

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

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

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

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

  • 使右节点(B)成为子树的根。
  • 移动旧的根节点(A)到新根的左节点。
  • 如果新根(B)原来有左节点,那么让原来B的左节点成为新根左节点(A)的右节点。

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

虽然这个过程概念上看起来简单,但实现时的细节有点棘手,因为要保持二叉搜索树的所有性质,必须以绝对正确的顺序把节点移来移去。此外,我们需要确保更新了所有的父节点。让我们看一个稍微复杂的树来说明右旋转。图 4 的左侧展现了一棵“左重”的树,根的平衡因子为 2。执行一个正确的右旋转,我们需要做到以下几点:

  • 使左节点(C)成为子树的根。
  • 移动旧根(E)到新根的右节点。
  • 如果新根(C)原来有右节点(D),那么让 D 成为新根右节点(E)的左节点。

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

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

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

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

Listing 2


 
 
  1. def rotateLeft(self,rotRoot): 
  2.     newRoot = rotRoot.rightChild 
  3.     rotRoot.rightChild = newRoot.leftChild 
  4.     if newRoot.leftChild != None: 
  5.         newRoot.leftChild.parent = rotRoot 
  6.     newRoot.parent = rotRoot.parent 
  7.     if rotRoot.isRoot(): 
  8.         self.root = newRoot 
  9.     else
  10.         if rotRoot.isLeftChild(): 
  11.                 rotRoot.parent.leftChild = newRoot 
  12.         else
  13.             rotRoot.parent.rightChild = newRoot 
  14.     newRoot.leftChild = rotRoot 
  15.     rotRoot.parent = newRoot 
  16.     rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(newRoot.balanceFactor, 0) 
  17.     newRoot.balanceFactor = newRoot.balanceFactor + 1 + max(rotRoot.balanceFactor, 0) 

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

 图 5:左旋转

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

newBal(B)=hA−hC

oldBal(B)=hA−hD

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

oldBal(B)=hA−(1+max(hC,hE))

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

beginsplitnewBal(B)−oldBal(B)=hA−hC−(hA−(1+max(hC,hE)))

newBal(B)−oldBal(B)=hA−hC−hA+(1+max(hC,hE))

newBal(B)−oldBal(B)=hA−hA+1+max(hC,hE)−hC

newBal(B)−oldBal(B)=1+max(hC,hE)−hC

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

newBal(B)=oldBal(B)+1+max(hC−hC,hE−hC)

但 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 行的语句:


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

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

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

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

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

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

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

 图 8:右旋转后左旋转

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

Listing 3


 
 
  1. def rebalance(self,node): 
  2.   if node.balanceFactor < 0: 
  3.          if node.rightChild.balanceFactor > 0: 
  4.             self.rotateRight(node.rightChild) 
  5.             self.rotateLeft(node) 
  6.          else
  7.             self.rotateLeft(node) 
  8.   elif node.balanceFactor > 0: 
  9.          if node.leftChild.balanceFactor < 0: 
  10.             self.rotateLeft(node.leftChild) 
  11.             self.rotateRight(node) 
  12.          else
  13.             self.rotateRight(node)  

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


作者:wzhvictor

来源:51CTO

相关文章
|
7天前
|
算法 Java Python
使用Python来绘制樱花树
本文以林徽因的《你是人间的四月天》为引,将春日意象与现代职场编程艺术结合,通过Python的Turtle模块绘制分形树和花瓣图案。文章详细解析了Turtle模块的使用方法、递归算法及随机性在图形生成中的应用,展示了如何用代码创造自然美感。核心代码包含tree函数(绘制分形树)和petal函数(绘制花瓣),最终生成一幅生动的春日画卷。项目不仅帮助读者掌握Turtle绘图技巧,更激发对编程艺术的兴趣,鼓励探索数字世界的无限可能。
47 5
|
1月前
|
算法 Java
算法系列之数据结构-Huffman树
Huffman树(哈夫曼树)又称最优二叉树,是一种带权路径长度最短的二叉树,常用于信息传输、数据压缩等方面。它的构造基于字符出现的频率,通过将频率较低的字符组合在一起,最终形成一棵树。在Huffman树中,每个叶节点代表一个字符,而每个字符的编码则是从根节点到叶节点的路径所对应的二进制序列。
63 3
 算法系列之数据结构-Huffman树
|
1月前
|
存储 自然语言处理 数据库
【数据结构进阶】AVL树深度剖析 + 实现(附源码)
在深入探讨了AVL树的原理和实现后,我们不难发现,这种数据结构不仅优雅地解决了传统二叉搜索树可能面临的性能退化问题,还通过其独特的平衡机制,确保了在任何情况下都能提供稳定且高效的查找、插入和删除操作。
104 19
|
24天前
|
存储 人工智能 索引
Python数据结构:列表、元组、字典、集合
Python 中的列表、元组、字典和集合是常用数据结构。列表(List)是有序可变集合,支持增删改查操作;元组(Tuple)与列表类似但不可变,适合存储固定数据;字典(Dictionary)以键值对形式存储,无序可变,便于快速查找和修改;集合(Set)为无序不重复集合,支持高效集合运算如并集、交集等。根据需求选择合适的数据结构,可提升代码效率与可读性。
|
4月前
|
存储 缓存 监控
局域网屏幕监控系统中的Python数据结构与算法实现
局域网屏幕监控系统用于实时捕获和监控局域网内多台设备的屏幕内容。本文介绍了一种基于Python双端队列(Deque)实现的滑动窗口数据缓存机制,以处理连续的屏幕帧数据流。通过固定长度的窗口,高效增删数据,确保低延迟显示和存储。该算法适用于数据压缩、异常检测等场景,保证系统在高负载下稳定运行。 本文转载自:https://www.vipshare.com
147 66
|
3月前
|
存储 C++
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
【数据结构——树】哈夫曼树(头歌实践教学平台习题)【合集】目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果:任务描述 本关任务:编写一个程序构建哈夫曼树和生成哈夫曼编码。 相关知识 为了完成本关任务,你需要掌握: 1.如何构建哈夫曼树, 2.如何生成哈夫曼编码。 测试说明 平台会对你编写的代码进行测试: 测试输入: 1192677541518462450242195190181174157138124123 (用户分别输入所列单词的频度) 预
109 14
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
|
3月前
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
95 12
|
3月前
|
C++
【C++数据结构——树】二叉树的性质(头歌实践教学平台习题)【合集】
本文档介绍了如何根据二叉树的括号表示串创建二叉树,并计算其结点个数、叶子结点个数、某结点的层次和二叉树的宽度。主要内容包括: 1. **定义二叉树节点结构体**:定义了包含节点值、左子节点指针和右子节点指针的结构体。 2. **实现构建二叉树的函数**:通过解析括号表示串,递归地构建二叉树的各个节点及其子树。 3. **使用示例**:展示了如何调用 `buildTree` 函数构建二叉树并进行简单验证。 4. **计算二叉树属性**: - 计算二叉树节点个数。 - 计算二叉树叶子节点个数。 - 计算某节点的层次。 - 计算二叉树的宽度。 最后,提供了测试说明及通关代
98 10
|
4月前
|
存储 运维 监控
探索局域网电脑监控软件:Python算法与数据结构的巧妙结合
在数字化时代,局域网电脑监控软件成为企业管理和IT运维的重要工具,确保数据安全和网络稳定。本文探讨其背后的关键技术——Python中的算法与数据结构,如字典用于高效存储设备信息,以及数据收集、异常检测和聚合算法提升监控效率。通过Python代码示例,展示了如何实现基本监控功能,帮助读者理解其工作原理并激发技术兴趣。
109 20
|
3月前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
117 2