簡介
相信各位在準備面試的時候,Binary Search Tree 這個資料結構一定會被問到,他是 Tree 大本營中最先學到的一種演算法,在資料的搜尋、插入、刪除上都有很好的表現,在Balanace Tree的狀況下可以達到O(logn)
,所謂的Balanace Tree就像是左邊跟右邊的枝葉數量差不多,不會出現左邊很多右邊很少的情況,這樣才能達到O(logn)
的時間複雜度。
定義
那我們先來說說BST的四個特性:
根(root)的左邊節點(left node)其子樹(sub-tree)中所有的節點值都小於根(root)的值。
根(root)的右邊節點(right node)其子樹(sub-tree)中所有的節點值都大於根(root)的值。
任意節點的左右子樹都符合BST。
不存在任何key/value相等的節點。
小提醒:我把第四點畫起來是因為很多人會不記得第四點,但遇過面試有問,但想一下就會知道如果有相等值的節點就會違反第一點或第二點。
如果我還是想要塞入相同的值怎麼辦? BST的變體 如果硬要塞相同的值,有一些變體可以處理重複的值,這些變體包括
左傾或右傾策略 (Left-Leaning or Right-Leaning Strategy):當插入重複的值時,可以選擇將重複的值插入左子樹或右子樹。例如,所有重複的值都放在右子樹。
計數器方法 (Counter Method): 在每個節點上添加一個計數器來記錄該值的出現次數。這樣,當插入重複的值時,只需要增加該節點的計數器,而不需要插入新的節點。
使用第一種策略(所有重複值都插入右子樹)時,樹的結構如下:
使用第二種策略(計數器方法)時,樹的結構如下,但節點 5 的計數器為 2:
Balance 的重要性
Binary Search Tree (BST) 是基於 Binary Search Algorithm 的概念以 Tree 的資料結構所建立,也可以說他是為了搜尋而生的演算法,因為他的搜尋時間複雜度是O(logn)
,當然這是在平衡樹的情況下,如果是一個不平衡的樹,那搜尋時間複雜度就會變成O(n)
。關鍵就在平衡 。那我們這邊就先來講 樹高 。
以 3 個 node 的 BST 為例,會有三種排列的可能性分別是:
好,那今天如果我們要搜尋 10 這個鍵值:
第一種排列:只需要一次比較(從 root 進入,10 < 20,往左走,找到了)
第二種排列:需要兩次比較(從 root 進入,10 < 30,往左走,10 < 20,繼續往左走,找到了)
所以我們可以看到在一樣數量的 node 的情況之下,不一樣的樹高對搜尋產生的影響是很關鍵的,如果有平衡就可以讓樹高在最理想的情況,使搜尋可以達到 O(log n) 。
如果平衡的話,假有共有 n 個節點,那麼樹高應該就是 ( log n ) + 1。
四種操作
接下來介紹搜尋、新增、改值、刪除四種操作
新增節點
假設我們今天要把值為 15 的節點新增進去,要做以下事情:
先建立 15 的節點
與 root 節點比大小,15 < 20,所以往左走
與節點10 比大小,15 > 10,所以往右走
搜尋節點
假設我們要搜尋 15 這個節點,其實跟新增的步驟差不多 ,先逐一比大小,差在要確認經過的節點的值是不是 15 即可。
把要找的值 15 與 root 節點比大小,15 < 20,所以往左走
把要找的值 15 與節點10 比大小,15 > 10,所以往右走
與節點15比較發現 15 == 15,找到了
修改節點的值
根搜尋也差不多,只是找到節點後,把該節點的值改掉即可。或是直接把舊的節點刪除,插入更新值的節點。
刪除節點
刪除節點比較複雜一點,大概會有以下刪除的狀況:
刪除的節點是葉子節點(沒有子節點):該節點無子節點,直接刪除。
刪除的節點有一個子節點:該節點只有一個子節點,直接刪除,並把子節點接上去。
刪除的節點有兩個子節點:該節點有兩個子節點,找到左子樹中最大的節點或右子樹中最小的節點來替代被刪除的節點。
情況1 - 刪除孤兒:直接把該節點刪除即可
情況2 - 替代被刪除的節點
情況3 - 從左子樹(small nodes)找最大(max)去掌管左子樹,或是從右子樹(big smalls)找最小(min)去掌管右子樹
實作
先定義兩個class,把操作方法定義在 BST 裡面
1 2 3 4 5 6 7 8 9 10 class Node (object ): def __init__ (self, value ): self.value = value self.left = None self.right = None self.parent = None class BST (object ): def __init__ (self, root: Node ): self.root = root
新增節點
以 root 為起點開始比大小,直到發現空位就可插入,這邊是用 loop 的方法來寫,也可以用遞迴的方式來寫。
loop 的方法:
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 class BST (object ): def __init__ (self, root: Node ): self.root = root def add_node (self, node: Node ): current_node = self.root while True : if node.value == current_node.value: raise Exception("Node already exists" ) elif node.value < current_node.value: if current_node.left is not None : current_node = current_node.left else : current_node.left = node node.parent = current_node break elif node.value > current_node.value: if current_node.right is not None : current_node = current_node.right else : current_node.right = node node.parent = current_node break
使用 Recursion 的方法:簡單來說就是把 continue 的部分改成呼叫自己,這樣就可以一直往下走,直到找到空位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 def add_node (self, node: Node ): if not self.root: self.root = node else : self._add_node_recursive(self.root, node) def _add_node_recursive (self, current_node: Node, new_node: Node ): if new_node.value == current_node.value: raise Exception("Node already exists" ) elif new_node.value < current_node.value: if current_node.left is None : current_node.left = new_node new_node.parent = current_node else : self._add_node_recursive(current_node.left, new_node) else : if current_node.right is None : current_node.right = new_node new_node.parent = current_node else : self._add_node_recursive(current_node.right, new_node)
搜尋節點
基本上跟新增一樣,也是去比大小,找到的話就回傳
loop 的方法:
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 class BST (object ): def __init__ (self, root: Node ): self.root = root def get_node_by_value (self, value: int ): current_node = self.root while True : if value == current_node.value: return current_node elif value < current_node.value: if current_node.left is not None : current_node = current_node.left else : raise Exception('node not found' ) elif value > current_node.value: if current_node.right is not None : current_node = current_node.right else : raise Exception('node not found' )
Recursion 的方法程式碼就少了很多,也比較清楚:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class BST (object ): def __init__ (self, root: Node ): self.root = root def get_node_by_value (self, value: int ): return self._get_node_by_value_recursive(self.root, value) def _get_node_by_value_recursive (self, current_node: Node, value: int ): if current_node is None : raise Exception('node not found' ) if value == current_node.value: return current_node elif value < current_node.value: return self._get_node_by_value_recursive(current_node.left, value) else : return self._get_node_by_value_recursive(current_node.right, value)
刪除節點
刪除就很麻煩了,直接使用遞迴會比較好寫:
如果值小於當前節點的值,則遞歸地在左子樹中刪除節點。
如果值大於當前節點的值,則遞歸地在右子樹中刪除節點。
如果找到節點,分三種情況處理 :
該節點無子節點
,直接刪除。
該節點有一個子節點
,讓該子節點接替被刪除節點的位置。
該節點有兩個子節點
:
找到欲刪除節點的右子樹中的最小值節點(或左子樹中的最大值節點)
將其值複製到當前節點
並遞歸地刪除最小值節點。
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 class BST (object ): def __init__ (self, root: Node ): self.root = root def delete_node_by_value (self, value: int ): self.root = self._delete_node_recursive(self.root, value) def _delete_node_recursive (self, current: Node, value: int ): if current_node is None : raise Exception('node not found' ) if value < current_node.value: current_node.left = self._delete_node_recursive(current_node.left, value) elif value > current_node.value: current_node.right = self._delete_node_recursive(current_node.right, value) else : if current_node.left is None : return current_node.right elif current_node.right is None : return current_node.left min_larger_node = self._get_min(current_node.right) current_node.value = min_larger_node.value current_node.right = self._delete_node_recursive(current_node.right, min_larger_node.value) return current_node def _get_min (self, node: Node ): current = node while current.left is not None : current = current.left return current
Ref