Boundary Traversal of Binary Tree
המערך root מכיל עץ בינארי בסדר רמות: השורש באינדקס 0, הילדים של אינדקס i באינדקסים 2i+1 (שמאל) ו-2i+2 (ימין), -1 מציין מקום ריק, וייתכן שבסוף יופיעו ערכי -1 נוספים. עלה הוא צומת ללא ילדים.
יש להחזיר את הגבול נגד כיוון השעון, החל מהשורש:
- השורש.
- הגבול השמאלי, מלמעלה למטה: החל מהילד השמאלי של השורש, עוברים לילד השמאלי אם יש כזה, אחרת לילד הימני. עוצרים לפני עלה.
- כל העלים, משמאל לימין.
- הגבול הימני, מלמטה למעלה: תמונת הראי של הגבול השמאלי, החל מהילד הימני של השורש.
אף צומת אינו מופיע פעמיים, ולכן עץ עם צומת אחד מחזיר רק את השורש שלו.
פונקציה
- rootinteger-array
- העץ הבינארי בסדר רמות, כאשר -1 מציין מקום ריק
- מחזירהinteger-array
- ערכי הגבול, נגד כיוון השעון מהשורש
אילוצים
1 ≤ root.length ≤ 32767- כל
root[i]הוא-1או ערך שמקיים0 ≤ root[i] ≤ 105. root[0]לעולם אינו-1, ולכן לעץ יש לפחות צומת אחד.- ייתכן שהמערך יסתיים בערכי
-1נוספים אחרי הצומת האחרון. - גם שני הילדים של מקום ריק ריקים, והעומק הוא לכל היותר
14. - ערכי הצמתים שונים זה מזה.
דוגמאות
- קלט
- root = [7, 3, 9, 1, 5, 8, -1, -1, -1, 4, 6]
- פלט
- [7, 3, 1, 4, 6, 8, 9]
- הסבר
- אחרי השורש
7, הגבול השמאלי הוא3: הילד שלו,1, הוא עלה. העלים הם1,4,6,8. הגבול הימני הוא9, מעל העלה8.
- קלט
- root = [10, 4, -1, -1, 6, -1, -1, -1, -1, 5, 8]
- פלט
- [10, 4, 6, 5, 8]
- הסבר
- ל־
4אין צאצא שמאלי, לכן הגבול השמאלי פונה אל6, ואז נעצר בעלה5. העלים5,8באים אחריו. לשורש אין צאצא ימני.
- קלט
- root = [3]
- פלט
- [3]
- הסבר
- השורש הוא העץ כולו. הוא מופיע פעם אחת, ולא מופיע שוב כעלה.
שאלת המשך
איך היית מונה את הגבול בכיוון השעון: השורש, הקצה הימני מלמעלה למטה, העלים מימין לשמאל, ואז הקצה השמאלי מלמטה למעלה?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
חלקו את התשובה לארבעה חלקים: השורש, הקצה השמאלי, העלים, הקצה הימני. אילו מהם הם מסלול יחיד במורד העץ, ואילו דורשים מעבר מלא?
כל קשת היא מסלול אחד: מהילד של השורש, ממשיכים לנוע אל הילד שבאותו צד, או אל הילד השני כשהצד הזה ריק. העלים לפי הסדר משמאל לימין מתקבלים מסריקה לעומק שמבקרת תחילה בתת-העץ השמאלי. שימו לב לחיבורים: העלה שבו מסתיימת קשת שייך לעלים.
רד כלפי מטה מאינדקס
1והוסף כל צומת שאינו עלה. לאחר מכן הוסף כל עלה בסריקה בסדר תחילי. לאחר מכן רד כלפי מטה מאינדקס2, אסוף כל צומת שאינו עלה והוסף את הרשימה בסדר הפוך.
פתרון
התשובה מחברת ארבעה חלקים: השורש, מסלול לאורך הקצה השמאלי, העלים משמאל לימין ומסלול לאורך הקצה הימני הנקרא לאחור. בנקודות החיבור, יש לרשום פעם אחת כל עלה שנמצא בקצה של מסלול, וגם שורש שהוא עלה.
עוברים על שלושת החלקים בנפרד
האינטואיציה
כל קשת היא מסלול אחד: מהילד של השורש, מונים צמתים עד שהבא בתור יהיה עלה, שאותו מונה במקום זאת חלק העלים. סריקה בסדר תחילי, שבה הולכים שמאלה לפני ימינה, פוגשת את העלים בסדר משמאל לימין, כי כל צומת בתת-העץ השמאלי נמצא משמאל לתת-העץ הימני, בלי קשר לעומק שלו. אוספים את הקשת הימנית מלמעלה למטה ומוסיפים אותה בסדר הפוך.
הקשתות עוקבות כל אחת אחר מסלול אחד, וסריקת העלים מבקרת בכל צומת פעם אחת: זמן O(n), עם רקורסיה בעומק של h רמות.
אלגוריתם
- התחל מהשורש; אם הוא עלה, החזר אותו.
- החל מאינדקס
1, כל עוד הצומת קיים ואינו עלה, הוסף אותו ועבור לילד השמאלי שלו, או לילד הימני אם אין ילד שמאלי. - עבור בסדר תחילי, קודם שמאלה, החל מהאינדקסים
1ו־2, והוסף כל עלה. - החל מאינדקס
2, בצע את אותו הדבר כמו בשלב 2, תוך העדפת ילדים ימניים ואיסוף הצמתים. - הוסף את הצמתים שנאספו בסדר הפוך.
def boundaryTraversal(root):
n = len(root)
def real(i):
return i < n and root[i] != -1
def is_leaf(i):
return not real(2 * i + 1) and not real(2 * i + 2)
result = [root[0]]
if is_leaf(0):
return result
# 1. The left boundary, top-down, stopping before its leaf.
i = 1
while real(i) and not is_leaf(i):
result.append(root[i])
i = 2 * i + 1 if real(2 * i + 1) else 2 * i + 2
# 2. Every leaf: a pre-order walk meets them from left to right.
def add_leaves(i):
if not real(i):
return
if is_leaf(i):
result.append(root[i])
return
add_leaves(2 * i + 1)
add_leaves(2 * i + 2)
add_leaves(1)
add_leaves(2)
# 3. The right boundary, collected top-down and added bottom-up.
right = []
i = 2
while real(i) and not is_leaf(i):
right.append(root[i])
i = 2 * i + 2 if real(2 * i + 2) else 2 * i + 1
result.extend(reversed(right))
return resultמעבר מקדים אחד עם דגלי קצוות
האינטואיציה
מזג את הסריקות למעבר קדם־סדר יחיד באמצעות מחסנית, על ידי מתן דגל לכל צומת: 1 בקצה השמאלי, 2 בקצה הימני, 0 בפנים. הילדים של השורש מתחילים עם 1 ו־2. צומת קצה מעביר את הדגל שלו לילד שבצד שלו, או לילד היחיד שלו; כל ילד אחר מקבל 0. בדוגמה השנייה, 1 עובר מ־4 אל 6 ואז אל 5.
כשהצמתים יוצאים מהמחסנית, הוסף כל עלה וכל צומת שאינו עלה ומסומן בדגל 1; אסוף את הצמתים שאינם עלים ומסומנים בדגל 2, והוסף אותם בסדר הפוך בסוף. מעבר קדם־סדר מגיע לכל צומת בקצה השמאלי לפני כל עלה, כי הקצה פונה ימינה רק במקום שבו אין שום דבר משמאלו.
אלגוריתם
- התחל בשורש. דחוף את הילד הימני שלו עם הדגל
2, ואז את הילד השמאלי שלו עם הדגל1. - הוצא צומת מהמחסנית. הוסף אותו אם הוא עלה או אם יש לו את הדגל
1; הוסף צומת שאינו עלה עם הדגל2לרשימה הימנית. - דחוף את הילד הימני שלו, ואז את הילד השמאלי שלו, כל אחד עם הדגל שהכלל מגדיר עבורו.
- כשהמחסנית ריקה, הוסף את הרשימה הימנית בסדר הפוך.
def boundaryTraversal(root):
n = len(root)
def real(i):
return i < n and root[i] != -1
result = [root[0]]
right = [] # the right boundary, top-down; it goes on the end reversed
# Each entry is (index, edge): 1 = left boundary, 2 = right boundary, 0 = inside.
stack = []
if real(2):
stack.append((2, 2))
if real(1):
stack.append((1, 1))
while stack:
i, edge = stack.pop()
has_left, has_right = real(2 * i + 1), real(2 * i + 2)
if not has_left and not has_right:
result.append(root[i]) # a leaf; pre-order meets them left to right
elif edge == 1:
result.append(root[i]) # the left boundary, top-down
elif edge == 2:
right.append(root[i])
# An edge passes to the child on its own side, or to an only child.
# The right child goes on the stack first, so the left one comes out first.
if has_right:
stack.append((2 * i + 2, edge if edge == 2 or not has_left else 0))
if has_left:
stack.append((2 * i + 1, edge if edge == 1 or not has_right else 0))
result.extend(reversed(right))
return result
מלכודות ומקרי קצה
- רישום של צומת פעמיים. סריקת קשת שאינה נעצרת לפני העלה שלו רושמת את
1פעמיים בדוגמה 1, ועץ בעל צומת אחד לא צריך לרשום שוב את השורש שלו כעלה. - התייחסות לצומת הראשון בכל רמה כאל הגבול השמאלי. אם הבן השמאלי של השורש הוא עלה ותת-העץ הימני עמוק יותר, הרמות העמוקות יותר מתחילות בצמתים מתת-העץ הימני, אך הגבול השמאלי ריק.
- מעקב אחר בנים שמאליים בלבד. בדוגמה 2 הגבול חייב לעבור מ-
4אל הבן הימני שלו,6. - איסוף העלים לפי רמות. כך, בדוגמה 1 מתקבלים
1,8,4,6. - ב-Lua וב-R, יש להשאיר את האינדקסים של הצמתים מבוססי 0 עבור החישוב
2i+1, ולקרוא אתroot[i + 1].
שאלות נפוצות3
מהי סריקת הגבול של עץ בינארי?
הוא מציג את קווי המתאר של העץ נגד כיוון השעון: השורש, הקצה השמאלי מלמעלה למטה, העלים משמאל לימין והקצה הימני מלמטה למעלה. הקצוות אינם כוללים את העלים שלהם, כך שאף צומת לא מופיע פעמיים.
מהי סיבוכיות הזמן של מעבר בגבול?
O(n). שתי ההליכות לאורך הקצוות עוקבות כל אחת אחר מסלול אחד, וההליכה בין העלים מבקרת בכל צומת פעם אחת. המקום הנוסף הוא O(h) עבור הרקורסיה או המחסנית.
האם הגבול השמאלי תמיד עוקב אחר ילדים שמאליים?
לא. הוא עובר לילד השמאלי כשיש כזה, ולילד הימני אחרת: הנתיב החיצוני ביותר שרואים מצד שמאל. אם לשורש אין ילד שמאלי, הגבול השמאלי ריק.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def boundaryTraversal(root):
# כתבו כאן את הקודבדיקה #1
בדיקה #2
בדיקה #3
ארגומנט
root = [7, 3, 9, 1, 5, 8, -1, -1, -1, 4, 6]
הפלט הצפוי
[7, 3, 1, 4, 6, 8, 9]