BST — 實戰題解
範圍 — 在
bst.md某個模板上多加一步的 BST 題,每題以一張題目卡寫一次,放在它歸約到的那張卡底下;模板或進階卡已經寫出的題目,這裡只列指標、不重解。 另見:bst.md — 這些題目歸約到的 BST 模板;bst_advanced.md — 延遲式迭代器、修復壞掉的 BST、rank 查詢與各種建構變形;tree_lca_distance.md — 根到葉與任意點到任意點的路徑題(LC 112、113、124、129、257、437),用不到大小順序;binary_tree.md — LC 298 以及其他無序樹的 DFS 寫法。
LeetCode 題目清單
總覽
BST 題組,依各題歸約到的 bst.md 模板分組,順序與那份講義的模板順序相同。模板本身已經解掉的題目, 在索引裡只是一列指向它的家,不會再抄一份。原本放在這裡的路徑和家族在任何二元樹上都成立,所以它們在 tree_lca_distance.md。
索引
| LC | 題目 | 歸約到 | 難度 |
|---|---|---|---|
| 776 | Split BST | bst § Template 3b — 兩邊都留下,而不是丟掉一邊 | Medium |
| 938 | Range Sum of BST | bst § Template 3b — 把範圍內的值加總,而不是把子節點接回去 | Easy |
| 98 | Validate Binary Search Tree — 在 bst § Template 4 寫出,含迭代版 | bst § Template 4 | Medium |
| 449 | Serialize and Deserialize BST | bst § Template 4 — 由上下界決定每個前序值放哪裡 | Medium |
| 538 | Convert BST to Greater Tree | bst § Template 5,反向 | Medium |
| 1038 | Binary Search Tree to Greater Sum Tree — 與 LC 538 同一題、同一份程式碼 | bst § Template 5,反向 | Medium |
| 173 | Binary Search Tree Iterator — 在 bst_advanced § Template 1 寫出 | bst § Template 5,在兩次呼叫之間暫停 | Medium |
修剪到範圍的例題
Split BST — LC 776
關鍵 — 修剪會丟掉根在範圍外的那一側;拆分則把它留下來當第二棵樹。每個節點回傳的兩棵樹裡, 有一棵就是節點自己,另一棵只來自「可能跨過
target的那個子節點」的拆分,所以只走一條路徑。
歸約到:bst § Template 3b —— 同樣是「另一側整個落在切點的同一邊」, 只是什麼都不丟。
# python
# LC 776 - Split BST
# IDEA: root <= target -> root heads the small tree; split its right child and keep the small part;
# root > target -> root heads the big tree; split its left child and keep the big part
# time = O(h), space = O(h)
def splitBST(root, target):
if not root:
return [None, None] # [<= target, > target]
if root.val <= target:
small, big = splitBST(root.right, target)
root.right = small # the part of the right subtree that stays with root
return [root, big]
small, big = splitBST(root.left, target)
root.left = big # the part of the left subtree that stays with root
return [small, root]
// java
// LC 776 - Split BST
// IDEA: split only the child that straddles target, reattach the half that belongs with root
// time = O(h), space = O(h)
public TreeNode[] splitBST(TreeNode root, int target) {
if (root == null) {
return new TreeNode[]{null, null};
}
if (root.val <= target) {
TreeNode[] split = splitBST(root.right, target);
root.right = split[0];
return new TreeNode[]{root, split[1]};
}
TreeNode[] split = splitBST(root.left, target);
root.left = split[1];
return new TreeNode[]{split[0], root};
}
[4,2,6,1,3,5,7], target = 2
at 4: 4 > 2 -> split 4.left (2); 4 keeps the big half of it
at 2: 2 <= 2 -> split 2.right (3); 2 keeps the small half of it
at 3: 3 > 2 -> split 3.left (null) -> [null, null]; return [null, 3]
back at 2: 2.right = null -> return [2, 3]
back at 4: 4.left = 3 -> return [2, 4]
small tree [2,1] big tree [4,3,6,null,null,5,7]
陷阱
- 接錯一半:
root.val <= target時,root.right要接split[0](小的那半),不是split[1]。 接反的話,上例中 3 會跑到 2 底下。 - 兩個子節點都拆是 O(n),結果仍然正確;但只有一個子節點可能跨過切點。
| 操作 | LC | 移除的節點 | 回傳 |
|---|---|---|---|
| Delete | 450 | 一個 | 一個 root |
| Trim | 669 | 所有在 [low, high] 之外的節點 |
一個 root |
| Split | 776 | 沒有 | 兩個 root |
Range Sum of BST — LC 938
關鍵 — 跟
trimBST一樣的三路剪枝:範圍外的 root 會排除整整一邊, 所以只有合併那一步改變,從把子節點接回去,變成把值加起來。
歸約到:bst § Template 3b — 求和,而不是重組結構。
# python
# IDEA: Template 3b's pruning; add the in-range values instead of reattaching them
# time = O(n), space = O(h)
# LC 938 - Range Sum of BST
def rangeSumBST(root, low, high):
if not root:
return 0
if root.val < low: # nothing on the left can be in range
return rangeSumBST(root.right, low, high)
if root.val > high: # nothing on the right can be in range
return rangeSumBST(root.left, low, high)
return (root.val
+ rangeSumBST(root.left, low, high)
+ rangeSumBST(root.right, low, high))
用上下界驗證的例題
Serialize and Deserialize BST — LC 449
關鍵 — BST 光靠前序就能決定整棵樹:下一個值落在目前子樹的
(low, high)區間內, 它才屬於這棵子樹。所以編碼不需要 null 標記,這正是它比 LC 297 的編碼更精簡的原因。
歸約到:bst § Template 4 —— 同一個往下帶的區間, 拿來決定值放哪裡,而不是拿來拒絕。
# python
# LC 449 - Serialize and Deserialize BST
# IDEA: serialize = preorder values only; deserialize = rebuild with (low, high) bounds:
# the next value starts this subtree iff it fits the interval, else this subtree is empty
# time = O(n), space = O(n)
from collections import deque
class Codec:
def serialize(self, root):
vals = []
def preorder(node):
if not node:
return
vals.append(str(node.val))
preorder(node.left)
preorder(node.right)
preorder(root)
return ",".join(vals)
def deserialize(self, data):
vals = deque(int(x) for x in data.split(",")) if data else deque()
def build(low, high):
if not vals or not (low < vals[0] < high):
return None # the next value belongs to an ancestor
node = TreeNode(vals.popleft())
node.left = build(low, node.val)
node.right = build(node.val, high)
return node
return build(float("-inf"), float("inf"))
陷阱
"".split(",")是[""],而int("")會丟例外:空樹要單獨擋掉。- 嚴格的
<假設值都不同,BST 保證這點。若有重複值,光靠前序就無法決定形狀。 - LC 297 那種帶
#標記的層序編碼在這裡也會過,但它沒用到 BST,寫出的符號大約多一倍。 要能說出為什麼有了上下界就不需要標記。
相似題 — LC 1008(只做反序列化那一半,bst_advanced § Template 4) · LC 297(任意二元樹:一定要標記)
中序走訪的例題
Convert BST to Greater Tree — LC 538
關鍵 — 右 → 節點 → 左依遞減走訪各個值,所以一個累加和就能把每個節點換成 所有大於等於它的值的總和。LC 1038 是同一題。
歸約到:bst § Template 5,反過來走(Reverse Inorder for Descending)。
# python
# IDEA: reverse in-order (right, node, left) with a running sum of the values seen so far
# time = O(n), space = O(h)
# LC 538 - Convert BST to Greater Tree
class Solution(object):
def convertBST(self, root):
self.running = 0
def reverse_inorder(node):
if not node:
return
reverse_inorder(node.right) # larger values first
self.running += node.val
node.val = self.running
reverse_inorder(node.left)
reverse_inorder(root)
return root