מבני נתונים — אוניברסיטת רייכמן

הרצאה 7 — עצי חיפוש בינאריים (BST)

Binary Search Trees · Operations & Augmentation

תוכן עניינים

  1. המוטיבציה — Sequence ADT וה-"wish list"
  2. הגדרה רקורסיבית: עץ, עץ בינארי, BST
  3. תכונת ה-BST ועומק העץ
  4. מימוש BST (עם מצביעי הורה)
  5. Find — חיפוש מפתח
  6. FindMin / FindMax
  7. היכן בעץ נמצאים האיברים הקטנים/גדולים מ-\(x\)
  8. Successor & Predecessor
  9. Insert — הכנסה
  10. Delete — מחיקה (3 מקרים)
  11. Join & Split
  12. מעבר על עצים: Preorder, Inorder, Postorder
  13. בניית BST — חסם תחתון Ω(n log n)
  14. בניית BST מאיברים ממויינים — Θ(n)
  15. Augmented Data — שדות נוספים
  16. SELECT(T, k) — האיבר ב-rank k
  17. RANK(T, x) — דרגת איבר נתון
  18. Sum of Keys < x
  19. סיכום ונקודות מפתח

1. המוטיבציה — Sequence ADT וה-"wish list"

בהרצאות הקודמות התחלנו עם מבני נתונים ל-ADTs פשוטים (sequence, stack, queue, priority queue), ניתחנו סיבוכיות, ולמדנו על אלגוריתמי מיון. עכשיו חוזרים ל-Sequence ADT עם תקווה לפעולות יעילות יותר.

סיבוכיות זמן — Array & Linked List

פעולהSorted ArraySorted Linked ListUnsorted ArrayDesired
FindO(log n)O(n)O(n)O(1)
InsertO(n)O(n)O(1)O(1)
DeleteO(n)O(n)O(n)O(1)
אי אפשר להגיע ל-O(1) בכל הפעולות
הסיבה: חסם תחתון Ω(n log n) למיון מבוסס השוואות מעיד שלא ניתן להחזיק Sequence עם כל הפעולות ב-O(1).

מה נשיג עם BST

פעולהSorted ArrayLinked ListUnsortedBST (מאוזן)
FindO(log n)O(n)O(n)O(log n)
InsertO(n)O(n)O(1)O(log n)
DeleteO(n)O(n)O(n)O(log n)

2. הגדרה רקורסיבית: עץ, עץ בינארי, BST

עץ (Tree)
קבוצה של צמתים שהיא או:
  • ריקה, או
  • יש לה צומת אחד הנקרא שורש, שמצביע לשורשים של אפס או יותר עצים זרים (תתי-עצים).
"זרים" = אין צמתים משותפים.
עץ בינארי (Binary Tree)
קבוצה של צמתים שהיא או:
  • ריקה, או
  • יש לה שורש שמצביע ל-{עץ בינארי שמאלי \(T_L\), עץ בינארי ימני \(T_R\)} כאשר \(T_L\) ו-\(T_R\) זרים.
\(T_L\) ו-\(T_R\) נקראים תת-העץ השמאלי והימני של השורש.
עץ חיפוש בינארי (Binary Search Tree, BST)
עץ בינארי עם תכונת ה-BST: לכל צומת \(v\):
  • כל המפתחות בתת-העץ השמאלי של \(v\) קטנים מ-\(\text{key}(v)\).
  • כל המפתחות בתת-העץ הימני של \(v\) גדולים מ-\(\text{key}(v)\).
(לעת עתה — כל המפתחות שונים.)

3. תכונת ה-BST ועומק העץ

תכונת ה-BST (נכונה לכל \(y\) בתת-עץ של \(v\))
יהי \(v\) צומת ב-BST:
  • אם \(y\) בתת-העץ השמאלי אז \(\text{key}(y) < \text{key}(v)\).
  • אם \(y\) בתת-העץ הימני אז \(\text{key}(y) > \text{key}(v)\).

עומק BST

דוגמה
לאותה קבוצת מפתחות \(\{2,3,5,6,7,8\}\) יש BSTs שונים — אחד מאוזן (עומק \(\Theta(\log n)\)) ואחד מנוון (עומק \(\Theta(n)\)). מבנה ה-BST תלוי בסדר ההכנסה.

4. מימוש BST (עם מצביעי הורה)

