การลบใน BST: สามกรณี
จัดการการลบใบไม้ การลบโหนดที่มีลูกหนึ่งโหนด และการลบโหนดที่มีลูกสองโหนดด้วยโหนดสืบทอดตามลำดับกลาง โดยสร้างอัลกอริทึมตั้งแต่ต้น
การลบใน BST: สามกรณี เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
เหตุใดการลบใน BST จึงซับซ้อน
การลบใน BST เป็นการดำเนินการหลักที่ซับซ้อนที่สุดในสามอย่าง เพราะการนำโหนดออกต้องรักษาคุณสมบัติของ BST ไว้ตลอดทั้งต้นไม้ มีสามกรณีที่แตกต่างกันตามโหนดลูกของโหนดนั้น: ไม่มีโหนดลูก (เป็นโหนดใบ) มีโหนดลูกหนึ่งโหนด หรือมีโหนดลูกสองโหนด แต่ละกรณีต้องใช้วิธีการที่แตกต่างกัน ผู้สัมภาษณ์ชื่นชอบปัญหานี้ เพราะใช้ทดสอบการจัดการตัวชี้ การคิดถึงกรณีพิเศษ และความรู้เกี่ยวกับแนวคิดโหนดถัดไปตามลำดับอินออร์เดอร์
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
# Three cases for deleting a node:
# Case 1: Leaf node (no children) -> simply remove it
# Case 2: One child -> replace node with its child
# Case 3: Two children -> replace value with in-order successor
# then delete the in-order successor
print('BST delete: 3 cases based on number of children')กรณีที่ 1: การลบโหนดใบ
โหนดใบไม่มีโหนดลูก การลบจึงทำได้ง่าย: ส่งคืน None จากการเรียกซ้ำ ซึ่งทำให้โหนดแม่ตั้งค่าตัวชี้ของตน (ซ้ายหรือขวา) เป็นค่าว่าง นี่คือกรณีฐานที่การใช้งานการลบ BST ทุกแบบต้องจัดการเป็นอันดับแรก ควรตรวจสอบว่าวิธีนี้ทำงานได้กับกรณีพิเศษที่ต้นไม้มีเพียงโหนดเดียว (รากเป็นโหนดใบ)
def find_min(node):
while node.left:
node = node.left
return node
# Demonstrating leaf deletion:
root = TreeNode(5)
root.left = TreeNode(3)
root.right = TreeNode(7)
root.left.left = TreeNode(1) # leaf
root.left.right = TreeNode(4) # leaf
# To delete node 1 (leaf): set root.left.left = None
root.left.left = None
print(root.left.left) # None -- deleted
print(root.left.val) # 3 still intactกรณีที่ 2: โหนดที่มีโหนดลูกหนึ่งโหนด
เมื่อโหนดมี โหนดลูกเพียงหนึ่งโหนดเท่านั้น ให้แทนที่โหนดนั้นด้วยโหนดลูกดังกล่าว คืนค่าโหนดลูกที่ไม่เป็นค่าว่างจากการเรียกซ้ำ เพื่ออัปเดตตัวชี้ของโหนดแม่ให้ข้ามโหนดที่ถูกลบ การทำงานนี้ใช้ได้อย่างราบรื่นไม่ว่าโหนดลูกเพียงหนึ่งโหนดจะอยู่ทางซ้ายหรือทางขวา เพียงคืนค่าโหนดที่มีอยู่
# Demonstrating one-child deletion:
# Tree: 5
# / \
# 3 7
# \
# 4
# Delete node 3 (has only right child 4):
# Result: 5
# / \
# 4 7
root = TreeNode(5)
root.left = TreeNode(3)
root.right = TreeNode(7)
root.left.right = TreeNode(4)
# In the recursive implementation:
# When we reach node 3 and it has no left child,
# we return root.right (node 4) to the parent.
# Parent sets its left pointer to 4, skipping 3.
print('One-child case: return the surviving child')กรณีที่ 3: โหนดที่มีโหนดลูกสองโหนด
เมื่อโหนดมี โหนดลูกสองโหนด เราไม่สามารถลบโหนดนั้นออกโดยตรงได้ แต่ให้ค้นหา โหนดถัดไปตามลำดับอินออร์เดอร์ ของโหนดนั้น ซึ่งก็คือค่าที่น้อยที่สุดในต้นไม้ย่อยด้านขวา คัดลอกค่าของโหนดดังกล่าวมาใส่ในโหนดปัจจุบัน จากนั้นลบโหนดถัดไปตามลำดับอินออร์เดอร์ออกจากต้นไม้ย่อยด้านขวา โหนดถัดไปนี้มีโหนดลูกได้ไม่เกินหนึ่งโหนด และไม่มีโหนดลูกด้านซ้าย ดังนั้นการลบจึงอยู่ในกรณีที่ 1 หรือกรณีที่ 2 ซึ่งเราได้ทราบวิธีจัดการแล้ว
# Demonstrating two-child deletion:
# Tree: 5
# / \
# 3 7
# / \
# 6 9
# Delete node 5 (two children 3 and 7):
# In-order successor = 6 (smallest in right subtree)
# Step 1: replace 5's value with 6
# Step 2: delete 6 from right subtree
# Result: 6
# / \
# 3 7
# \
# 9
print('Two-child case: replace with in-order successor')การใช้งานการลบ BST ฉบับสมบูรณ์
การลบแบบเรียกซ้ำฉบับเต็มจะรวมทั้งสามกรณีเข้าด้วยกัน ให้ค้นหาโหนดที่จะลบโดยเปรียบเทียบค่า จากนั้นจัดการตามกรณีที่เหมาะสม รูปแบบการคืนค่าโหนดรากที่อาจถูกแก้ไขในแต่ละระดับ แล้วกำหนดค่านั้นกลับให้กับ root.left หรือ root.right ช่วยจัดการการอัปเดตตัวชี้ทั้งหมดได้อย่างสง่างาม โดยไม่ต้องติดตามโหนดแม่อย่างชัดเจน ความซับซ้อนด้านเวลาคือ O(h)
def delete_node(root, key):
if not root:
return None # key not found
if key < root.val:
root.left = delete_node(root.left, key)
elif key > root.val:
root.right = delete_node(root.right, key)
else: # found the node to delete
if not root.left: # Case 1 or 2: no left child
return root.right
if not root.right: # Case 2: no right child
return root.left
# Case 3: two children -> find in-order successor
successor = find_min(root.right)
root.val = successor.val # copy successor value up
root.right = delete_node(root.right, successor.val) # delete successor
return root
root = TreeNode(5)
root.left = TreeNode(3)
root.right = TreeNode(7)
root.right.left = TreeNode(6)
root.right.right = TreeNode(9)
root = delete_node(root, 5)
print(root.val) # 6 (successor replaced 5)เหตุใดจึงใช้โหนดถัดไปตามลำดับอินออร์เดอร์
เราใช้โหนดถัดไปตามลำดับอินออร์เดอร์ ซึ่งเป็นค่าต่ำสุดของต้นไม้ย่อยด้านขวา แทนค่าสูงสุดของต้นไม้ย่อยด้านซ้าย เนื่องจากทั้งสองตัวเลือกถูกต้อง และการใช้ตัวเลือกใดตัวเลือกหนึ่งก็ยังคงคุณสมบัติของ BST ไว้ได้ โหนด ก่อนหน้าตามลำดับอินออร์เดอร์ ซึ่งเป็นค่าสูงสุดของต้นไม้ย่อยด้านซ้าย ก็ใช้ได้เช่นกัน การใช้งานบางรูปแบบสลับใช้ทั้งสองวิธีเพื่อช่วยรักษาสมดุลของต้นไม้ ในการสัมภาษณ์ โดยทั่วไปมักคาดหวังวิธีใช้โหนดถัดไปตามลำดับอินออร์เดอร์มากกว่า ดังนั้นควรกล่าวด้วยว่าวิธีใช้โหนดก่อนหน้าก็ใช้ได้ดีเท่าเทียมกัน
# Both approaches are valid for two-child deletion:
# Option A: Replace with in-order SUCCESSOR (min of right subtree)
# - Successor goes to current position
# - Delete successor from right subtree
# Option B: Replace with in-order PREDECESSOR (max of left subtree)
# - Predecessor goes to current position
# - Delete predecessor from left subtree
def find_max(node):
while node.right:
node = node.right
return node
# Using predecessor:
def delete_node_pred(root, key):
if not root:
return None
if key < root.val:
root.left = delete_node_pred(root.left, key)
elif key > root.val:
root.right = delete_node_pred(root.right, key)
else:
if not root.left:
return root.right
if not root.right:
return root.left
pred = find_max(root.left)
root.val = pred.val
root.left = delete_node_pred(root.left, pred.val)
return root
print('Both successor and predecessor deletion are correct')ลบโหนดทั้งหมดที่มีค่าเดียวกัน
โจทย์รูปแบบหนึ่งอาจขอให้คุณลบโหนดทั้งหมดที่มีค่าอยู่ภายในช่วงหนึ่งหรือเป็นไปตามเงื่อนไขที่กำหนด สำหรับ BST วิธีนี้มีประสิทธิภาพ โดยเรียกซ้ำไปยังต้นไม้ย่อยที่เหมาะสมตามผลการเปรียบเทียบ และดำเนินการลบทุกจุดที่ตรงตามเงื่อนไข โครงสร้างแบบเรียกซ้ำของการลบใน BST สามารถต่อยอดไปยังสถานการณ์เหล่านี้ได้โดยไม่ต้องท่องต้นไม้แยกอีกครั้ง
# Delete all nodes with values outside [low, high]
def trim_bst(root, low, high):
if not root:
return None
if root.val < low:
# Entire left subtree is also < low, skip to right
return trim_bst(root.right, low, high)
if root.val > high:
# Entire right subtree is also > high, skip to left
return trim_bst(root.left, low, high)
# Current node is within range
root.left = trim_bst(root.left, low, high)
root.right = trim_bst(root.right, low, high)
return root
root = TreeNode(3)
root.left = TreeNode(0)
root.right = TreeNode(4)
root.left.right = TreeNode(2)
root.left.right.left = TreeNode(1)
root = trim_bst(root, 1, 3)
print(root.val, root.left.val) # 3 2รูปแบบตัววนซ้ำ BST
ตัววนซ้ำ BST (LeetCode #173) คืนค่าองค์ประกอบทีละรายการตามลำดับที่เรียงแล้ว โดยใช้เวลาเฉลี่ย O(1) และพื้นที่ O(h) ให้ใช้งานด้วยสแตกที่จำลองการท่องต้นไม้แบบอินออร์เดอร์เชิงวนซ้ำ โดยเมื่อสร้างตัววนซ้ำ ให้ใส่โหนดด้านซ้ายทั้งหมดตั้งแต่โหนดรากลงไปในสแตก เมื่อเรียกใช้ next() ให้นำโหนดด้านบนออก แล้วใส่โหนดด้านซ้ายทั้งหมดของต้นไม้ย่อยด้านขวาลงในสแตก วิธีนี้คือการคลี่อัลกอริทึมอินออร์เดอร์เชิงวนซ้ำออกทีละขั้นอย่างมีการควบคุม
class BSTIterator:
def __init__(self, root):
self.stack = []
self._push_left(root)
def _push_left(self, node):
while node:
self.stack.append(node)
node = node.left
def next(self):
node = self.stack.pop()
if node.right:
self._push_left(node.right)
return node.val
def has_next(self):
return bool(self.stack)
root = TreeNode(7)
root.left = TreeNode(3)
root.right = TreeNode(15)
root.right.left = TreeNode(9)
it = BSTIterator(root)
while it.has_next():
print(it.next(), end=' ') # 3 7 9 15การลบโหนด: การวิเคราะห์ความซับซ้อน
การลบใน BST ใช้เวลา O(h) โดย h คือความสูงของต้นไม้ สำหรับ BST ที่สมดุล จะมีความซับซ้อนเป็น O(log n) ส่วนต้นไม้ที่เอนเอียงจะมีประสิทธิภาพลดลงเป็น O(n) การค้นหาโหนดถัดไปตามลำดับอินออร์เดอร์อาจเพิ่มการท่องต้นไม้ย่อยด้านขวาอีกอย่างมาก O(h) หนึ่งครั้ง แต่ไม่ทำให้ความซับซ้อนโดยรวมเปลี่ยนแปลง ความซับซ้อนด้านพื้นที่คือ O(h) สำหรับสแตกการเรียกใช้ในการใช้งานแบบเรียกซ้ำ
# Complexity summary for BST operations:
# Operation | Balanced | Skewed
# ----------|-----------|-------
# Search | O(log n) | O(n)
# Insert | O(log n) | O(n)
# Delete | O(log n) | O(n)
# Min/Max | O(log n) | O(n)
# In-order | O(n) | O(n) (visits all nodes)
# The key: BST guarantees these complexities only when balanced.
# Python standard library has no balanced BST.
# Use sortedcontainers.SortedList for O(log n) ops in practice.
print('All BST core ops are O(h): O(log n) balanced, O(n) skewed')ผลรวมสองค่าใน BST
ผลรวมสองค่า IV ใน BST เป็นโจทย์ที่ถามว่ามีโหนดสองโหนดใดบ้างที่มีผลรวมเท่ากับค่าเป้าหมายหรือไม่ วิธีหนึ่งคือใช้เซต โดยท่องต้นไม้แบบอินออร์เดอร์เพื่อเก็บค่า พร้อมตรวจสอบว่า target - current มีอยู่ในเซตที่เก็บไว้ก่อนหน้านี้หรือไม่ วิธีที่สง่างามกว่าคือใช้ตัววนซ้ำ BST แบบเดินไปข้างหน้าและตัววนซ้ำ BST แบบเดินย้อนกลับพร้อมกัน ซึ่งทำงานคล้ายตัวชี้สองตัว วิธีนี้ไม่ต้องใช้พื้นที่เพิ่มเติมเกิน O(h) สำหรับสแตกของตัววนซ้ำแต่ละตัว
def find_target_bst(root, k):
seen = set()
def inorder(node):
if not node:
return False
if inorder(node.left):
return True
if k - node.val in seen:
return True
seen.add(node.val)
return inorder(node.right)
return inorder(root)
root = TreeNode(5)
root.left = TreeNode(3)
root.right = TreeNode(6)
root.left.left = TreeNode(2)
root.left.right = TreeNode(4)
root.right.right = TreeNode(7)
print(find_target_bst(root, 9)) # True (2+7)
print(find_target_bst(root, 28)) # Falseแปลง BST เป็นต้นไม้ผลรวมค่าที่มากกว่า
ต้นไม้ผลรวมค่าที่มากกว่า (LeetCode #538) จะแทนที่ค่าของแต่ละโหนดด้วยผลรวมของค่าทั้งหมดใน BST ที่ มากกว่าหรือเท่ากับค่านั้น แนวคิดสำคัญคือการท่องต้นไม้แบบ อินออร์เดอร์ย้อนกลับ (ขวา → ราก → ซ้าย) เพื่อเยี่ยมชมโหนดจากค่ามากไปค่าน้อย และสะสมผลรวมต่อเนื่อง วิธีนี้ใช้เวลา O(n) และพื้นที่ O(h)
def bst_to_gst(root):
acc = [0] # running accumulated sum
def reverse_inorder(node):
if not node:
return
reverse_inorder(node.right) # visit larger values first
acc[0] += node.val
node.val = acc[0] # replace with cumulative sum
reverse_inorder(node.left)
reverse_inorder(root)
return root
root = TreeNode(4)
root.left = TreeNode(1)
root.right = TreeNode(6)
root.right.left = TreeNode(5)
root.right.right = TreeNode(7)
bst_to_gst(root)
print(root.val) # 4+5+6+7 = 22
print(root.right.val) # 5+6+7 = 18ตรวจสอบความเข้าใจอย่างรวดเร็ว
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสำหรับการสัมภาษณ์ด้านการเขียนโปรแกรมจากบทเรียนนี้
ทบทวนบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ สามกรณีของการลบ BST ได้แก่ โหนดใบ โหนดที่มีโหนดลูกหนึ่งโหนด และโหนดที่มีโหนดลูกสองโหนด รวมถึง เทคนิคใช้โหนดถัดไปตามลำดับอินออร์เดอร์ สำหรับการลบโหนดที่มีโหนดลูกสองโหนด และรูปแบบการเรียกซ้ำที่กระชับ เช่น ตัววนซ้ำ BST และการแปลง BST เป็นต้นไม้ผลรวมค่าที่มากกว่า บทถัดไป เราจะตรวจสอบความถูกต้องของ BST และใช้ประโยชน์จากคุณสมบัติของอินออร์เดอร์
คำถามที่พบบ่อย
บทเรียน “การลบใน BST: สามกรณี” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การลบใน BST: สามกรณี” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การลบใน BST: สามกรณี”
จัดการการลบใบไม้ การลบโหนดที่มีลูกหนึ่งโหนด และการลบโหนดที่มีลูกสองโหนดด้วยโหนดสืบทอดตามลำดับกลาง โดยสร้างอัลกอริทึมตั้งแต่ต้น คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน
บทเรียน “การลบใน BST: สามกรณี” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม
ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การแทรกและค้นหาใน BST
- การลบใน BST: สามกรณี
- ตรวจสอบ BST และคุณสมบัติลำดับกลาง
- ค่าที่น้อยที่สุดลำดับ k ผลรวมช่วง และ BST เป็นอาร์เรย์เรียงลำดับ