Vertical Order Traversal of a Binary Tree
עץ בינארי מאוחסן במערך root בסדר ערימה: השורש נמצא באינדקס 0, הילדים של האינדקס i נמצאים ב-2*i+1 (שמאל) וב-2*i+2 (ימין), -1 מציין מקום ריק, והמערך עשוי להסתיים בערכי -1 נוספים.
השורש נמצא בעמודה 0; ילד שמאלי נמצא עמודה אחת משמאל להורה שלו, וילד ימני עמודה אחת מימין לו. החזר רשימה אחת לכל עמודה, משמאל לימין. בתוך כל עמודה, סדר את הצמתים מלמעלה למטה, ובמקרה של צמתים שנמצאים באותה עמודה ובאותו עומק, סדר אותם לפי ערך עולה.
פונקציה
- rootinteger-array
- העץ בסדר ערימה, עם -1 עבור מקום ריק
- מחזירהinteger-2d-array
- רשימה אחת של ערכים לכל עמודה, החל מהעמודה השמאלית ביותר, מלמעלה למטה
אילוצים
1 ≤ root.length ≤ 32767- כל
root[i]הוא-1או ערך שעבורו0 ≤ root[i] ≤ 5000, ואף ערך אינו מופיע פעמיים. root[0]לעולם אינו-1, ולכן יש בעץ לפחות צומת אחד.- המערך עשוי להסתיים בערכי
-1נוספים אחרי הצומת האחרון. - שני הילדים של מקום ריק גם הם ריקים, ולעץ יש לכל היותר
14רמות.
דוגמאות
- קלט
- root = [5, 2, 9, 4, 7, 1, 6]
- פלט
- [[4], [2], [5, 1, 7], [9], [6]]
- הסבר
- עמודה 0 מכילה את השורש
5, אחריו את7ואת1שתי רמות למטה. שני אלה נמצאים באותה עמודה ובעומק זהה, לכן1מופיע לפני7, אף על פי ש-7נמצא שמאלה יותר.
- קלט
- root = [1, 2, 3, -1, 4, -1, -1, -1, -1, 5]
- פלט
- [[2, 5], [1, 4], [3]]
- הסבר
- עמודה -1 מכילה את
2בעומק 1 ואת5בעומק 3 (הילד השמאלי של4). עמודה 0 מכילה את השורש1ואת4באינדקס 4. עמודה 1 מכילה רק את3.
- קלט
- root = [6, -1, -1]
- פלט
- [[6]]
- הסבר
- ל־
6אין ילדים, לכן לתשובה יש עמודה אחת,[6].
שאלת המשך
כל ערך הוא לכל היותר 5000. האם תוכל להשתמש בכך כדי לוותר על המיון באמצעות השוואות ולסיים בזמן O(n + 5000)?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
לכל צומת יש עמודה, עומק וערך. אם היו לך את כל השלשות האלה ברשימה אחת, איך היית מסדר אותן לפי סדר התשובה?
חיפוש לרוחב כבר מחזיר את הצמתים מלמעלה למטה. איזה חלק מהסדר עדיין חסר כשכמה צמתים חולקים עמודה ורמה?
עבדי על העץ רמה אחת בכל פעם. מיין כל רמה לפי עמודה, ואז לפי ערך, הוסף כל ערך לרשימה של העמודה שלו, ובסוף קרא את הרשימות מהעמודה הקטנה ביותר לגדולה ביותר.
פתרון
לכל צומת יש שלושה מפתחות: עמודה, עומק וערך. התשובה היא הצמתים המסודרים לפי המפתחות האלה, רשימה אחת לכל עמודה. במקרה של שוויון בעמודה ובעומק, הסדר נקבע לפי הערך, ולא לפי הסדר משמאל לימין שבו הסריקה פוגשת בהם, לכן צריך למיין: את כל הצמתים בבת אחת, או כל רמה בחיפוש לרוחב.
תעדו כל צומת, ואז מיינו
האינטואיציה
בקרו בכל צומת פעם אחת, בכל סדר שהוא, ורשמו את השלשה (עמודה, עומק, ערך). מעבר לצומת ילד מוסיף 1 לעומק ומזיז את העמודה 1 שמאלה או ימינה. לאחר מכן מיינו את השלשות לפי עמודה, עומק וערך. כעת הצמתים של כל עמודה נמצאים יחד, בסדר הנדרש. עברו על הרשימה הממוינת והתחילו רשימה פנימית חדשה בכל פעם שהעמודה משתנה.
בדוגמה 1, השלשות של עמודה 0 ממוינות כך: (0, 0, 5), (0, 2, 1), (0, 2, 7). המיון, O(n log n), הוא הגורם הדומיננטי ביחס למעבר O(n), והשלשות תופסות מרחב של O(n).
אלגוריתם
- דחוף למחסנית (אינדקס 0, עמודה 0, עומק 0).
- הוצא צומת, תעד (עמודה, עומק, ערך), ודחוף כל ילד אמיתי עם עמודה
col-1אוcol+1ועומקdepth+1. - כשהמחסנית ריקה, מיין את השלשות לפי עמודה, אחר כך עומק, ואז ערך.
- עבור עליהן, והתחל רשימה חדשה בכל פעם שהעמודה משתנה.
def verticalTraversal(root):
n = len(root)
entries = [] # (column, depth, value) for every node
stack = [(0, 0, 0)] # (node index, column, depth); the root is never empty
while stack:
i, col, depth = stack.pop()
entries.append((col, depth, root[i]))
for child, child_col in ((2 * i + 1, col - 1), (2 * i + 2, col + 1)):
if child < n and root[child] != -1:
stack.append((child, child_col, depth + 1))
# Column, then depth, then value: exactly the order of the answer.
entries.sort()
result = []
for k, (col, depth, value) in enumerate(entries):
if k == 0 or col != entries[k - 1][0]:
result.append([]) # a new column starts
result[-1].append(value)
return resultרמה אחר רמה, מיין בתוך כל רמה
האינטואיציה
חיפוש לרוחב מטפל בכל פעם ברמה אחת. אם כל צומת מוסיף את הערך שלו לרשימת העמודה שלו כשהוא מטופל, כל רשימה כבר מסודרת מלמעלה למטה, ולכן אין צורך למיין לפי העומק. שוויונות מתרחשים רק בתוך אותה רמה, לכן ממיינים כל רמה לפי עמודה ואז לפי ערך, לפני שמטפלים בה.
בדוגמה 1, רמה 2 מגיעה בתור 4, 7, 1, 6 בעמודות -2, 0, 0, 2 ומתמיינת ל-4, 1, 7, 6, ולכן עמודה 0 הופכת ל-[5, 1, 7]. העמודות המאוכלסות יוצרות טווח רציף אחד, ולכן די לעקוב אחר הקטנה והגדולה ביותר.
מיון של רמה הכוללת m צמתים עולה O(m log m). במקרה הגרוע הסיבוכיות עדיין O(n log n), מכיוון שהרמה האחרונה של עץ מלא מכילה כמחצית מהצמתים. צריכת הזיכרון היא O(n).
אלגוריתם
- התחל ברמה שמכילה (אינדקס 0, עמודה 0).
- מיין את הרמה לפי עמודה, ואז לפי ערך.
- לפי הסדר הזה, הוסף כל ערך לרשימה של העמודה שלו, ואסוף את הילדים האמיתיים שלו, עם העמודות
col-1ו־col+1, לרמה הבאה. - חזור על הפעולה עד שרמה מתרוקנת, תוך מעקב אחר העמודה הקטנה ביותר והגדולה ביותר.
- החזר את הרשימות מהעמודה הקטנה ביותר ועד הגדולה ביותר.
def verticalTraversal(root):
n = len(root)
columns = {} # column -> its values, top to bottom
level = [(0, 0)] # (node index, column) of every node on the current level
while level:
# Every node here has the same depth: order them by column, then value.
level.sort(key=lambda node: (node[1], root[node[0]]))
next_level = []
for i, col in level:
columns.setdefault(col, []).append(root[i])
for child, child_col in ((2 * i + 1, col - 1), (2 * i + 2, col + 1)):
if child < n and root[child] != -1:
next_level.append((child, child_col))
level = next_level
# The columns that hold a node form one unbroken range.
return [columns[col] for col in range(min(columns), max(columns) + 1)]
מלכודות ומקרי קצה
- השארת שוויונות בסדר משמאל לימין. חיפוש לרוחב ללא מיון בכל רמה נותן
[5, 7, 1]עבור עמודה 0 בדוגמה 1, ולא[5, 1, 7]. - הוספת ערכים במהלך מעבר לעומק ללא מיון. ב-
[1, 2, 3, -1, 4, -1, -1, -1, -1, -1, 5], מעבר שמתחיל משמאל מוסיף את5(עומק 3) לעמודה 1 לפני3(עומק 1). - מיון לפי עמודה וערך, אך לא לפי עומק. עמודה 0 בדוגמה 1 נקראת כעת
[1, 5, 7]. - שימוש במחרוזות כמפתחות של העמודות. במיון כטקסט,
"-1"מופיע לפני"-2", ולכן העמודות שמשמאל ל-0 מופיעות בסדר שגוי.
שאלות נפוצות3
מהי סיבוכיות הזמן של מעבר לפי סדר אנכי בעץ בינארי?
מיון כל הצמתים לפי עמודה, עומק וערך עולה O(n log n) והוא דומיננטי ביחס למעבר של O(n). למיון רמה אחר רמה יש אותו מקרה גרוע ביותר, כי הרמה האחרונה של עץ מלא מכילה כמחצית מהצמתים. המקום הוא O(n).
האם אפשר לבצע מעבר בסדר אנכי ללא מיון?
רק אם בשוויון הסדר הוא משמאל לימין. אז חיפוש לרוחב שמוסיף כל ערך לרשימה של העמודה שלו נותן את התשובה ב־O(n). אם בשוויון הסדר נקבע לפי הערך, צריך לבצע מיון כלשהו, אבל רק בתוך רמה, כי רק צמתים באותה רמה יכולים להיות בשוויון.
איך סריקה אנכית לפי סדר קשורה למבט מלמעלה ולמבט מלמטה?
שלושת הייצוגים מקבצים צמתים לפי אותם מספרי עמודות. התצוגה העליונה שומרת את הצומת הרדוד ביותר בכל עמודה, והתצוגה התחתונה את העמוק ביותר. מעבר לפי סדר אנכי שומר את כל הצמתים, וכמה צמתים באותה רמה מסודרים לפי ערכם, בעוד שהתצוגות בוחרות לפי מיקום.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def verticalTraversal(root):
# כתבו כאן את הקודבדיקה #1
בדיקה #2
בדיקה #3
ארגומנט
root = [5, 2, 9, 4, 7, 1, 6]
הפלט הצפוי
[[4], [2], [5, 1, 7], [9], [6]]