כל צומת מכיל:

נשמור גם מצביע T לשורש העץ.

5. Find — חיפוש מפתח

מימוש רקורסיבי

Find(T, x):
case
  T = null  : return null
  T.key = x : return T
  T.key > x : return Find(T.left, x)
  T.key < x : return Find(T.right, x)

מימוש לא-רקורסיבי

Find(T, x):
while T ≠ null and T.key ≠ x:
  if x < T.key: T = T.left
  else:         T = T.right
return T
סיבוכיות
O(h) — בלכל איטרציה יורדים רמה אחת בעץ, ולכן בחסם הגרוע מבצעים h צעדים, כאשר h = גובה העץ.

6. FindMin / FindMax

FindMin: ירידה רצופה שמאלה עד לצומת ללא בן שמאל.

FindMin(T):
if T.left = null then return T
else return FindMin(T.left)

// או לולאה:
FindMin(T):
while T.left ≠ null do T = T.left
return T

FindMax: סימטרי — ירידה רצופה ימינה.

סיבוכיות
O(h).

7. היכן בעץ נמצאים האיברים הקטנים/גדולים מ-\(x\)?

איברים קטנים מ-\(x\)

איברים גדולים מ-\(x\)

תובנה
הניתוח הזה הוא הבסיס לאלגוריתמים של Successor, Predecessor, Rank, ו-SumSmaller. כשעולים בעץ — כל פעם שעוברים מבן ימני להורה, ההורה ותת-העץ השמאלי שלו קטנים מ-\(x\); ולהפך מבן שמאלי.

8. Successor & Predecessor

Successor של \(x\)
  • אם ל-\(x\) יש תת-עץ ימני: ה-Successor הוא ה-minimum של תת-העץ הימני.
    דוגמה: successor(7) = 9 (ירידה שמאלה מ-15).
  • אחרת: ה-Successor הוא ה-ancestor הנמוך ביותר ש-\(x\) בתת-העץ השמאלי שלו.
    דוגמאות: successor(11) = 15, successor(18) = 20.
Tree-Successor(x):
if x.right ≠ null then
    return FindMin(x.right)
else
    y ← x.parent
    while y ≠ null and x = y.right:
        x ← y
        y ← y.parent
    return y
סיבוכיות
O(h).

Predecessor: סימטרי — חפש max בתת-עץ שמאלי, אחרת עלה עד שמגיעים מבן שמאלי.

9. Insert — הכנסה

רעיון
  1. בצע "Find" עבור \(x\).
  2. אם \(x\) נמצא — הכרז שהמפתח כבר קיים.
  3. אחרת, ה-Find עוצר ב-pointer null. הוסף שם צומת חדש עם המפתח \(x\).
Tree-Insert(T, z):
y ← null      // prev node
x ← T         // current node
While x ≠ null:
  y ← x
  if z.key < x.key
    then x ← x.left
    else x ← x.right
z.parent ← y
if y = null
  then T ← z
  elseif z.key < y.key
    then y.left ← z
    else y.right ← z
סיבוכיות
O(h).

10. Delete — מחיקה (3 מקרים)

בעיה
כשמוחקים צומת \(v\), איך ממלאים את ה-"חור" בעץ?

שלושת המקרים

  1. ל-\(v\) אין בנים: מצביע אליו → null. דוגמה: Delete(5) — סתם מנקים.
  2. ל-\(v\) בן אחד: מחליפים את המצביע ל-\(v\) במצביע לבן שלו. דוגמה: Delete(24) → הבן 11 תופס את מקומו.
  3. ל-\(v\) שני בנים: מעתיקים את Successor (= ה-minimum בתת-עץ הימני) ל-\(v\), ואז מוחקים את ה-Successor רקורסיבית. ה-Successor לעולם אין לו בן שמאלי, ולכן ה-מחיקה הרקורסיבית נופלת למקרה 1 או 2.
Tree-Delete(T, z):
1  if z.left = null or z.right = null
2     then y ← z
3     else y ← Tree-Successor(z)
4  if y.left ≠ null
5     then x ← y.left
6     else x ← y.right
7  if x ≠ null
8     x.parent ← y.parent
9  if y.parent = null
10    then T.root ← x
11    else if y = (y.parent).left
12       then (y.parent).left  ← x
13       else (y.parent).right ← x
14 if y ≠ z
15    then z.key ← y.key   // also copy all data fields
16 return y

