حذف BST: ثلاث حالات
عالج حذف الورقة وحذف العقدة ذات الابن الواحد وحذف العقدة ذات الابنين باستخدام السلف اللاحق بالترتيب الوسطي، ونفّذ الخوارزمية من الصفر
حذف BST: ثلاث حالات درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 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 من الاستدعاء العودي، مما يجعل العقدة الأب تضبط مؤشرها (الأيسر أو الأيمن) على null. هذه هي الحالة الأساسية التي يجب أن تتعامل معها جميع تطبيقات حذف 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
تسأل مسألة Two Sum 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. تكمن الفكرة الأساسية في تنفيذ اجتياز وسطي عكسي (right → root → left) لزيارة العقد بترتيب تنازلي وتجميع مجموع تراكمي. يعمل ذلك في زمن 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تحقق سريع
اختبر مدى فهمك لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.
مراجعة الدرس
في هذا الدرس تعلّمت: حالات حذف BST الثلاث (ورقة، وابن واحد، وابنان)، وتقنية الخلف في الاجتياز الوسطي لحذف العقدة ذات الابنين، وأنماطًا تكرارية واضحة مثل مكرّر BST وشجرة BST للمجموع الأكبر. في الجزء التالي سنتحقق من صحة BST ونستفيد من خصائص الاجتياز الوسطي.
الأسئلة الشائعة
هل درس «حذف BST: ثلاث حالات» مجاني؟
نعم — نص درس «حذف BST: ثلاث حالات» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «حذف BST: ثلاث حالات»؟
عالج حذف الورقة وحذف العقدة ذات الابن الواحد وحذف العقدة ذات الابنين باستخدام السلف اللاحق بالترتيب الوسطي، ونفّذ الخوارزمية من الصفر تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟
لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.
كم من الوقت يستغرق درس «حذف BST: ثلاث حالات»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟
نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- إدراج BST والبحث فيه
- حذف BST: ثلاث حالات
- التحقق من BST وخصائص الترتيب الوسطي
- العنصر الأصغر رقم k ومجموع النطاق وتحويل BST إلى مصفوفة مرتبة