1. 程式人生 > >Python資料結構——AVL樹的實現

Python資料結構——AVL樹的實現

  • 遞迴呼叫已到達樹的根。
  • 父節點的平衡因子已調整為零。一旦子樹平衡因子為零,那麼父節點的平衡因子不會發生改變。

我們將實現 AVL 樹的子類BinarySearchTree。首先,我們將重寫_put方法,並寫一個新的輔助方法updateBalance。這些方法如Listing 1 所示。除了第 7 行和第 13 行對 updateBalance的呼叫,你會注意到_put和簡單的二叉搜尋樹的定義完全相同。

Listing 1

Python
1234567891011121314151617181920212223242526 def_put(self,key,val,currentNode):ifkey<currentNode.key:ifcurrentNode.hasLeftChild():self._put(key,val,currentNode.leftChild)else:currentNode.leftChild=TreeNode(key,val,parent=currentNode)self.updateBalance(currentNode
.leftChild)else:ifcurrentNode.hasRightChild():self._put(key,val,currentNode.rightChild)else:currentNode.rightChild=TreeNode(key,val,parent=currentNode)self.updateBalance(currentNode.rightChild)defupdateBalance(self,node):ifnode.balanceFactor>1ornode.balanceFactor<-1:self.rebalance(node)returnif
node.parent!=None:ifnode.isLeftChild():node.parent.balanceFactor+=1elifnode.isRightChild():node.parent.balanceFactor-=1ifnode.parent.balanceFactor!=0: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

123456789101112131415161718 def rotateLeft(self,rotRoot):newRoot=rotRoot.rightChildrotRoot.rightChild=newRoot.leftChildifnewRoot.leftChild!=None:newRoot.leftChild.parent=rotRootnewRoot.parent=rotRoot.parentifrotRoot.isRoot():self.root=newRootelse:ifrotRoot.isLeftChild():rotRoot.parent.leftChild=newRootelse:rotRoot.parent.rightChild=newRootnewRoot.leftChild=rotRootrotRoot.parent=newRootrotRoot.balanceFactor=rotRoot.balanceFactor+1-min(newRoot.balanceFactor,0)newRoot.balanceFactor=newRoot.balanceFactor+1+max(rotRoot.balanceFactor,0)

最後,第 16-17 行需要解釋一下。這兩行我們更新了舊根和新根的平衡因子。因為其他操作都是移動整個子樹,被移動的子樹內的節點的平衡因子不受旋轉的影響。但我們如何在沒有重新計算新的子樹的高度的情況下更新平衡因子?下面的推導將讓你明白,這些程式碼都是正確的。

圖 5:左旋轉

qq%e5%9b%be%e7%89%8720161008005816現在方程所有的項都是已知數。如果我們記得 B 是rotRoot,D 是newRoot,可以看出這正好符合第 16 行的語句:

12 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

12345678 def rebalance(self,node):ifnode.balanceFactor0:self.rotateRight(node.rightChild)self.rotateLeft(node)else:self.rotateLeft(node)elif node.balanceFactor>0:ifnode.leftChild.balanceFactor

通過保持樹的平衡,我們可以確保get方法執行的時間複雜度為 O(log2n)。但問題是put方法的時間複雜度是多少?我們把put操作進行分解。由於每一個新節點都是作為葉節點插入的,每一輪更新所有父節點的平衡因子最多隻需要 log2n 次操作,每層執行一次。如果子樹是不平衡的最多需要兩個旋轉把子樹恢復平衡。但是,每個旋轉的操作的複雜度為 O(1) ,所以即使我們進行put操作最終的複雜度仍然是 O(log2n)。

相關推薦

Python資料結構——AVL實現

遞迴呼叫已到達樹的根。 父節點的平衡因子已調整為零。一旦子樹平衡因子為零,那麼父節點的平衡因子不會發生改變。 我們將實現 AVL 樹的子類BinarySearchTree。首先,我們將重寫_put方法,並寫一個新的輔助方法updateBalance。這些方法如Listing 1 所示。除了第 7 行和第

Python資料結構——AVL的基本概念

平衡二叉搜尋樹 在上一節中我們討論了建立一個二叉搜尋樹。我們知道,當樹變得不平衡時get和put操作會使二叉搜尋樹的效能降低到O(n)。在這一節中我們將看到一種特殊的二叉搜尋樹,它可以自動進行調整,以確保樹隨時都保持平衡。這種樹被稱為AVL樹,命名源於其發明者:G.M. Ade

Python資料結構——解析的遍歷

解析樹 完成樹的實現之後,現在我們來看一個例子,告訴你怎麼樣利用樹去解決一些實際問題。在這個章節,我們來研究解析樹。解析樹常常用於真實世界的結構表示,例如句子或數學表示式。 圖 1:一個簡單句的解析樹 圖 1 顯示了一個簡單句的層級結構。將一個句子表示為一個樹,能使我們通過利用子樹來處理句子中的每個獨立的

資料結構——AVL

    AVL樹是最早提出的自平衡二叉樹,在AVL樹中任何節點的兩個子樹的高度最大差別為一,即|HL-HR|<=1,所以它也被稱為高度平衡樹。AVL樹得名於它的發明者G.M. Adelson-Velsky和E.M. Landis,他們在1962年的論文《An a

資料結構-----AVL的插入刪除操作

對於AVL的插入和刪除,主要利用的就是上篇文章所述的四種旋轉操作,根據插入後不同的結構選用不同的方式復原平衡。 再次宣告一下,http://www.cnblogs.com/QG-whz/p/5167238.html這篇文章講解的比較好,我也是從連結處的大神那學習的,這裡只用

資料結構實現 10.2:對映_基於AVL實現(C++版)