סמלים: \(y\) — הצומת שבאמת נמחק; \(x\) — בנו היחיד של \(y\) (או null).

סיבוכיות
O(h).

11. Join & Split

Join(T₁, x, T₂)

קלט / פלט
קלט: שני עצי BST \(T_1, T_2\) וצומת \(x\), כך שמפתחות \(T_1\) < key(\(x\)) < מפתחות \(T_2\).
פלט: BST אחד המכיל \(T_1 \cup \{x\} \cup T_2\).
פתרון: שים את \(x\) כשורש, \(T_1\) כתת-עץ שמאלי, \(T_2\) כתת-עץ ימני.

סיבוכיות: O(1).

Split(T, x)

קלט / פלט
קלט: BST \(T\) ואיבר עם מפתח \(x\) בעץ.
פלט: שני BSTs ו-\(x\) — \(T_1\) עם המפתחות הקטנים מ-\(x\), \(T_2\) עם הגדולים, ו-\(x\) ביניהם.

אלגוריתם:

  1. סרוק את המסלול מ-root ל-\(x\).
  2. חבר את תת-העץ השמאלי של \(x\) עם כל העצים משמאל למסלול → \(T_1\).
  3. חבר את תת-העץ הימני של \(x\) עם כל העצים מימין למסלול → \(T_2\).

סיבוכיות: O(h).

המשמעות של Time Complexity
כל הפעולות שראינו עד כה הן ב-\(O(h)\). Worst case: \(h\) עשוי להיות \(\Omega(n)\) (עץ מנוון). Best case: \(O(\log n)\) (עץ מאוזן). שמירה על איזון העץ — נושא של הרצאות הבאות.

12. מעבר על עצים: Preorder, Inorder, Postorder

שלושה מעברים נפוצים, מוגדרים רקורסיבית:

  1. Preorder (DLR): צומת, אז ילדים (שמאל קודם).
  2. Inorder (LDR): בן שמאל, צומת, בן ימין.
  3. Postorder (LRD): ילדים, אז צומת.

Inorder — אלגוריתם ודוגמה

inorder-tree-walk(T):
if T ≠ null then
  inorder-tree-walk(T.left)
  print T.key
  inorder-tree-walk(T.right)
תוצאה חשובה
Inorder על BST מדפיס את המפתחות בסדר ממויין!
לדוגמה, על עץ עם המפתחות {5,10,11,17,24,94,97}: הפלט הוא 5, 10, 11, 17, 24, 94, 97.

Postorder — שימוש להערכת ביטויים מתמטיים

בעץ בינארי מלא שבו כל צומת פנימי = פעולה בינארית וכל עלה = מספר, postorder מחשב את ערך הביטוי:

Preorder — שימושים

סיבוכיות מעברים
כל צומת מבוקר לכל היותר 3 פעמים. סה"כ: Θ(n).

13. בניית BST — חסם תחתון Ω(n log n)

Naïve insert

טענה (חסם תחתון)
הכנסת \(n\) מפתחות לא-ממויינים ל-BST, באמצעות השוואות בלבד, דורשת \(\Omega(n \log n)\) זמן.
כלומר — בלתי אפשרי לבנות BST מ-keys לא-ממויינים ב-\(o(n \log n)\).
הוכחה (בשלילה):
נניח שאפשר לבנות BST ב-o(n log n).
נבצע Inorder ב-Θ(n) ונקבל את n המפתחות בסדר ממויין.
זה אומר שניתן למיין n מפתחות ב-o(n log n) ע"י השוואות —
סתירה לחסם התחתון Ω(n log n) של מיון מבוסס-השוואות!  ∎

14. בניית BST מאיברים ממויינים — Θ(n)

רעיון
קח את החציון כשורש. הפעל רקורסיבית על שני החצאים. כל צומת מתווסף ב-\(\Theta(1)\), והעץ הנוצר מאוזן.
BuildBST(A, q, r):
if q > r then return null
else
  m = ⌊(q+r)/2⌋
  create a new node v
  v.data = A[m]
  v.left  = BuildBST(A, q, m-1)
  v.right = BuildBST(A, m+1, r)
