Python-資料結構與演算法(九、二分搜尋樹)
阿新 • • 發佈:2018-12-15
保證一週更兩篇吧,以此來督促自己好好的學習!程式碼的很多地方我都給予了詳細的解釋,幫助理解。好了,幹就完了~加油! 宣告:本python資料結構與演算法是imooc上liuyubobobo老師java資料結構的python改寫,並添加了一些自己的理解和新的東西,liuyubobobo老師真的是一位很棒的老師!超級喜歡他~ 如有錯誤,還請小夥伴們不吝指出,一起學習~ No fears, No distractions.
一、基本知識點
1. 二叉樹
對於每個節點來說,它最多隻有兩個分支。 和連結串列一樣,是一種動態資料結構。
class Node{
E e; # 節點攜帶的元素
Node left; # 左孩子
Node right; # 右孩子
}
所以這麼看來就可以把連結串列看成是隻有一個孩子的一叉樹- - 特點:二叉樹具有唯一根節點
- 每個節點都有指向做孩子和右節點的指標,只不過有可能指向空(葉子節點)
- 二叉樹每個節點最多有兩個孩子(注意是最多,可以只有一個孩子,而另一個為None哦)
- 二叉樹每個節點最多有一個父親(根節點沒有父親節點!)
- 具有天然的遞迴結構 每個節點的左子樹也是二叉樹(以其左孩子為根的二叉樹) 每個節點的右子樹也是二叉樹(以其右孩子為根的二叉樹)
- 二叉樹不一定是“滿”的(有的節點可能只有左孩子,沒有右孩子,或者只有右孩子,沒有左孩子),當然一個節點也可以看做成是二叉樹(None也可以看成二叉樹!,是否是樹取決與你的主觀判斷),相應的程式碼也會發生一定的更改,因為判斷條件不一樣了嘛。
2. 典型的樹結構物件
- 樹結構是一種天然的組織結構,比如檔案系統。
- 有時將資料使用樹結構儲存後,出奇的高效。
- 樹結構的典型物件: 二分搜尋樹 平衡二叉樹:AVL;紅黑樹 堆;並查集 線段樹;Trie(字典樹,字首樹)
3. 二分搜尋樹
- 是二叉樹的一種
- 對於二分搜尋樹的每個節點的值(這裡討論的是無重複元素的二分搜尋樹): 大於其左子樹的所有節點的值 小於其右子樹所有節點的值
- 每一棵子樹也是一棵二分搜尋樹
- 二分搜尋樹不一定是“滿”樹(滿樹的定義前面有講)
- 儲存的元素必須有可比較性
- 我們實現的二分搜尋樹不包含重複元素 如果想包含重複元素的話,只需要定義: 左子樹小於等於節點,或者右子樹大於等於節點 注意:我們之前實現的陣列和連結串列,可以有重複元素 二分搜尋樹新增元素的非遞迴寫法,和連結串列很像
二、二分搜尋樹的實現
# -*- coding: utf-8 -*-
# Author: Annihilation7
# Data: 2018-10-13 11:27 am
# Python version: 3.6
from Stack.arrayStack import ArrayStack # 我們實現的棧
from queue.loopqueue import LoopQueue # 我們實現的迴圈佇列
class Node:
def __init__(self, elem):
"""
節點建構函式,三個成員:攜帶的元素,指向左孩子的指標(標籤),指向右孩子的指標(標籤)
:param elem: 攜帶的元素
"""
self.elem = elem
self.left = None # 左孩子設為空
self.right = None # 右孩子設為空
class BST:
def __init__(self):
"""
二分搜尋樹的建構函式——————空樹
"""
self._root = None # 根節點設為None
self._size = 0 # 有效元素個數初始化為0
def getSize(self):
"""
返回節點個數
:return: 節點個數
"""
return self._size
def isEmpty(self):
"""
判斷二分搜尋樹是否為空
:return: bool值,空為True
"""
return self._size == 0
def add(self, elem):
"""
向二分搜尋樹插入元素elem
時間複雜度:O(logn)
:param elem: 待插入的元素
:return: 二分搜尋樹的根
"""
self._root = self._add(self._root, elem) # 呼叫私有函式self._add
def contains(self, elem):
"""
檢視二分搜尋樹中是否包含elem
時間複雜度:O(logn)
:param elem: 待查詢元素
:return: bool值,查到為True
"""
return self._contains(self._root, elem) # 呼叫私有函式self._contains
def preOrfer(self):
"""
二分搜尋樹的前序遍歷
時間複雜度:O(n)
前序遍歷、中序遍歷以及後續遍歷是針對當前的根節點來說的。前序就是把對根節點的操作放在遍歷左、右子樹的前面,相應的中序遍歷以及後序遍歷以此類推
前序遍歷是最自然也是最常用的二叉搜尋樹的遍歷方式
"""
self._preOrder(self._root) # 呼叫self._preOrder函式
def inOrder(self):
"""
二分搜尋樹的中序遍歷
時間複雜度:O(n)
特點:輸出的元素是從小到大排列的,因為先處理左子樹,到底後再處理當前節點,最後再處理右子樹,而左子樹的值都比當前節點小,
右子樹的值都比當前節點大,所以是排序輸出
"""
self._inOrder(self._root) # 呼叫self._inOrder函式
def postOrder(self):
"""
二分搜尋樹的後序遍歷
應用場景:二叉搜尋樹的記憶體回收,例如C++中的解構函式
時間複雜度:O(n)
"""
self._postOrder(self._root) # 呼叫self._postOrder函式
def preOrderNR(self):
"""
前序遍歷的非遞迴寫法
此時需要藉助一個輔助的資料結構————棧
時間複雜度:O(n)
空間複雜度:O(n)
技巧:壓棧的時候先右孩子,再左孩子,從而左孩子先出棧。
"""
self._preOrderNR(self._root) # 呼叫self._preOrderNE函式
def levelOrder(self):
"""
層序遍歷(廣度優先遍歷)
時間複雜度:O(n)
空間複雜度:O(n)
"""
self._levelOrder(self._root) # 呼叫self._levelOrder函式
def minimum(self):
"""
Description: 返回當前二叉搜尋樹的最小值
時間複雜度:O(n)
"""
if self.getSize() == 0: # 空樹直接報錯
raise Exception('Empty binary search tree!')
return self._minimum(self._root).elem # 呼叫self._minimum函式,它傳入當前的根節點
def maximum(self):
"""
Description: 返回當前二叉搜尋樹的最大值
時間複雜度:O(logn)
"""
if self.getSize() == 0: # 空樹直接報錯
raise Exception('Empty binary search tree!')
return self._maximum(self._root).elem # 呼叫self._maxmum函式,它傳入當前的根節點
def removeMin(self):
"""
Description: 刪除當前二叉搜尋樹的最小值的節點
時間複雜度:O(logn)
Returns: 被刪除節點所攜帶的元素的值
"""
ret = self.minimum() # 找到當前二叉搜尋樹的最小值
self._root = self._removeMin(self._root) # 呼叫self._removeMin函式,該函式返回刪除節點後的二叉搜尋樹的根節點
return ret # 返回最小值
def removeMax(self):
"""
Description: 刪除當前二叉搜尋樹的最大值的節點
時間複雜度:O(logn)
Returns: 被刪除節點所攜帶的元素的值
"""
ret = self.maximum() # 找到當前二叉搜尋樹的最大值
self._root = self._removeMax(self._root) # 呼叫self._removeMax函式,該函式返回刪除節點後的二叉搜尋樹的根節點
return ret # 返回最大值
def remove(self, elem):
"""
Description: 刪除二叉搜尋樹中值為elem的節點,注意我們的二叉搜尋樹中的元素的值是不重複的,所以刪除就是真正的刪除,無殘餘
這個演算法是二叉搜尋樹中最難的一個演算法
note: 因為刪除的是指定的值,使用者已經直到該值了,所以就不需要返回這個值了。
時間複雜度:O(logn)
"""
self._root = self._remove(self._root, elem) # 呼叫self._remove函式,該函式返回刪除節點後二叉搜尋樹的根節點
# private
def _add(self, node, elem):
"""
向以Node為根的二分搜尋樹插入元素elem,遞迴演算法,這個根可以是任意節點哦,因為二分搜尋樹的每一個節點都是一個新的二分搜尋樹的根節點
:param Node: 根節點
:param elem: 帶插入元素
:return: 插入新節點後二分搜尋樹的根
"""
if node is None: # 遞迴到底的情況,此時已經到了None的位置。注意Node也算一棵二分搜尋樹
self._size += 1 # 維護self._size
return Node(elem) # 新建一個攜帶elem的節點Node,並將它返回
if elem < node.elem: # 待新增元素小於當前節點的elem值
node.left = self._add(node.left, elem) # 繼續遞歸向node的左子樹新增elem,設想此時node.left已經為空,根據上面的語句,
# 將返回一個新節點,而此時這個節點與二叉搜尋樹沒有任何聯絡,所以要用node.left接住這個新節點,從而讓新節點掛接到二叉搜尋樹上
elif node.elem < elem: # 當前節點的elem值小於待新增元素,原理同上
node.right = self._add(node.right, elem)
# 注意我們實現的是一個不帶重複元素的二分搜尋樹,所以要用elif,而不是else,相當於對於插入了一個重複元素,我們什麼也不做
return node # 最後要把node返回,還是這個根,滿足定義。
def _contains(self, node, elem):
"""
在以node為根的二叉搜尋樹中查詢是否包含元素elem
:param node: 根節點
:param elem: 帶查詢元素
:return: bool值,存在為True
"""
if node is None: # 遞迴到底的情況,已經到None了,還沒有找到,返回False
return False
if node.elem < elem: # 節點元素小於帶查詢元素,就向右子樹的根節點遞迴查詢
return self._contains(node.right, elem)
elif elem < node.elem: # 帶查詢元素小於節點元素,就向左子樹的根節點遞迴查詢
return self._contains(node.left, elem)
else: # 最後一種情況就是相等了,此時返回True
return True
def _preOrder(self, node):
"""
對以node為根的節點的二叉搜尋樹的前序遍歷
:param node: 當前根節點
"""
if node is None: # 同樣的,先寫好遞迴到底的情況
return
print(node.elem, end=' ') # 在這裡我只是對當前節點進行了列印操作,並沒有什麼別的操作
self._preOrder(node.left) # 前序遍歷以node.left為根節點的二叉搜尋樹
self._preOrder(node.right) # 最後才是右子樹
def _inOrder(self, node):
"""
對以node為根節點的二叉搜尋樹的中序遍歷
:param node: 當前根節點
"""
if node is None: # 遞迴到底的情況
return
self._inOrder(node.left) # 先左子樹
print(node.elem, end=' ') # 再當前節點的操作,這裡只是列印
self._inOrder(node.right) # 最後右子樹
def _postOrder(self, node):
"""
對以node為根節點的二叉搜尋樹的後序遍歷
:param node: 當前根節點
"""
if node is None: # 遞迴到底的情況
return
self._postOrder(node.left) # 先左子樹
self._postOrder(node.right) # 再右子樹
print(node.elem, end=' ') # 最後進行當前節點的操作
def _preOrderNR(self, node):
"""
對以node為根節點的二叉搜尋樹的非遞迴的前序遍歷
:param node: 當前根節點
"""
stack = ArrayStack() # 初始化一個我們以前實現的棧
if node:
stack.push(node) # 如果根節點不為空,就首先入棧
else:
return
while not stack.isEmpty(): # 棧始終不為空
ret = stack.pop() # 出棧並拿到棧頂的節點
print(ret.elem, end=' ') # 列印(我這裡就只選擇列印操作了,當然可以對這個節點執行任何你想要的操作)
if ret.right: # 出棧後,看一下ret的左右孩子,先入右孩子
stack.push(ret.right)
if ret.left: # 再入棧左孩子,想想為什麼是先右後左
stack.push(ret.left)
def _levelOrder(self, node):
"""
Description: 對以node為根節點的二叉搜尋樹的廣度優先遍歷
Params:
- node: 當前根節點
"""
if node is None: # node本身就是None
return
queue = LoopQueue() # 建立一個我們以前實現的迴圈佇列,作為輔助資料結構
queue.enqueue(node) # 當前根節點入隊
while not queue.isEmpty(): # 如果佇列不為空
tmp_node = queue.dequeue() # 取出隊首的元素
print(tmp_node.elem, end=' ') # 這裡僅僅是列印,無其他操作
if tmp_node.left: # 如果左孩子不是None
queue.enqueue(tmp_node.left) # 將它的左孩子入隊,注意是先左孩子後右孩子哦,想象為什麼是這樣
if tmp_node.right: # 如果右孩子不是None
queue.enqueue(tmp_node.right) # 右孩子入隊
def _minimum(self, node):
"""
Description: 返回以node為根的二叉搜尋樹攜帶最小值的節點
"""
if node.left is None: # 遞迴到底的情況,二叉搜尋樹的最小值就從當前節點一直向左孩子查詢就好了
return node
return self._minimum(node.left) # 否則向該節點的左子樹繼續查詢
def _maximum(self, node):
"""
Description: 返回以node為根的二叉搜尋樹攜帶最大值的節點
"""
if node.right is None: # 遞迴到底的情況,二叉搜尋樹的最大值就從當前節點一直向右孩子查詢就好了
return node
return self._maximum(node.right) # 否則向該節點的右子樹繼續查詢
def _removeMin(self, node):
"""
Descriptoon: 刪除以node為根節點的二叉搜尋樹攜帶最小值的節點
Returns: 刪除後的二叉搜尋樹的根節點,與新增操作有異曲同工之處
"""
if node.left is None: # 遞迴到底的情況
right_node = node.right # 記錄當前節點的右節點,即使是None也沒關係
node.right = None # 將當前節點的右節點置為None,便於垃圾回收
self._size -= 1 # 維護self._size
return right_node # 返回當前節點的右子樹的根,因為刪除最小節點有兩種情況,一種是node是葉子節點,直接用None來代替就好了。另外一種就是node還有右子樹
# 此時需要用node的右節點來代替當前的節點
node.left = self._removeMin(node.left) # 沒到底就繼續向左子樹前進,注意要用node.left接住被刪除節點的右節點,從而與整棵樹產生連線。
return node # 將節點返回,從而在遞迴演算法完成後的迴歸過程中逐層返回直到最後到根節點
def _removeMax(self, node):
"""
Description: 刪除以node為根節點的二叉搜尋樹攜帶最大值的節點
Returns: 刪除後的二叉搜尋樹的根節點,與新增操作有異曲同工之處
"""
if node.right is None: # 與self.removeMin原理差不多,不再贅述
left_node = node.left
node.left = None
self._size -= 1
return left_node
node.right = self._removeMax(node.right)
return node
def _remove(self, node, elem):
"""
Description: 刪除以node為根節點的二叉搜尋樹中攜帶值為elem的節點
Returns: 刪除節點後的二叉搜尋樹的根節點
"""
if node is None: # 沒找到攜帶elem的節點
return None
if elem < node.elem: # 要尋找的元素小於當前節點的elem值
node.left = self._remove(node.left, elem) # 向node的左子樹繼續尋找,注意要用node.left接住返回值,從而讓代替被刪除節點的節點與搜尋樹產生連線
return node # 返回node,從而在遞迴完事後的迴歸過程中最終返回到搜尋樹的根節點
elif node.elem < elem: # 同理
node.right = self._remove(node.right, elem)
return node
else: # 此時 elem == node.elem
if node.left is None: # node左子樹為空的情況,單獨處理,與前面的刪除最大/最小節點的方法一致,不再贅述
ret = node.right
node.right = None
self._size -= 1
return ret
elif node.right is None: # node右子樹為空的情況
ret = node.left
node.left = None
self._size -= 1
return ret
else: # 此時node左右子樹均不為空,此時是該演算法重頭戲
# 既然node的左右子樹均不為空,那麼刪除node後究竟要用誰來接替這個刪除後的空位呢,答案是node的前驅或者後繼節點!前驅節點就是node左子樹攜帶最大值的節點
# 這個節點滿足:它的elem一定小於node右子樹全部元素的elem,但是還大於左子樹全部元素的elem(除了他自己--),同理後繼是node右子樹的最小值,代替
# node後也滿足二叉搜尋樹的要求,本文通過node的後繼來實現,小夥伴們可以用前驅來實現,也非常簡單。
successor = self._minimum(node.right) # 通過self._minimum方法找到node的後繼節點,並記為seccessor
successor.right = self._removeMin(node.right) # 通過self._removeMin方法將node的右子樹的最小節點刪除,注意返回的刪除節點的新的右子樹的根節點,所以
# 此時直接將返回值作為successor的右節點就可以了
self._size += 1 # 但是我們的目的是讓後繼來取代被刪除的位置的節點,並不是要刪除它,而self._removeMin方法中已經對self._size進行了維護,所以在這裡我們要加回來
successor.left = node.left # successor的左孩子就是node的左孩子就好了,代替嘛,畫個圖看看就懂啦
node.left = node.right = None # 可以把node扔了,他已經沒用了,讓node從樹中脫離
self._size -= 1 # 把二叉搜尋樹上的節點都扔了,肯定要維護一下self._size
return successor # 返回取代node後的後繼節點
三、測試
# -*- coding: utf-8 -*-
from bst import BST
test_bst = BST()
print('初始大小:', test_bst.getSize())
print('是否為空:', test_bst.isEmpty())
add_list = [15, 4, 25, 22, 3, 19, 23, 7, 28, 24]
print('待新增元素:', add_list)
for add_elem in add_list:
test_bst.add(add_elem)
##################################################
# 15 #
# / \ #
# 4 25 #
# / \ / \ #
# 3 7 22 28 #
# / \ #
# 19 23 #
# \ #
# 24 #
##################################################
print('新增元素後,樹的大小:', test_bst.getSize())
print('是否包含28?', test_bst.contains(28))
print('前序遍歷:(遞迴版本)')
test_bst.preOrfer()
print() # 為了美觀,起換行作用
print('前序遍歷:(非遞迴版本)')
test_bst.preOrderNR()
print()
print('中序遍歷:')
test_bst.inOrder()
print()
print('後序遍歷:')
test_bst.postOrder()
print()
print('廣度優先遍歷(層序遍歷):')
test_bst.levelOrder()
print()
print('樹中最小值:', test_bst.minimum())
print('樹中最大值:', test_bst.maximum())
print('-------------------------------------------')
print('刪除最小值後的層序遍歷以及樹的大小')
print('刪除的最小值為:', test_bst.removeMin())
print('層序遍歷:', end=' ')
test_bst.levelOrder()
print()
print('最小值刪除後的size:', test_bst.getSize())
print('-------------------------------------------')
print('刪除最大值後的層序遍歷以及樹的大小')
print('刪除的最大值為:', test_bst.removeMax())
print('層序遍歷:', end=' ')
test_bst.levelOrder()
print()
print('最大值刪除後的size:', test_bst.getSize())
print('-------------------------------------------')
print('刪除特定元素22,以及刪除後樹的大小')
test_bst.remove(22)
print('層序遍歷:', end=' ')
test_bst.levelOrder()
print()
print('刪除22後的size:', test_bst.getSize())
print('-------------------------------------------')
四、輸出
初始大小: 0
是否為空: True
待新增元素: [15, 4, 25, 22, 3, 19, 23, 7, 28, 24]
新增元素後,樹的大小: 10
是否包含28? True
前序遍歷:(遞迴版本)
15 4 3 7 25 22 19 23 24 28
前序遍歷:(非遞迴版本)
15 4 3 7 25 22 19 23 24 28
中序遍歷:
3 4 7 15 19 22 23 24 25 28
後序遍歷:
3 7 4 19 24 23 22 28 25 15
廣度優先遍歷(層序遍歷):
15 4 25 3 7 22