二叉搜尋樹
我們已經知道了在一個集合中獲取鍵值對的兩種不同的方法。回憶一下這些集合是如何實現ADT
(抽象資料型別)MAP
的。我們討論兩種ADT MAP
的實現方式,基於列表的二分查詢和雜湊表。在這一節中,我們將要學習二叉搜尋樹,這是另一種鍵指向值的Map集合,在這種情況下我們不用考慮元素在樹中的實際位置,但要知道使用二叉樹來搜尋更有效率。
搜尋樹操作
在我們研究這種實現方式之前,讓我們回顧一下ADT MAP
提供的介面。我們會注意到,這種介面和Python的字典非常相似。
Map()
建立了一個新的空Map集合。
put(key,val)
在Map中增加了一個新的鍵值對。如果這個鍵已經在這個Map中了,那麼就用新的值來代替舊的值。
get(key)
提供一個鍵,返回Map中儲存的資料,或者返回None
。
del
使用del map[key]
這條語句從Map中刪除鍵值對。
len()
返回Map中儲存的鍵值對的數目
in
如果所給的鍵在Map中,使用key in map
這條語句返回True
。
搜尋樹實現
一個二叉搜尋樹,如果具有左子樹中的鍵值都小於父節點,而右子樹中的鍵值都大於父節點的屬性,我們將這種樹稱為BST
搜尋樹。如之前所述的,當我們實現Map時,BST
方法將引導我們實現這一點。圖 1 展示了二叉搜尋樹的這一特性,顯示的鍵沒有關聯任何的值。注意這種屬性適用於每個父節點和子節點。所有在左子樹的鍵值都小於根節點的鍵值,所有右子樹的鍵值都大於根節點的鍵值。
圖 1:一個簡單的二叉搜尋樹
現在你知道什麼是二叉搜尋樹了,我們再來看如何構造一個二叉搜尋樹,我們在搜尋樹中按圖 1 顯示的節點順序插入這些鍵值,圖 1 搜尋樹存在的節點:70,31,93,94,14,23,73
。因為 70 是第一個被插入到樹的值,它是根節點。接下來,31 小於 70,因此是 70 的左子樹。接下來,93 大於 70,因此是 70 的右子樹。我們現在填充了該樹的兩層,所以下一個鍵值,將會是 31 或者 93 的左子樹或右子樹。由於 94 大於 70 和 93,就變成了 93 的右子樹。同樣,14 小於 70 和 31,因此成為了 31 的左子樹。23 也小於 31,因此必須是 31 的左子樹。然而,它大於 14,所以是 14 的右子樹。
為了實現二叉搜尋樹,我們將使用節點和引用的方法,這類似於我們實現連結串列和表示式樹的過程。因為我們必須能夠建立和使用一個空的二叉搜尋樹,所以我們將使用兩個類來實現,第一個類我們稱之為 BinarySearchTree
,第二個類我們稱之為TreeNode
。BinarySearchTree
類有一個TreeNode
類的引用作為二叉搜尋樹的根,在大多數情況下,外部類定義的外部方法只需檢查樹是否為空,如果在樹上有節點,要求BinarySearchTree
類中含有私有方法把根定義為引數。在這種情況下,如果樹是空的或者我們想刪除樹的根,我們就必須採用特殊操作。BinarySearchTree
類的建構函式以及一些其他函式的程式碼如Listing 1 所示。
Listing 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class BinarySearchTree: def __init__(self): self.root = None self.size = 0 def length(self): return self.size def __len__(self): return self.size def __iter__(self): return self.root.__iter__() |
TreeNode
類提供了許多輔助函式,使得BinarySearchTree
類的方法更容易實現過程。如Listing 2 所示,一個樹節點的結構,是由這些輔助函式實現的。正如你看到的那樣,這些輔助函式可以根據自己的位置來劃分一個節點作為左或右孩子和該子節點的型別。TreeNode
類非常清楚地跟蹤了每個父節點的屬性。當我們討論刪除操作的實現時,你將明白為什麼這很重要。
對於Listing 2 中的TreeNode
實現,另一個有趣的地方是,我們使用Python的可選引數。可選的引數很容易讓我們在幾種不同的情況下建立一個樹節點,有時我們想建立一個新的樹節點,即使我們已經有了父節點和子節點。與現有的父節點和子節點一樣,我們可以通過父節點和子節點作為引數。有時我們也會建立一個包含鍵值對的樹,我們不會傳遞父節點或子節點的任何引數。在這種情況下,我們將使用可選引數的預設值。
Listing 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
class TreeNode: def __init__(self,key,val,left=None,right=None, parent=None): self.key = key self.payload = val self.leftChild = left self.rightChild = right self.parent = parent def hasLeftChild(self): return self.leftChild def hasRightChild(self): return self.rightChild def isLeftChild(self): return self.parent and self.parent.leftChild == self def isRightChild(self): return self.parent and self.parent.rightChild == self def isRoot(self): return not self.parent def isLeaf(self): return not (self.rightChild or self.leftChild) def hasAnyChildren(self): return self.rightChild or self.leftChild def hasBothChildren(self): return self.rightChild and self.leftChild def replaceNodeData(self,key,value,lc,rc): self.key = key self.payload = value self.leftChild = lc self.rightChild = rc if self.hasLeftChild(): self.leftChild.parent = self if self.hasRightChild(): self.rightChild.parent = self |
現在,我們擁有了BinarySearchTree
和TreeNode
類,是時候寫一個put
方法使我們能夠建立二叉搜尋樹。put
方法是BinarySearchTree
類的一個方法。這個方法將檢查這棵樹是否已經有根。如果沒有,我們將建立一個新的樹節點並把它設定為樹的根。如果已經有一個根節點,我們就呼叫它自己,進行遞迴,用輔助函式_put
按下列演算法來搜尋樹:
- 從樹的根節點開始,通過搜尋二叉樹來比較新的鍵值和當前節點的鍵值,如果新的鍵值小於當前節點,則搜尋左子樹。如果新的關鍵大於當前節點,則搜尋右子樹。
- 當搜尋不到左(或右)子樹,我們在樹中所處的位置就是設定新節點的位置。
- 向樹中新增一個節點,建立一個新的
TreeNode
物件並在這個點的上一個節點中插入這個物件。
Listing 3 顯示了在樹中插入新節點的Python程式碼。_put
函式要按照上述的步驟編寫遞迴演算法。注意,當一個新的子樹插入時,當前節點(CurrentNode
)作為父節點傳遞給新的樹。
我們執行插入的一個重要問題是重複的鍵值不能被正確的處理,我們的樹實現了鍵值的複製,它將在右子樹建立一個與原來節點鍵值相同的新節點。這樣做的後果是,新的節點將不會在搜尋過程中被發現。我們用一個更好的方式來處理插入重複的鍵值,舊的值被新鍵關聯的值替換。我們把這個錯誤的修復,作為練習留給你。
Listing 3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
def put(self,key,val): if self.root: self._put(key,val,self.root) else: self.root = TreeNode(key,val) self.size = self.size + 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) else: if currentNode.hasRightChild(): self._put(key,val,currentNode.rightChild) else: currentNode.rightChild = TreeNode(key,val,parent=currentNode) |
隨著put
方法的實現,我們可以很容易地通過__setitem__
方法過載[]
作為操作符來呼叫put
方法(參見Listing 4)。這使我們能夠編寫像myZipTree['Plymouth'] = 55446
一樣的python語句,這看上去就像Python的字典。
Listing 4
1 2 3 |
def __setitem__(self,k,v): self.put(k,v) |
圖 2 說明了將新節點插入到一個二叉搜尋樹的過程。灰色節點顯示了插入過程中遍歷樹節點順序。
圖 2: 插入一個鍵值 = 19 的節點
一旦樹被構造,接下來的任務就是為一個給定的鍵值實現檢索。get
方法比put
方法更容易因為它只需遞迴搜尋樹,直到發現不匹配的葉節點或找到一個匹配的鍵值。當找到一個匹配的鍵值後,就會返回節點中的值。
Listing 5 顯示了get
,_get
和__getitem__
的程式碼。用_get
方法搜尋的程式碼與put
方法具有相同的選擇左或右子樹的邏輯。請注意,_get
方法返回TreeNode
中get
的值,_get
就可以作為一個靈活有效的方式,為BinarySearchTree
的其他可能需要使用TreeNode
裡的資料的方法提供引數。
通過實現__getitem__
方法,我們可以寫一個看起來就像我們訪問字典一樣的Python語句,而事實上我們只是操作二叉搜尋樹,例如Z = myziptree ['fargo']
。正如你所看到的,__getitem__
方法都是在呼叫get
。
Listing 5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
def get(self,key): if self.root: res = self._get(key,self.root) if res: return res.payload else: return None else: return None def _get(self,key,currentNode): if not currentNode: return None elif currentNode.key == key: return currentNode elif key < currentNode.key: return self._get(key,currentNode.leftChild) else: return self._get(key,currentNode.rightChild) def __getitem__(self,key): return self.get(key) |
使用get,我們可以通過寫一個BinarySearchTree
的__contains__
方法來實現操作,__contains__
方法簡單地呼叫了get
方法,如果它有返回值就返回True
,如果它是None
就返回False
。如Listing 6 所示。
Listing 6
1 2 3 4 5 6 |
def __contains__(self,key): if self._get(key,self.root): return True else: return False |
回顧一下__contains__
過載的操作符,這允許我們寫這樣的語句:
1 2 3 |
if 'Northfield' in myZipTree: print("oom ya ya") |