การแทรกและค้นหาใน BST
สร้างการแทรกและค้นหาทั้งแบบเรียกซ้ำและแบบวนซ้ำ ติดตามเส้นทางผ่านต้นไม้สำหรับคีย์ต่าง ๆ และวิเคราะห์ความซับซ้อนกรณีเลวร้ายสุดของต้นไม้ที่ไม่สมดุล
การแทรกและค้นหาใน BST เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
นิยามคุณสมบัติของ BST
ต้นไม้ค้นหาแบบทวิภาคมีเงื่อนไขคงที่หนึ่งข้อ: สำหรับทุกโหนด ค่าทั้งหมดในต้นไม้ย่อยด้านซ้ายต้องน้อยกว่าค่าของโหนดนั้นอย่างเคร่งครัด และค่าทั้งหมดในต้นไม้ย่อยด้านขวาต้องมากกว่าอย่างเคร่งครัด คุณสมบัติด้านลำดับนี้ต้องคงอยู่ตลอดทั้งต้นไม้ย่อย ไม่ใช่เฉพาะโหนดลูกโดยตรง จึงทำให้การค้นหา การแทรก และการลบในต้นไม้ที่สมดุลใช้เวลา O(log n) และทำให้ BST แตกต่างจากต้นไม้ทวิภาคทั่วไป
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
# Valid BST:
# 4
# / \
# 2 6
# / \ / \
# 1 3 5 7
# For node 4: left subtree {1,2,3} < 4 < right subtree {5,6,7}
# This holds recursively for EVERY node in the tree.
print('BST property: left < node < right at every level')การค้นหาใน BST แบบเรียกซ้ำ
การค้นหาใน BST ทำงานคล้ายการค้นหาแบบทวิภาค โดยเปรียบเทียบเป้าหมายกับค่าของโหนดปัจจุบัน แล้วเรียกซ้ำไปยังต้นไม้ย่อยที่เหมาะสม หากเป้าหมายเท่ากับค่าปัจจุบัน ให้ส่งคืนโหนดนั้น หากเป้าหมายมีค่าน้อยกว่า ให้ไปทางซ้าย หากมีค่ามากกว่า ให้ไปทางขวา หากไปถึงโหนดว่าง ให้ส่งคืนค่าว่าง ความซับซ้อนด้านเวลาคือ O(h) — O(log n) สำหรับต้นไม้ที่สมดุล และ O(n) สำหรับต้นไม้ที่เอนเอียง
def search_bst(root, val):
if not root:
return None # not found
if root.val == val:
return root # found
if val < root.val:
return search_bst(root.left, val)
else:
return search_bst(root.right, val)
root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(7)
root.left.left = TreeNode(1)
root.left.right = TreeNode(3)
result = search_bst(root, 2)
print(result.val if result else 'Not found') # 2
result = search_bst(root, 5)
print(result.val if result else 'Not found') # Not foundการค้นหาใน BST แบบวนซ้ำ
การค้นหาแบบวนซ้ำหลีกเลี่ยงค่าใช้จ่ายของสแตกการเรียกฟังก์ชัน และเป็นรูปแบบที่เหมาะกับโค้ดที่ใช้งานจริงมากกว่า ใช้ตัวชี้ curr เดินลงไปตามต้นไม้ โดยเลือกทางซ้ายหรือขวาตามผลการเปรียบเทียบ วิธีนี้เป็นวงวนอย่างง่ายที่มีสามกรณี: ค่าว่าง (ไม่พบ) ค่าตรงกัน (พบ) หรือปรับทิศทาง การค้นหาแบบวนซ้ำยังใช้เวลา O(h) แต่ใช้พื้นที่ O(1) เมื่อเทียบกับ O(h) ของแบบเรียกซ้ำ
def search_bst_iterative(root, val):
curr = root
while curr:
if val == curr.val:
return curr
elif val < curr.val:
curr = curr.left
else:
curr = curr.right
return None # not found
root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(7)
root.left.left = TreeNode(1)
root.left.right = TreeNode(3)
node = search_bst_iterative(root, 3)
print(node.val if node else 'Not found') # 3
print(search_bst_iterative(root, 9)) # Noneการแทรกใน BST แบบเรียกซ้ำ
การแทรกใน BST จะหาตำแหน่งที่ถูกต้องโดยตัดสินใจไปทางซ้ายหรือขวาแบบเดียวกับการค้นหา จากนั้นเชื่อมโหนดใหม่เข้ากับตำแหน่ง null แรกที่พบ วิธีแบบเรียกซ้ำจะส่งคืนรากของต้นไม้ย่อยแต่ละต้น (ซึ่งอาจเป็นรากใหม่): หากโหนดปัจจุบันเป็นค่าว่าง ให้ส่งคืน TreeNode ใหม่ มิฉะนั้นให้ปรับปรุง root.left หรือ root.right ด้วยผลลัพธ์จากการเรียกซ้ำ รูปแบบนี้เข้าใจง่ายและพบได้บ่อยในคำตอบการสัมภาษณ์
def insert_bst(root, val):
if not root:
return TreeNode(val) # create new node here
if val < root.val:
root.left = insert_bst(root.left, val)
elif val > root.val:
root.right = insert_bst(root.right, val)
# val == root.val: duplicate, do nothing (or handle as needed)
return root
root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(7)
root = insert_bst(root, 1)
root = insert_bst(root, 5)
# Tree is now: 4, left=2(left=1), right=7(left=5)
print(root.right.left.val) # 5การแทรกใน BST แบบวนซ้ำ
การแทรกแบบวนซ้ำใช้ตัวชี้ parent เพื่อติดตามโหนดที่ไม่ใช่ค่าว่างล่าสุดก่อนถึงตำแหน่งที่จะแทรก ให้เดินลงไปในต้นไม้เหมือนการค้นหา พร้อมติดตามโหนดแม่และทิศทางล่าสุดที่เดินไป เมื่อถึงค่าว่าง ให้เชื่อมโหนดใหม่เข้ากับด้านที่เหมาะสมของโหนดแม่ ควรจัดการกรณีพิเศษของต้นไม้ที่ว่างเปล่า (รากเป็นค่าว่าง) แยกต่างหากเสมอ
def insert_bst_iterative(root, val):
new_node = TreeNode(val)
if not root:
return new_node
curr = root
while True:
if val < curr.val:
if curr.left is None:
curr.left = new_node
break
curr = curr.left
else: # val > curr.val
if curr.right is None:
curr.right = new_node
break
curr = curr.right
return root
root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(7)
root = insert_bst_iterative(root, 3)
print(root.left.right.val) # 3กรณีเลวร้ายที่สุดของ BST: ต้นไม้เอนเอียง
หากแทรกลำดับที่เรียงแล้วลงใน BST จะได้ต้นไม้เอนเอียงที่เสื่อมสภาพเป็นเหมือนรายการเชื่อมโยง การค้นหา การแทรก และการลบทั้งหมดจะใช้เวลา O(n) นี่คือเหตุผลที่มี BST ที่สมดุล เช่น ต้นไม้ AVL และต้นไม้แดง-ดำ ในการสัมภาษณ์ ควรกล่าวถึงกรณีเลวร้ายที่สุดนี้เสมอเมื่อถูกถามเกี่ยวกับความซับซ้อนของ BST — การตอบว่า 'โดยเฉลี่ย O(log n) และกรณีเลวร้ายที่สุด O(n) สำหรับต้นไม้ที่ไม่สมดุล' แสดงให้เห็นถึงความเข้าใจอย่างลึกซึ้ง
# Inserting 1, 2, 3, 4, 5 into a BST:
# 1
# \
# 2
# \
# 3
# \
# 4
# \
# 5
# This is a right-skewed tree: search is O(n) not O(log n)
root = None
for val in [1, 2, 3, 4, 5]:
root = insert_bst(root, val)
# Verify the skew
node = root
depth = 0
while node:
depth += 1
node = node.right
print(f'Height: {depth}') # 5 = O(n), not O(log n)การค้นหาค่าต่ำสุดและค่าสูงสุด
ใน BST ค่าต่ำสุดจะอยู่ที่โหนดซ้ายสุดเสมอ (ไปทางซ้ายต่อไปจนพบค่าว่าง) และค่าสูงสุดจะอยู่ที่โหนดขวาสุด การดำเนินการเหล่านี้ใช้เวลา O(h) และมักใช้เป็นฟังก์ชันย่อยในการลบจาก BST (เพื่อค้นหาโหนดถัดไปตามลำดับอินออร์เดอร์) และการค้นหาช่วง การจำฟังก์ชันผู้ช่วยเหล่านี้ให้ขึ้นใจจะช่วยประหยัดเวลาในการสัมภาษณ์
def find_min(root):
while root.left:
root = root.left
return root
def find_max(root):
while root.right:
root = root.right
return root
root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(7)
root.left.left = TreeNode(1)
root.left.right = TreeNode(3)
root.right.right = TreeNode(9)
print(find_min(root).val) # 1
print(find_max(root).val) # 9โหนดถัดไปและโหนดก่อนหน้าตามลำดับอินออร์เดอร์
โหนดถัดไปตามลำดับอินออร์เดอร์ของโหนดหนึ่งคือโหนดที่มีค่าน้อยที่สุดซึ่งมากกว่าค่าของโหนดนั้น หากโหนดนั้นมีต้นไม้ย่อยด้านขวา โหนดถัดไปคือ find_min(node.right) หากไม่มีต้นไม้ย่อยด้านขวา โหนดถัดไปคือบรรพบุรุษที่อยู่ต่ำที่สุดซึ่งโหนดที่กำหนดอยู่ในต้นไม้ย่อยด้านซ้าย การทำความเข้าใจเรื่องนี้มีความสำคัญอย่างยิ่งต่อปัญหาการลบใน BST และตัววนซ้ำของ BST
def inorder_successor(root, p):
successor = None
while root:
if p.val < root.val:
successor = root # possible successor
root = root.left
else:
root = root.right
return successor
def inorder_predecessor(root, p):
predecessor = None
while root:
if p.val > root.val:
predecessor = root # possible predecessor
root = root.right
else:
root = root.left
return predecessor
root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(6)
root.left.left = TreeNode(1)
root.left.right = TreeNode(3)
p = root.left # node with val=2
print(inorder_successor(root, p).val) # 3
print(inorder_predecessor(root, p).val) # 1การวิเคราะห์ความซับซ้อนของการค้นหาใน BST
ประสิทธิภาพของ BST ขึ้นอยู่กับความสูงของต้นไม้โดยสิ้นเชิง สำหรับ BST ที่สมดุลซึ่งมี n โหนด ความสูงคือ O(log n) ทำให้การค้นหา การแทรก และการลบใช้เวลา O(log n) สำหรับ BST ที่เอนเอียง ความสูงคือ O(n) ทำให้การดำเนินการทั้งหมดใช้เวลา O(n) Python ไม่มี BST ที่สมดุลมาให้ในตัว (ต่างจาก TreeMap ของ Java) ดังนั้นคุณอาจเขียน AVL หรือต้นไม้แดง-ดำด้วยตัวเอง ใช้ sortedcontainers.SortedList หรือใช้ฮีปสำหรับกรณีใช้งานคิวลำดับความสำคัญ
# Python's BST alternatives:
# 1. heapq - min/max heap, O(log n) push/pop
# 2. sortedcontainers.SortedList (third-party, often allowed)
# 3. Manual AVL or Red-Black (rarely required in interviews)
# When interviews say 'use a BST':
# - LeetCode: implement TreeNode-based solution
# - Real interview: mention sortedcontainers or Java TreeMap equivalent
# - O(log n) operations matter when you need ordered access
# For pure insert/lookup without ordering: use dict (O(1) average)
print('Use heap for priority, dict for lookup, BST for ordered range')การแทรกลงใน BST: กรณีพิเศษ
ควรตรวจสอบเสมอว่าการแทรกของคุณจัดการกับกรณีต่อไปนี้ได้: ต้นไม้ที่ว่างเปล่า (ส่งคืนโหนดใหม่เป็นราก) ค่าซ้ำ (กำหนดว่าจะละเว้น แทรกทางซ้าย หรือแทรกทางขวา และใช้แนวทางนั้นอย่างสม่ำเสมอ) และค่าที่ใหญ่มากหรือเล็กมาก ในการสัมภาษณ์ ให้ระบุสมมติฐานเกี่ยวกับค่าซ้ำก่อนเขียนโค้ด ข้อตกลงที่พบบ่อยที่สุดในปัญหาของ LeetCode คือค่าทั้งหมดแตกต่างกัน เว้นแต่จะระบุไว้เป็นอย่างอื่น
def insert_bst_no_duplicates(root, val):
if not root:
return TreeNode(val)
if val < root.val:
root.left = insert_bst_no_duplicates(root.left, val)
elif val > root.val:
root.right = insert_bst_no_duplicates(root.right, val)
# else: val == root.val -> duplicate, skip
return root
# Test all edge cases:
root = None
root = insert_bst_no_duplicates(root, 5) # empty tree
root = insert_bst_no_duplicates(root, 5) # duplicate
root = insert_bst_no_duplicates(root, 3)
root = insert_bst_no_duplicates(root, 7)
print(root.val, root.left.val, root.right.val) # 5 3 7BST จากอาร์เรย์ที่เรียงแล้ว
การสร้าง BST ที่สมดุลตามความสูงจากอาร์เรย์ที่เรียงแล้ว (LeetCode #108) ใช้แนวคิดแบ่งแล้วพิชิต: สมาชิกตรงกลางจะกลายเป็นราก ครึ่งซ้ายจะกลายเป็นต้นไม้ย่อยด้านซ้าย และครึ่งขวาจะกลายเป็นต้นไม้ย่อยด้านขวา วิธีนี้รับประกันต้นไม้ที่สมดุลและมีความสูง O(log n) ความซับซ้อนด้านเวลาคือ O(n) เนื่องจากประมวลผลสมาชิกแต่ละตัวเพียงครั้งเดียว
def sorted_array_to_bst(nums):
if not nums:
return None
mid = len(nums) // 2
root = TreeNode(nums[mid])
root.left = sorted_array_to_bst(nums[:mid])
root.right = sorted_array_to_bst(nums[mid+1:])
return root
nums = [-10, -3, 0, 5, 9]
root = sorted_array_to_bst(nums)
print(root.val) # 0 (middle element)
print(root.left.val) # -3
print(root.right.val) # 9ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
ทบทวนบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้: คุณสมบัติของ BST (ต้นไม้ย่อยด้านซ้ายมีค่าน้อยกว่าอย่างเคร่งครัด ต้นไม้ย่อยด้านขวามีค่ามากกว่าอย่างเคร่งครัด) การค้นหาและการแทรกทั้งแบบเรียกซ้ำและแบบวนซ้ำในเวลา O(h) และต้นไม้เอนเอียงในกรณีเลวร้ายที่สุดที่ความสูงเท่ากับ n บทถัดไปเราจะจัดการกับการลบใน 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 ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน
บทเรียน “การแทรกและค้นหาใน BST” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม
ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การแทรกและค้นหาใน BST
- การลบใน BST: สามกรณี
- ตรวจสอบ BST และคุณสมบัติลำดับกลาง
- ค่าที่น้อยที่สุดลำดับ k ผลรวมช่วง และ BST เป็นอาร์เรย์เรียงลำดับ