1. 程式人生 > >Python-資料結構與演算法(九、二分搜尋樹)

Python-資料結構與演算法(九、二分搜尋樹)

保證一週更兩篇吧,以此來督促自己好好的學習!程式碼的很多地方我都給予了詳細的解釋,幫助理解。好了,幹就完了~加油! 宣告:本python資料結構與演算法是imooc上liuyubobobo老師java資料結構的python改寫,並添加了一些自己的理解和新的東西,liuyubobobo老師真的是一位很棒的老師!超級喜歡他~ 如有錯誤,還請小夥伴們不吝指出,一起學習~ No fears, No distractions.

一、基本知識點

1. 二叉樹

對於每個節點來說,它最多隻有兩個分支。 和連結串列一樣,是一種動態資料結構。

class Node{
    E e;           # 節點攜帶的元素
Node left; # 左孩子 Node right; # 右孩子 }

所以這麼看來就可以把連結串列看成是隻有一個孩子的一叉樹- - 特點:二叉樹具有唯一根節點

  1. 每個節點都有指向做孩子和右節點的指標,只不過有可能指向空(葉子節點)
  2. 二叉樹每個節點最多有兩個孩子(注意是最多,可以只有一個孩子,而另一個為None哦)
  3. 二叉樹每個節點最多有一個父親(根節點沒有父親節點!)
  4. 具有天然的遞迴結構 每個節點的左子樹也是二叉樹(以其左孩子為根的二叉樹) 每個節點的右子樹也是二叉樹(以其右孩子為根的二叉樹)
  5. 二叉樹不一定是“滿”的(有的節點可能只有左孩子,沒有右孩子,或者只有右孩子,沒有左孩子),當然一個節點也可以看做成是二叉樹(None也可以看成二叉樹!,是否是樹取決與你的主觀判斷),相應的程式碼也會發生一定的更改,因為判斷條件不一樣了嘛。

2. 典型的樹結構物件

  1. 樹結構是一種天然的組織結構,比如檔案系統。
  2. 有時將資料使用樹結構儲存後,出奇的高效。
  3. 樹結構的典型物件: 二分搜尋樹 平衡二叉樹:AVL;紅黑樹 堆;並查集 線段樹;Trie(字典樹,字首樹)

3. 二分搜尋樹

  1. 是二叉樹的一種
  2. 對於二分搜尋樹的每個節點的值(這裡討論的是無重複元素的二分搜尋樹): 大於其左子樹的所有節點的值 小於其右子樹所有節點的值
  3. 每一棵子樹也是一棵二分搜尋樹
  4. 二分搜尋樹不一定是“滿”樹(滿樹的定義前面有講)
  5. 儲存的元素必須有可比較性
  6. 我們實現的二分搜尋樹不包含重複元素 如果想包含重複元素的話,只需要定義: 左子樹小於等於節點,或者右子樹大於等於節點 注意:我們之前實現的陣列和連結串列,可以有重複元素 二分搜尋樹新增元素的非遞迴寫法,和連結串列很像

二、二分搜尋樹的實現

# -*- 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
是否包含28True
前序遍歷:(遞迴版本)
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