1. 程式人生 > 程式設計 >資料結構之二叉樹——二叉查詢樹

資料結構之二叉樹——二叉查詢樹

定義

二叉查詢樹(Binary Search Tree),又稱為二叉搜尋樹,二叉排序樹。它可以是一棵空樹,如果不是空樹,則具有下列的性質:

  • 非空左子樹的所有鍵值小於其根結點的鍵值
  • 非空右子樹的所有鍵值大於其根結點的鍵值
  • 左、右子樹都是二叉查詢樹

比如下面兩棵樹,左邊的樹,因為5小於10,應該在10的左子樹上,因此不是二叉查詢樹,右邊的樹則符合二叉查詢樹的條件。

由於二叉查詢樹的特性,中序遍歷二叉查詢樹,得到的就是一個升序的數列。以上圖右邊的樹為例,它的中序遍歷則是:15 30 33 41 50

查詢

二叉查詢樹的查詢過程,可以分為下面的步驟:

  • 從根結點開始,如果樹為空,則返回NULL
  • 如果樹不為空,則根結點的值與查詢值X進行比較,並進行不同的處理
    • 如果X等於根結點的值
      ,則查詢完成,返回結點
    • 如果X小於根結點的值,則在左子樹中繼續查詢
    • 如果X大於根結點的值,則在右子樹中繼續查詢

下圖以查詢33為例,展示了查詢的軌跡

根據上面的步驟,可以使用遞迴寫出該演演算法

def find(root: TreeNode,key):
    if not root:
        return None
    if key == root.val:
        return root
    elif key < root.val:
        return find(root.left,key)
    else:
        return
find(root.right,key) 複製程式碼

當然,上面的遞迴演演算法中存在尾遞迴,一般的編譯器都會自動優化尾遞迴,我們也可以將程式碼改為迭代的方式

def find(root: TreeNode,key):
    if not root:
        return None
    while root:
        if key == root.val:
            return root
        elif key < root.val:
            root = root.left
        else:
            root = root.right
複製程式碼

可以看的出來,演演算法的複雜度和樹的高度有關,每一次對比之後都會捨棄掉原來資料的一半,因此演演算法的時間複雜為O(logn)

最小元素與最小元素

根據二叉查詢樹的性質:

  • 最小元素一定是在樹的最左分枝的端結點上
  • 最大元素一定是在樹的最右分枝的端結點上
    演演算法的實現也比較清晰
def findMax(root: TreeNode):
    if root:
        while root.right:
            root = root.right
    return root


def findMin(root: TreeNode):
    if root:
        while root.left:
            root = root.left
    return root
複製程式碼

插入結點

由於二叉查詢樹具有特定的性質,因此在插入新的結點的時候,也要保證二叉查詢樹的性質,整個插入新結點的過程與查詢的過程類似,

以插入35為例,下圖展示了插入35的軌跡

以插入32為例,下圖展示例插入32的軌跡

def insert(root: TreeNode,key):
    if not root:
        return TreeNode(key) # 如果是空樹,則新建根結點
    while root:
        if key == root.val:
            break # 如果插入的key已經存在,則直接退出迴圈
        elif key < root.val:
            # 小於向左子樹查詢
            if not root.left:
                root.left = TreeNode(key)
            else:
                root = root.left
        else:
            # 大於向右子樹查詢
            if not root.right:
                root.right = TreeNode(key)
            else:
                root = root.right
    return root
複製程式碼

刪除結點

刪除操作比較複雜,要分情況談論:

要刪除的結點為葉子結點

這種情況下,直接刪除。如下圖要刪除35結點

要刪除的結點只有一個孩子結點

這種情況下,直接將要刪除結點的孩子結點,向上提,替代要刪除的結點,如下圖要刪除33結點

要刪除的結點有左、右兩棵子樹

這種情況下,可以有兩種方案:

  • 左子樹的最大元素替代要刪除結點
  • 右子樹的最小元素替代要刪除結點

如果要刪除41結點(使用左子樹最大元素代替)

左子樹的最大元素,依舊比右子樹的所有元素小,但是比其他左子樹的元素大,因此它成為新的根結點的時候,依舊能保持左子樹下的所有元素比根結點小,右子樹下的所有元素比根結點大。

如果要刪除41結點(使用右子樹最小元素代替)

右子樹的最小元素,依舊比左子樹的所有元素大,但是比其他右子樹的元素小,成為新的根結點,依舊能保持二叉查詢樹的性質

綜合上面的情況,程式碼如下

def delete(root: TreeNode,key):
    if not root:
        return None
    if key < root.val:
        root.left = delete(root.left,key)
    elif key > root.val:
        root.right = delete(root.right,key)
    else:
        if root.left and root.right:
            # 有左右孩子
            leftMax = findMax(root.left)
            root.val = leftMax.val
            root.left = delete(root.left,leftMax.val)
        elif not root.left and not root.right:
            # 葉子結點
            root = None
        elif root.left:
            # 只有左孩子
            root = root.left
        elif root.right:
            # 只有右孩子
            root = root.right
    return root
複製程式碼

在有左右孩子的情況下,實際上只是把替代結點的值賦值給要刪除結點的值真正刪除的是原來的替代結點,比如要刪除41結點,替代結點為35,則把41改為35,然後刪除原來的35結點。

總結

二叉查詢樹將資料按照順序組織好,排列成二叉樹結構,因此相關演演算法的複雜度基本取決於樹的高度,如果在構建二叉查詢樹的時候,不注意樹的高度,容易構建成一棵斜樹,這樣二叉查詢樹就失去了優勢。

二叉查詢樹有較高的插入和刪除效率,並且具備較高的查詢效率,對組織動態資料比較友好。

Thanks!