return v
סיבוכיות
Θ(n) — כי \(A\) ממויין, אין השוואות, כל יצירת צומת ב-\(\Theta(1)\).
בונוס: העץ הנוצר מאוזן, בגובה \(\Theta(\log n)\).

15. Augmented Data — שדות נוספים

רעיון
נשמור בכל צומת שדה נוסף שמסייע לענות על שאילתות יעיל. השדה צריך להיות ניתן עדכון תוך כדי Insert/Delete בסיבוכיות \(\Theta(h)\).

שאלות נפוצות שאפשר לענות עליהן בעזרת augmented data

Size augmentation

בכל צומת \(v\) נשמור:

עדכון השדה

16. SELECT(T, k) — האיבר ב-rank \(k\)

מטרה
החזר pointer לצומת המכיל את האיבר ב-rank \(k\) ב-\(T\).

רעיון

\(r[x]\) = דרגת \(x\) בתת-העץ ששורשו \(x\). בעזרת שדה \(\text{size}\): \(r[x] = (x.\text{left}).\text{size} + 1\).

SELECT(x, k):
if (x = null) return null
if x.left ≠ null
  then r ← (x.left).size + 1
  else r ← 1
if k = r:        return x
else if k < r:   return SELECT(x.left,  k)
else:            return SELECT(x.right, k - r)
סיבוכיות
O(h) — יורדים בעץ פעם אחת.
דוגמה — SELECT(T, 3) כאשר T שורש 10
  1. בשורש 10: \(r[10] = 3 + 1 = 4\). 4 > 3 → לך שמאלה.
  2. בצומת 8: \(r[8] = 1 + 1 = 2\). 2 < 3 → לך ימינה עם \(k = 3 - 2 = 1\).
  3. בצומת 9: \(r[9] = 0 + 1 = 1\). 1 = 1 → נמצא! החזר 9.

17. RANK(T, x) — דרגה של איבר נתון

מטרה
בהינתן pointer \(x\) לצומת ב-BST \(T\), החזר את הדרגה של \(x\) ב-\(T\).

רעיון: סופרים את המפתחות שקטנים מ-\(x\)

  1. הוסף את האיברים בתת-העץ השמאלי של \(x\) (= \(x.\text{left}.\text{size}\)), \(+1\) ל-\(x\) עצמו.
  2. עבור על המסלול \(p\) מ-\(x\) ל-root: לכל \(v \in p\), אם \(v\) הוא בן ימני — הוסף את ה-size של תת-העץ השמאלי של אבא של \(v\), \(+1\) על האבא עצמו.
RANK(T, x):
if x.left ≠ null
  then r ← (x.left).size + 1
  else r ← 1
y ← x
while y.parent ≠ null:
  if y = (y.parent).right
    then if (y.parent).left ≠ null
      then r ← r + ((y.parent).left).size + 1
      else r ← r + 1
  y ← y.parent
return r
סיבוכיות
O(h) — עולים בעץ פעם אחת.

18. Sum of Keys < \(x\)

מטרה
בהינתן \(x\), החזר את סכום כל המפתחות בעץ הקטנים מ-\(x\).

הרחבה חדשה: \(v.\text{sum}\)

בכל צומת \(v\): \(v.\text{sum}\) = סכום כל המפתחות בתת-העץ ששורשו \(v\). (אנלוגי ל-size, אבל סוכמים מפתחות במקום סופרים צמתים.)

SumSmaller(x, k):
if x = null then return 0
if k = x.key
  then s ← (x.left).sum
elseif k < x.key
  then s ← SumSmaller(x.left, k)
else
  s ← x.key + (x.left).sum + SumSmaller(x.right, k)
return s
סיבוכיות
Θ(h) — בכל רמה צעד אחד.
עדכון \(v.\text{sum}\) — דומה ל-size
Insert: צומת חדש \(x\) → \(x.\text{sum} = x.\text{key}\). הוסף \(x.\text{key}\) ל-\(v.\text{sum}\) של כל אב.
Delete: חסר את ה-key של הצומת הנמחק מ-\(v.\text{sum}\) של כל אב.

19. סיכום ונקודות מפתח

מבט קדימה — שאלות פתוחות
  • איך שומרים על איזון העץ? נדון בעצי AVL / Red-Black בהרצאות הבאות.
  • Augmented data נוסף: כל שדה שמתעדכן ב-\(O(h)\) מתאים — height, min/max, count של איברים בטווח, ועוד.