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, ששווה לערך השורש. תת-העץ הימני חייב להיות גדול יותר ממש, ולכן העץ נכשל.
שאלת המשך
ההורה של צומת נמצא באינדקס (i-1)/2, בעיגול כלפי מטה. האם תוכל לעבור על העץ לפי הסדר עם שטח נוסף O(1), באמצעות מעבר דרך ההורים במקום שימוש במחסנית או ברקורסיה?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
ב־
[10, 5, 15, -1, -1, 6, 20], כל צומת גדול יותר מהילד השמאלי שלו וקטן יותר מהילד הימני שלו. למה זה עדיין לא עץ חיפוש?כל אב קדמון מציב גבול לצומת: מתחתיו אם הצומת נמצא משמאלו, מעליו אם הצומת נמצא מימינו. יחד, הגבולות האלה יוצרים טווח פתוח אחד. מעבר שמאלה מערך
vמוריד את הגבול העליון ל־v; מעבר ימינה מעלה את הגבול התחתון ל־v.שמרו מחסנית של
(index, low, high), שמתחילה בשורש ובטווח רחב יותר מכל ערך מותר. הוציאו רשומה מהמחסנית, נכשלו אם הערך אינו נמצא strictly בתוך הטווח, והכניסו כל ילד קיים עם הטווח המצומצם שלו.
פתרון
הכלל חל על תתי־עצים שלמים, ולכן השוואה של כל צומת עם שני ילדיו אינה מספיקה: צומת שנמצא עמוק בעץ עלול להפר מגבלה שקבע אחד מאבותיו כמה רמות למעלה. אפשר לעבור על העץ לפי הסדר ולבדוק שהערכים עולים בקפדנות, או להעביר לכל צומת את הטווח שאבותיו מתירים.
השווה כל צומת עם תתי־העצים שלו
האינטואיציה
כל ערך בצד שמאל של צומת קטן מערכו 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הערכים בסריקה לפי הסדר חייבים לעלות באופן строго
האינטואיציה
סריקה בסדר inorder מבקרת בתת-העץ השמאלי, אחר כך בצומת, ואז בתת-העץ הימני, ולכן בעץ חיפוש בינארי היא קוראת את הערכים בסדר ממוין.
גם ההפך נכון. לכל צומת 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]אינו נמצא ממש בין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²).
האם אפשר לאמת עץ חיפוש בינארי באמצעות סריקה בסדר־תוכי בלי לשמור כל ערך?
כן. הבדיקה משווה כל ערך רק לערך שלפניו, לכן שמור את הערך הקודם במשתנה במקום ברשימה, והחזר false ברגע שערך כלשהו אינו גדול יותר. המקום הנוסף יורד ל־O(h) עבור הרקורסיה או המחסנית.
האם עץ חיפוש בינארי יכול להכיל ערכים כפולים?
לא לפי ההגדרה המחמירה שבה משתמשים כאן: ערך שווה אינו מתאים לאף אחד מהצדדים. יש ספרי לימוד שמאפשרים ערכים שווים באחד הצדדים, למשל בצד ימין. לפי הכלל הזה, אחת מההשוואות המחמירות הופכת ל־≤.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def isValidBST(tree):
# כתבו כאן את הקודבדיקה #1
בדיקה #2
בדיקה #3
ארגומנט
tree = [8, 3, 12, 1, 6, 10, 15]
הפלט הצפוי
true