On this page
ข้อ 42 · LC450 Delete Node in a BST (ลบโหนดใน BST) 🟡
delete node แล้วยังคงกฎ BST แยกจัดการ 3 กรณี ไม่มี child มี child ตัวเดียว และมี child สองตัว
โจทย์ (LC450): กำหนด root ของ binary search tree (BST) และค่า key ให้ delete node ที่มีค่าเท่ากับ key ออกจาก BST โดยต้องคง property ของ BST ไว้ (left < node < right) แล้ว return root reference ของต้นไม้หลัง delete ถ้าไม่พบ key ใน tree ให้ return root ของ tree เดิม
- Input:
- root = [5,3,6,2,4,null,7], key = 3
- Output:
- [5,4,6,2,null,null,7]
- Explanation:
- node 3 มี child สองตัว จึงเอา successor (ค่าน้อยสุดฝั่งขวาของ 3 คือ 4) ขึ้นมาแทน ได้ tree ที่ยังเป็น BST ถูกต้อง (คำตอบอื่นที่ยังตรงกฎ BST ก็ยอมรับได้)
- Input:
- root = [5,3,6,2,4,null,7], key = 0
- Output:
- [5,3,6,2,4,null,7]
- Explanation:
- ไม่มีค่า 0 อยู่ใน tree เลย จึง return tree เดิมโดยไม่เปลี่ยนแปลง
- จำนวน node อยู่ระหว่าง 0 ถึง 10^4
- -10^5 <= Node.val <= 10^5
- ค่าใน node ไม่ซ้ำกัน
- root เป็น BST จริง
- -10^5 <= key <= 10^5
การ delete มี 3 กรณีที่ต้องคิดให้ครบ ไม่มี child (ลูก) เลย มี child ตัวเดียว และมี child สองตัว แต่ละกรณีจัดการต่างกัน
แนวทาง — ต้องใช้อะไร & คิดยังไง
ใช้ BST + recursion (การเรียกตัวเอง) traverse (เดินไล่) หา node ที่จะ delete แล้วต่อ subtree (ต้นไม้ย่อย) กลับด้วยการเขียน root.left = delete(root.left, key) เทคนิคนี้ทำให้การเชื่อม node ใหม่หลัง delete เกิดขึ้นเองอัตโนมัติ ไม่ต้องเก็บ pointer (ตัวชี้) ของ parent node (node พ่อ) ไว้เอง
หัวใจอยู่ที่กรณี node มี child สองตัว ถ้า delete ตรง ๆ จะเหลือ child กำพร้าสองก้อนซ่อม tree ไม่ได้ เราจึงไม่ delete node นั้นจริง แต่หา successor คือค่าที่น้อยที่สุดในฝั่งขวา (เดินขวาหนึ่งก้าวแล้วเดินซ้ายจนสุด) เอาค่ามันมาแทน แล้วไป delete successor ตัวเดิมออกจากฝั่งขวาแทน ซึ่งจะกลายเป็นกรณีง่ายเพราะ successor ไม่มี child ซ้ายแน่นอน
- ถ้า tree ว่าง (root เป็น None) ไม่มีอะไรให้ delete คืน None
- ถ้า key น้อยกว่าค่า root เข้าไป delete ในฝั่งซ้าย แล้วรับผลกลับมาต่อกับ root.left
- ถ้า key มากกว่าค่า root เข้าไป delete ในฝั่งขวา แล้วต่อกับ root.right
- ถ้า key เท่ากับค่า root คือเจอ node ที่จะ delete แยก 3 กรณี
- ไม่มี child ซ้าย คืน child ขวาขึ้นไปแทน (ถ้าไม่มี child เลย child ขวาก็เป็น None พอดี เท่ากับ delete ทิ้ง)
- ไม่มี child ขวา คืน child ซ้ายขึ้นไปแทน
- มี child สองตัว หา successor ในฝั่งขวา เอาค่ามาแทนค่า root แล้ว delete successor ออกจากฝั่งขวาต่อ
- คืน root กลับขึ้นไปให้ชั้นบนต่อ node
ลืม update (อัปเดต) การเชื่อม node กลับ (ลืม root.left = ... หรือ root.right = ...) ทำให้ tree ไม่เปลี่ยน อีกจุดคือกรณีสอง child แล้วลืม delete successor ตัวเดิมออก ทำให้เกิดค่าซ้ำใน tree
ไล่ทีละสเต็ป
delete key = 3 จาก root = [5,3,6,2,4,null,7] (node 3 มี child สองตัวคือ 2 กับ 4)
| ขั้น | node ปัจจุบัน | ทำอะไร |
|---|---|---|
| 1 | 5 | 3 < 5 ลงไป delete ในฝั่งซ้าย |
| 2 | 3 | เจอ key มี child สองตัว หา successor ในฝั่งขวา |
| 3 | 4 | เดินขวาจาก 3 ได้ 4 ไม่มี child ซ้าย → successor = 4 |
| 4 | 3 | เอาค่า 4 มาแทน (node กลายเป็น 4) แล้ว delete 4 เดิมออกจากฝั่งขวา |
| 5 | 4 (ตัวเดิม) | ไม่มี child คืน None เท่ากับ delete ทิ้ง เชื่อมกลับเสร็จ |
▶ เฉลยละเอียด (ลองเองก่อนนะ)
# LeetCode ให้ class นี้มาให้แล้ว ที่เขียนไว้ตรงนี้เพื่อให้บล็อกนี้รันได้เองทั้งก้อน
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
# class TreeNode:
# def __init__(self, val=0, left=None, right=None): ...
def delete_node(root, key):
if not root:
return None # ต้นไม้ว่าง ไม่มีอะไรให้ลบ
if key < root.val:
root.left = delete_node(root.left, key) # key อยู่ฝั่งซ้าย
elif key > root.val:
root.right = delete_node(root.right, key) # key อยู่ฝั่งขวา
else:
# เจอ node ที่จะลบแล้ว แยก 3 กรณี
if not root.left:
return root.right # ไม่มีลูกซ้าย ยกลูกขวาขึ้นมาแทน
if not root.right:
return root.left # ไม่มีลูกขวา ยกลูกซ้ายขึ้นมาแทน
# กรณีมีลูกสองตัว หาตัวที่น้อยสุดในฝั่งขวา (successor)
succ = root.right
while succ.left:
succ = succ.left
root.val = succ.val # เอาค่า successor มาแทนค่าปัจจุบัน
# แล้วลบ successor ออกจาก subtree ขวา
root.right = delete_node(root.right, succ.val)
return root
def inorder(node):
"""ไล่อ่าน BST แบบ in-order จะได้ค่าเรียงจากน้อยไปมากเสมอ"""
if node is None:
return []
return inorder(node.left) + [node.val] + inorder(node.right)
# BST [5, 3, 6, 2, 4, null, 7]
root = TreeNode(5, TreeNode(3, TreeNode(2), TreeNode(4)), TreeNode(6, None, TreeNode(7)))
print(inorder(root))
root = delete_node(root, 3) # ลบ 3 ซึ่งมีลูกสองข้าง
print(inorder(root))[2, 3, 4, 5, 6, 7]
[2, 4, 5, 6, 7]ขั้นแรกเราต้อง traverse ไปหา node ที่จะ delete ก่อน โดยใช้กฎ BST เหมือน search ถ้า key น้อยกว่า root ปัจจุบันก็เข้าไป delete ในฝั่งซ้าย (แล้วรับผลลัพธ์กลับมาต่อกับ root.left) ถ้ามากกว่าก็ทำกับฝั่งขวา เทคนิคการเขียน root.left = delete_node(root.left, key) ช่วยให้การเชื่อม node ใหม่หลัง delete เกิดขึ้นเองอัตโนมัติ ไม่ต้องเก็บ pointer ตัวพ่อไว้เอง
พอเจอ node ที่จะ delete (key == root.val) แยกเป็น 3 กรณี กรณีไม่มี child หรือมี child ตัวเดียว จัดการง่าย แค่คืน child อีกฝั่งขึ้นไปแทนตำแหน่งของมัน (ถ้าไม่มี child เลย ฝั่งที่คืนก็เป็น None พอดี เท่ากับ delete ทิ้ง) กรณียากคือมี child สองตัว เรา delete ตรง ๆ ไม่ได้เพราะจะเหลือ child กำพร้าสองก้อน วิธีแก้คือหา successor คือค่าที่น้อยที่สุดในฝั่งขวา (เดินขวาหนึ่งก้าวแล้วเดินซ้ายจนสุด) ค่านี้มากกว่าทุกตัวในฝั่งซ้าย และน้อยกว่าทุกตัวที่เหลือในฝั่งขวา จึงเอามาแทนที่แล้ว tree ยังเป็น BST อยู่ จากนั้น delete successor ตัวเดิมออก (ซึ่งจะเข้ากรณีง่ายเพราะ successor ไม่มี child ซ้ายแน่นอน)
Time O(h) traverse หา node แล้วเดินหา successor รวมแล้วไม่เกิน height (ความสูง) · Space O(h) จาก call stack ของ recursion เท่า height
การ delete ใน BST = traverse หา node ด้วยกฎ BST แล้วต่อ subtree กลับด้วย root.child = recurse(...) เทคนิคแทนที่ด้วย successor (ค่าน้อยสุดฝั่งขวา) เป็นลูกเล่นที่ใช้ได้ทุกครั้งที่ต้อง delete node ที่มี child สองตัว