Validate Binary Search Tree
تُخزَّن الشجرة الثنائية في المصفوفة tree بترتيب الكومة: الجذر عند الفهرس 0، وأبناء الفهرس i عند 2*i+1 (الأيسر) و2*i+2 (الأيمن)، وتشير -1 إلى موضع فارغ، وقد تنتهي المصفوفة بإدخالات -1 إضافية.
اكتب isValidBST، التي تُرجع true إذا كانت الشجرة شجرة بحث ثنائية، وfalse خلاف ذلك. في شجرة البحث الثنائية، تكون قيمة كل عقدة أكبر تمامًا من كل قيمة في شجرتها الفرعية اليسرى، وأصغر تمامًا من كل قيمة في شجرتها الفرعية اليمنى.
الدالة
- treeinteger-array
- الشجرة الثنائية بترتيب المستويات، مع استخدام -1 للدلالة على موضع فارغ
- تُرجعboolean
- صحيح إذا كانت الشجرة شجرة بحث ثنائية، وخطأ خلاف ذلك
القيود
1 ≤ tree.length ≤ 32767- كل
tree[i]إما-1أو قيمة تحقق0 ≤ tree[i] ≤ 105. tree[0]لا تساوي أبدًا-1، لذا تحتوي الشجرة على عقدة واحدة على الأقل.- قد تنتهي المصفوفة بإدخالات
-1إضافية بعد العقدة الأخيرة. - كلا الابنين في الموضع الفارغ فارغان أيضًا، والعمق لا يتجاوز
14. - قد تتكرر القيم.
أمثلة
- المدخلات
- tree = [8, 3, 12, 1, 6, 10, 15]
- المخرجات
- true
- الشرح
- عند القراءة بالترتيب (الشجرة الفرعية اليسرى، العقدة، الشجرة الفرعية اليمنى)، تكون القيم
1, 3, 6, 8, 10, 12, 15. وهي تزداد بصرامة، لذا تقع كل عقدة على الجانب الصحيح من كل سلف.
- المدخلات
- tree = [10, 5, 15, -1, -1, 6, 20]
- المخرجات
- false
- الشرح
- كل عقدة أكبر من ابنها الأيسر وأصغر من ابنها الأيمن، ومع ذلك فإن
6عند الفهرس5تقع في الشجرة الفرعية اليمنى للجذر، لذا يجب أن تكون أكبر من10.
- المدخلات
- tree = [12, 7, 12]
- المخرجات
- false
- الشرح
- يحمل الابن الأيمن للجذر القيمة
12، وهي مساوية لقيمة الجذر. يجب أن تكون الشجرة الفرعية اليمنى أكبر strictly، لذا تفشل الشجرة.
سؤال إضافي
يقع والد العقدة عند الفهرس (i-1)/2، بعد التقريب إلى أسفل. هل يمكنك اجتياز الشجرة بالترتيب باستخدام مساحة إضافية O(1)، والتنقل عبر الآباء بدلًا من استخدام مكدس أو الاستدعاء الذاتي؟
تلميحات
افتحها واحدًا تلو الآخر. كل تلميح يكشف أكثر قليلًا.
في
[10, 5, 15, -1, -1, 6, 20]، كل عقدة أكبر من ابنها الأيسر وأصغر من ابنها الأيمن. فلماذا لا تزال هذه شجرة بحث؟يضع كل سلف حدًا للعقدة: يكون الحد أدنى منها إذا كانت العقدة على يساره، وأعلى منها إذا كانت على يمينه. تشكّل هذه الحدود معًا نطاقًا مفتوحًا. يؤدي الانتقال يسارًا من قيمة
vإلى خفض الحد الأعلى إلىv؛ ويؤدي الانتقال يمينًا إلى رفع الحد الأدنى إلىv.احتفِظ بمكدس من
(index, low, high)، بدءًا من الجذر وبنطاق أوسع من كل قيمة مسموح بها. أخرج عنصرًا من المكدس، وأخفِق إذا لم تكن القيمة واقعة ضمن النطاق بصرامة، وادفع كل ابن حقيقي مع نطاقه المُضيَّق.
الحل
تنطبق القاعدة على الأشجار الفرعية كاملةً، لذا فإن مقارنة كل عقدة بطفليها لا تكفي: فقد تخالف عقدة في عمق الشجرة حدًا وضعه أحد أسلافها قبل عدة مستويات. إما أن تقرأ الشجرة بالترتيب وتتحقق من أن القيم تزداد باستمرار، أو تمرر إلى كل عقدة النطاق الذي يسمح به أسلافها.
قارن كل عقدة مع أشجارها الفرعية كاملةً
الفكرة
تكون كل قيمة في الجانب الأيسر من العقدة أقل من قيمتها v بالضبط عندما تكون أكبر قيمة هناك أقل من v، وتكون كل قيمة في جانبها الأيمن أكبر من v بالضبط عندما تكون أصغر قيمة هناك أكبر من v. وتجد دالتان مساعدتان تكراريتان هاتين القيمتين. يعيد الجانب الفارغ -1 أو 100001، وهما خارج نطاق القيم، لذلك لا يفشل أبدًا.
تُفحَص كل عقدة مرة واحدة لكل سلف لها، أي نحو n × h زيارة لشجرة عمقها h. عندما لا يتجاوز العمق 14، تكون هذه الكلفة منخفضة، لكن في مسار مكوّن من n عقدة تنمو إلى O(n²).
الخوارزمية
- مرّ على كل فهرس
iتكون قيمته غير-1. - أوجد أكبر قيمة في الشجرة الفرعية اليسرى عند
2*i+1(-1إذا كانت فارغة)، وأصغر قيمة في الشجرة الفرعية اليمنى عند2*i+2(100001إذا كانت فارغة). - إذا كانت القيمة الأكبر أكبر من أو تساوي
tree[i]، أو كانت القيمة الأصغر أقل من أو تساويtree[i]، فأعِدfalse. - أعِد
true.
def isValidBST(tree):
n = len(tree)
def largest(i):
# Largest value in the subtree at index i, or -1 when that spot is empty.
if i >= n or tree[i] == -1:
return -1
return max(tree[i], largest(2 * i + 1), largest(2 * i + 2))
def smallest(i):
# Smallest value in the subtree at index i, or 100001 when that spot is empty.
if i >= n or tree[i] == -1:
return 100001
return min(tree[i], smallest(2 * i + 1), smallest(2 * i + 2))
for i in range(n):
if tree[i] == -1:
continue
# Everything on the left must be smaller, everything on the right larger.
if largest(2 * i + 1) >= tree[i] or smallest(2 * i + 2) <= tree[i]:
return False
return Trueيجب أن تزداد القيم بالترتيب الوسطي زيادةً صارمة
الفكرة
تزور الجولة بالترتيب الوسطي الشجرة الفرعية اليسرى، ثم العقدة، ثم الشجرة الفرعية اليمنى، لذا فإنها تقرأ القيم بترتيب تصاعدي في شجرة البحث الثنائية.
والعكس صحيح أيضًا. لأي عقدة v، تأتي شجرتها الفرعية اليسرى بأكملها قبلها مباشرةً في التسلسل، وتأتي شجرتها الفرعية اليمنى بأكملها بعدها مباشرةً. إذا كان التسلسل يزداد بصرامة، فكل قيمة تسبق v أصغر منها، وكل قيمة تليها أكبر منها، لذا تنطبق القاعدة على v وعلى كل عقدة مثلها.
يُظهر المثال الثاني القيم 5, 10, 6, 15, 20: فالانخفاض من 10 إلى 6 يكشف عن العقدة الموضوعة في غير مكانها. تُزار كل عقدة مرة واحدة: زمن O(n)، ومساحة O(n) للقائمة.
الخوارزمية
- اكتب
walk(i): إذا كان الموضع فارغًا، فتوقّف؛ وإلا فانتقل إلى2*i+1، وألحِقtree[i]، ثم انتقل إلى2*i+2. - استدعِ
walk(0)لجمع القيم بالترتيب. - لكل موضع
kبدءًا من1، إذا كانvalues[k-1] ≥ values[k]، فأعِدfalse. - أعِد
true.
def isValidBST(tree):
n = len(tree)
values = []
def walk(i):
# Left subtree, then the node, then the right subtree.
if i >= n or tree[i] == -1:
return
walk(2 * i + 1)
values.append(tree[i])
walk(2 * i + 2)
walk(0)
# A search tree read in order gives strictly increasing values.
for k in range(1, len(values)):
if values[k - 1] >= values[k]:
return False
return Trueمرِّر النطاق المسموح به إلى أسفل الشجرة
الفكرة
يضع كل سلف حدًا واحدًا على العقدة: أقل من a إذا كانت العقدة في الفرع الأيسر لسلف يحمل القيمة a، وأكبر من a إذا كانت في الفرع الأيمن. تشكّل الحدود معًا مجالًا مفتوحًا واحدًا (low, high)، وتكون العقدة في موضعها الصحيح تمامًا عندما تقع قيمتها داخله بشكل صارم.
أنشئ المجال أثناء النزول. يحتفظ الابن الأيسر بـ low ويحصل على high = v؛ ويحتفظ الابن الأيمن بـ high ويحصل على low = v. في المثال الثاني، تمرر 15 المجال (10, 15) إلى ابنها الأيسر، وتفشل 6 فورًا. تقع القيم بين 0 و10^5، لذا يُستخدم -1 و100001 للدلالة على عدم وجود حد. تُفحَص كل عقدة مرة واحدة، بزمن O(n)، مع O(h) عقدة على المكدس.
الخوارزمية
- أضِف
(0, -1, 100001)إلى المكدّس: الجذر، من دون حدّ حقيقي. - أزِل
(i, low, high)من المكدّس. إذا لم تكنtree[i]واقعةً strictly بينlowوhigh، فأعِدfalse. - أضِف كل ابن موجود: الأيسر عند
2*i+1مع(low, tree[i])، والأيمن عند2*i+2مع(tree[i], high). - عندما يصبح المكدّس فارغًا، أعِد
true.
def isValidBST(tree):
n = len(tree)
# Each entry: a node index and the open range (low, high) its value must fall in.
# -1 and 100001 lie outside every allowed value, so they mean "no limit".
stack = [(0, -1, 100001)]
while stack:
i, low, high = stack.pop()
value = tree[i]
if not (low < value < high):
return False
left, right = 2 * i + 1, 2 * i + 2
if left < n and tree[left] != -1:
stack.append((left, low, value)) # the left side must stay below value
if right < n and tree[right] != -1:
stack.append((right, value, high)) # the right side must stay above value
return True
أخطاء شائعة وحالات حدّية
- مقارنة العقدة بأبنائها فقط. في
[10, 5, 15, -1, -1, 6, 20]تبدو كل أزواج الآباء والأبناء صحيحة، لكن6تتجاوز الحد الذي يفرضه الجذر. - السماح بالقيم المتساوية.
[12, 7, 12]غير صالحة: استخدم المقارنة مع<، وليس أبدًا≤. - استخدام
0باعتباره «لا حد أدنى». فهذا يرفض الشجرة الصالحة[0]. - قراءة ابن دون التحقق من
2*i+1 < tree.length. في[12, 7, 12]، يقع الفهرس3خارج نطاق المصفوفة. - في Lua وR، تبدأ المصفوفات من 1. أبقِ فهارس العقد بدءًا من 0 لاستخدام
2*i+1، واقرأtree[i + 1].
أسئلة شائعة3
ما هو التعقيد الزمني للتحقق من صحة شجرة بحث ثنائية؟
يفحص التحقق بالترتيب الوسطي والتحقق من النطاق كل عقدة مرة واحدة: زمن O(n)، مع مساحة إضافية O(h) لمكدس التحقق من النطاق في شجرة عمقها h. وتكلفة مقارنة كل عقدة مع أشجارها الفرعية كاملة هي O(n × h)، وتصل إلى O(n²) في شجرة على شكل مسار.
هل يمكنك التحقق من صحة شجرة بحث ثنائية (BST) باستخدام اجتياز بالترتيب دون تخزين كل قيمة؟
نعم. يتحقق الفحص فقط من مقارنة كل قيمة بالقيمة التي تسبقها، لذا احتفظ بالقيمة السابقة في متغير بدلًا من قائمة، وأعِد false فور أن لا تكون إحدى القيم أكبر. تنخفض المساحة الإضافية إلى O(h) للاستدعاء التعاودي أو المكدس.
هل يمكن أن تحتوي شجرة البحث الثنائية على قيم مكررة؟
ليس وفق التعريف الصارم المستخدم هنا: فالقيمة المساوية لا تناسب أيًّا من الجانبين. تسمح بعض الكتب الدراسية بالقيم المساوية على أحد الجانبين، مثل الجانب الأيمن. وفقًا لهذه القاعدة، تصبح إحدى المقارنتين الصارمتين ≤.
مسائل مشابهة
مسائل تعتمد على الأفكار نفسها. حلّ اثنتين أو ثلاث منها يثبّت النمط.
def isValidBST(tree):
# اكتب الشيفرة هنااختبار #1
اختبار #2
اختبار #3
وسيط
tree = [8, 3, 12, 1, 6, 10, 15]
الإخراج المتوقع
true