---
# System prepended metadata

title: '[資演] 10- 二元搜尋樹'
tags: [資演]

---

# [資演] 10- 二元搜尋樹

###### tags: `資演`

![](https://hackmd.io/_uploads/BJyGueX-c.png)

在生活中少不了要**比大小**的場景。所謂的比大小，其實是要對一群可以互相比較的量值（例如整數、浮點數）進行**排序**。

二元搜尋樹（binary search tree, BST）就是一種為了方便快速排序而誕生的資料結構。從名字也可以看出來，它和binary search脫不了關係。

## 什麼是二元搜尋樹？
簡單的說，二元搜尋樹是一種特別的二元樹，他滿足以下性質：
* 若任意節點的左子樹不為空，則左子樹上所有節點的值均小於它的根節點的值
* 若任意節點的右子樹不為空，則右子樹上所有節點的值均大於它的根節點的值
* 任意節點的左、右子樹也分別為二元搜尋樹
* 不存在鍵值相等的節點

簡單來說就是，對於任一個在二元搜尋樹：
* 左子樹都比父節點小
* 右子樹都比父節點大
* 每一個節點的值都不重複

所以當我們要查找資料的時候，就可以從根節點開始，比根節點小的就從左子樹開始找，比較大的就從右子樹開始找，如此不斷重複下去，直到找到目標節點、或者走到葉節點為止。


## 二元搜尋樹上的操作

### 搜尋
給定一個值`key`和一個BST（的根節點）`root`，我們想在這個BST中尋找這個值有沒有存在BST中，如果有則回傳`True`，沒有則回傳`False`。

按照前面提到的邏輯，我們從根節點開始，如果目標值比根節點小，就往左小孩走，搜尋左子樹；比根節點大，就往右小孩走，搜尋右子樹。如此不斷重複下去，直到找到目標節點、或者走到葉節點為止。這是一個遞迴的算法，因此可以寫出以下code：
```
def search(root, key):
    # Base Case 1: 根節點為空
    if not root:
        return False
        
    # Base Case 2: 根節點就是要找的值
    elif root.val == key:
        return True
 
    # 要找的值比根節點的值大 -> 往右子樹找
    elif root.val < key:
        return search(root.right,key)
        
    # 要找的值比根節點的值小 -> 往左子樹找
    else:
        return search(root.left,key)
```

### 排序
事實上，對一個BST進行inorder traversal，很自然地就會產生一個由小到大的順序，也就是對這些節點完成了排序。

其他種traversal方式也會生成不同的搜尋樹：

![](https://hackmd.io/_uploads/HJxxuyLZc.png)


### 插入
給定一個值`key`和BST的根節點`root`，要將`key`插入該BST，我們應該怎麼做呢？

在這邊我們使用和搜尋類似的思路，先想像要插入的節點存在BST裡面，然後在BST裡面尋找這個節點。根據BST的規則，我們必須先找到這個要新增的節點**適合的位置**，再將這個節點接上BST。

要尋找對新節點而言的適當位置，需要召喚一位「哨兵」指針`succ`先行探路，而「將會成為新節點的父節點(準新手爸媽)」的那個指針`prev`，則跟著「哨兵」的腳步，往前推進。

假設我們要插入的值是100。一開始我們先把`succ`初始化為`root`，`prev`則是`None`，如下圖：
![](https://hackmd.io/_uploads/SkjLG-IZ5.png)
我們發現，100比10還要大，所以我們往`root`的右小孩前進，也就是說：
```
prev = succ
succ = succ.right
```
![](https://hackmd.io/_uploads/ryuPQ-LWq.png)
現在我們發現，100比1000還要小，所以往左小孩前進：
![](https://hackmd.io/_uploads/rJABEWLZc.png)
100和500比還是比較小，所以我們繼續往左小孩走；但是已經沒有左小孩了，所以這時候的`succ`變成`None`，`prev`則是500的節點。於是我們知道，100必須是500的左小孩，也就是說，走到不能再走的時候，哨兵`succ`的位置就是要插入的位置，準新手爸媽`prev`就是插入的新節點的父節點。最後完成插入的BST如下：
![](https://hackmd.io/_uploads/HJ5RLb8Wq.png)



### 刪除
要從一個BST裡面刪除一個節點，有以下幾種可能：
1. 該節點不存在
    這個情況不考慮，可以直接擲出例外或回傳`None`。
2. 該節點為葉節點，沒有左右子樹
    這個狀況很簡單，直接刪去該節點即可。
3. 該節點有右子樹，無左子樹
    這個也很簡單，只要刪掉該節點，然後把右子樹直接接到刪去的那個節點的父節點上就好了。
4. 該節點有左子樹，無右子樹
    和狀況3類似，刪掉該節點，然後把左子樹直接接到刪去的那個節點的父節點上就好了。
6. 該節點有左右子樹
    這時候就必須找到替代即將被刪除節點的中序接續子（Inorder Successor）節點，接著讓接續子節點取代被刪除的子節點，假如接續子節點本身也有子節點，那要同步找尋適合放置的位置。




## 例題
* 把BST變成circular doubly linked list
    給定一個BST，我們希望把這個BST上的節點按照順序由小到大串成一個circular doubly linked list。注意在這邊我們希望可以**原地完成**這個操作(in-place)。
![](https://hackmd.io/_uploads/r1_zwkUbq.png)

    :::spoiler 解答
    在這邊直覺就會想對這個BST進行inorder traversal，因為要排序。我們已經知道怎麼做inorder traversal了，接下來就是對節點進行適當的操作，把它們串成circular doubly linked list就好了。所以最直接的方式就是使用遞迴。這邊我們可以定義一個helper function，用這個函式來進行遞迴。這個helper function的概念是，把每個子樹都串成一個circular doubly linked list。
    
    ```
    """
    # 節點的定義
    class Node(object):
        def __init__(self, val, left=None, right=None):
            self.val = val
            self.left = left
            self.right = right
    """
    
    class Solution(object):
        def treeToDoublyList(self, root):
            """
            :type root: Node
            :rtype: Node
            """
            if root:
                head, tail = self.helper(root)
                return head
            return None


        def helper(self, root):
            """
            Idea: Construct a DLL for each subtree, then return the head and tail
            """
            
            head, tail = root, root
            
            if root.left:
                lh, lt = self.helper(root.left)
                lt.right = root
                root.left = lt
                head = lh
            if root.right:
                rh, rt = self.helper(root.right)
                rh.left = root
                root.right = rh
                tail = rt
            head.left = tail
            tail.right = head
            return (head, tail)
    ```
    :::