資料結構實現 10.2:對映_基於AVL樹實現(C++版) 1. 概念及基本框架 2. 基本操作程式實現 2.1 增加操作 2.2 刪除操作 2.3 修改操作 2.4 查詢操作 2.5 其他操作 3.

Python資料結構——二叉搜尋實現(下)

搜尋樹實現(續) 最後,我們把注意力轉向二叉搜尋樹中最具挑戰性的方法,刪除一個鍵值(參見Listing 7)。首要任務是要找到搜尋樹中要刪除的節點。如果樹有一個以上的節點,我們使用_get方法找到需要刪除的節點。如果樹只有一個節點,這意味著我們要刪除樹的根,但是我們仍然要檢查根的鍵值是否與要刪除的鍵值匹配。

平衡二叉AVL)建立、查詢、插入操作 《大話資料結構》 c++實現程式碼

//平衡二叉樹,或者稱為AVL樹 #include<iostream> using namespace std; typedef int status; #define true 1 #define false 0 #define LH +1 //左高

Python資料結構——二叉搜尋實現(上)

二叉搜尋樹 我們已經知道了在一個集合中獲取鍵值對的兩種不同的方法。回憶一下這些集合是如何實現ADT(抽象資料型別)MAP的。我們討論兩種ADT MAP的實現方式,基於列表的二分查詢和雜湊表。在這一節中,我們將要學習二叉搜尋樹,這是另一種鍵指向值的Map集合,在這種情況下我們不用

Python資料結構——實現

在用巢狀列表表示樹時,我們使用 Python 的列表來編寫這些函式。雖然把介面寫成列表的一系列方法與我們已實現其他的抽象資料型別有些不同,但這樣做比較有意思,因為它為我們提供一個簡單、可以直接檢視的遞迴資料結構。在列表實現樹時,我們將儲存根節點作為列表的第一個元素的值。列表的第二個元素的本身是一個表示左子樹

JAVA數據結構--AVL實現

oid 非遞歸 max https -a ext 二叉 line 英語 AVL樹的定義 在計算機科學中,AVL樹是最先發明的自平衡二叉查找樹。在AVL樹中任何節點的兩個子樹的高度最大差別為1,所以它也被稱為高度平衡樹。查找、插入和刪除在平均和最壞情況下的時間復雜度都是。增

Python-資料結構與演算法(十一、字典(對映)——基於兩種不同的底層實現

保證一週更兩篇吧,以此來督促自己好好的學習!程式碼的很多地方我都給予了詳細的解釋,幫助理解。好了,幹就完了~加油! 宣告:本python資料結構與演算法是imooc上liuyubobobo老師java資料結構的python改寫,並添加了一些自己的理解和新的東西,liuyubobobo

python資料結構之KMP演算法的實現

我相信網上已經有很多關於KMP演算法的講解,大致都是關於部分匹配表的實現思路和作用,還有就是目標串的下標不變,僅改變模式串的下標來進行匹配,確實用KMP演算法,當目標串很大模式串很小時,其效率很高的,但都是相對而言。至於對於部分匹配表的作用以及實現思路,建議看一下這篇文章寫的是比較易懂的

python 資料結構與演算法 day05 二叉的深度優先遍歷(縱向)

1. 二叉樹深度優先遍歷三種方式   不同於樹的廣度優先遍歷(一層一層的走,同一層從左到右走完開始走下一層的橫向遍歷方式),深度優先遍歷是一條路走到黑,然後再走下一條;    先序遍歷:根節點--左子節點---右子節點(先從根節點開始,走左子樹,對這個左子樹依然按照根節點

資料結構——鏈佇列實現二叉的層次遍歷

在二叉樹的遍歷這篇部落格中https://www.cnblogs.com/wkfvawl/p/9901462.html 對於二叉樹的層次遍歷我只是給出了基於C++ STL的程式碼,這裡我使用資料結構的連結串列,構建一個鏈佇列來實現。這也算是我第一次使用鏈佇列來完成某個任務,鏈佇列程式碼還是來自課本,因為之前

Python資料結構——二叉排序

二叉排序樹的過程主要是:二叉樹的構建和遍歷。 當樹構建好後,對樹進行中序遍歷(左中右),即可得到,對資料從小到大排序的結果。 如果對樹進行“右中左遍歷”,則可以得到,對資料從大到小排序的結果 # -*- coding:utf-8 -*- # file: pySort.py #

Python資料結構——二叉的遍歷(先根,中根,後根)

先序遍歷:根左右 中序遍歷:左根右 後序遍歷:左右根 # -*- coding:utf-8 -*- # file: TreeTraversal.py # class BTree: # 二叉樹節點 def __init__(self, value):

Python資料結構--遍歷演算法

1 ''' 2 遍歷是訪問樹的所有節點的過程,也可以列印它們的值。 因為所有節點都通過邊(連結)連線,所以始終從根(頭)節點開始。 3 也就是說,我們不能隨機訪問樹中的一個節點。 這裡介紹三種方式來遍歷一棵樹 -順序遍歷 -前序遍歷 -後序遍歷 4 ''' 5 6 7 class No

Python資料結構——二叉堆的實現

優先佇列的二叉堆實現 在前面的章節裡我們學習了“先進先出”(FIFO)的資料結構:佇列(Queue)。佇列有一種變體叫做“優先佇列”(Priority Queue)。優先佇列的出隊(Dequeue)操作和佇列一樣,都是從隊首出隊。但在優先佇列的內部,元素的次序卻是由“優先順序”來決定:高優先順序

python資料結構之二叉

這裡用python 實現了二叉樹 # Definition for a binary tree node. class TreeNode: def __init__(self, x): self.val = x self.left =