עץ בינארי הוא מבנה נתונים היררכי שבו לכל צומת יש לכל היותר שני ילדים: ילד שמאלי וילד ימני. בפרק זה נכיר את המחלקה <BinNode<T, נבין כיצד מייצגים עץ בינארי ונלמד פעולות בסיסיות כמו טרוורסות, הוספה וחיפוש.
הגדרת עץ בינארי
עץ בינארי הוא מבנה נתונים שבו כל צומת מחזיק:
- ערך
- מצביע Left
- מצביע Right
אם לצומת אין ילד שמאלי או ימני אז המצביע המתאים הוא null. בראש העץ נמצא צומת השורש (root), וכל צומת יכול להוות שורש של תת‑עץ.
מימוש מחלקת <BinNode<T
מימוש אפשרי עם תכונת C# אמיתיות. (אסור לשימוש)
ב‑#C ניתן היה לממש מחלקה גנרית עבור עץ בינארי באופן הבא:
public class BinNode<T>
{
public T Value { get; set; }
public BinNode<T> Left { get; set; }
public BinNode<T> Right { get; set; }
public BinNode(T value, BinNode<T> left = null, BinNode<T> right = null)
{
Value = value;
Left = left;
Right = right;
}
}
המימוש הרשמי שבו נשתמש הוא זה:
מימוש רשמי
אבל
public class BinNode<T>
{
private T value;
private BinNode<T> left;
private BinNode<T> right;
//-----------------------------------
//constructors
public BinNode(T value)
{
this.value = value;
this.left = null;
this.right = null;
}
public BinNode(BinNode<T> left, T value, BinNode<T> right)
{
this.value = value;
this.left = left;
this.right = right;
}
//-----------------------------------
//getters
public T GetValue()
{
return value;
}
public BinNode<T> GetLeft()
{
return this.left;
}
public BinNode<T> GetRight()
{
return this.right;
}
//-----------------------------------
//setters
public void SetValue(T value)
{
this.value = value;
}
public void SetLeft(BinNode<T> left)
{
this.left = left;
}
public void SetRight(BinNode<T> right)
{
this.right = right;
}
//-----------------------------------
//returns true if this.left is not null, else returns false
public bool HasLeft()
{
return (this.left != null);
}
//returns true if this.right is not null, else returns false
public bool HasRight()
{
return (this.right != null);
}
//-----------------------------------
//ToString
public override string ToString()
{
return "(" + left + " " + value + " " + right + ")";
}
}
ניתן לייצר עץ בינארי על ידי יצירת צמתים וחיבורם לילדי שמאל וימין. היעדר ילד מסומן בערך null, ונקרא עלה
טרוורסות בסיסיות
קיימות שלוש דרכי מעבר עיקריות על עץ בינארי:
| אלגוריתם | שם הטרוורסה | Order |
|---|---|---|
| בקר בצומת ⟵ עבור לעץ השמאלי ⟵ עבור לעץ הימני | Preorder | root, left, right |
| בקר בעץ השמאלי ⟵ בקר בצומת ⟵ בקר בעץ הימני | Inorder | left, root, right |
| בקר בעץ השמאלי ⟵ בקר בעץ הימני ⟵ בקר בצומת | Postorder | left, right, root |
// טרוורסה Inorder ב‑#C
public static void Inorder<T>(BinNode<T> root)
{
if (root == null)
return;
Inorder(root.GetLeft());
Console.WriteLine(root.GetValue());
Inorder(root.GetRight());
}
הקוד מדגים טרוורסה inorder: הוא עובר קודם לתת‑עץ שמאלי, מדפיס את הערך של הצומת ואז עובר לתת‑עץ ימני.
דיאגרמה – עץ בינארי
בדוגמה זו כל צומת מחזיק ערך מספרי. צמתים משמאל קטנים מהשורש, וצמתים מימין גדולים – תכונה אופיינית לעצים בינאריים של חיפוש.
פעולות שלא קיימות אך נחמד לכתוב עבור BinNode
| תפקיד | Method |
|---|---|
| מוסיף ערך חדש לעץ תוך שמירה על סדר (בעץ חיפוש בינארי) | Insert(BinNode<T> root, T value) |
| מחפש ערך בעץ ומחזיר אמת אם נמצא | bool Contains(BinNode<T> root, T value) |
| טרוורסה preorder – הדפסת הערכים בסדר root,left,right | void Preorder(BinNode<T> root) |
| טרוורסה inorder – הדפסת הערכים בסדר left,root,right | void Inorder(BinNode<T> root) |
| טרוורסה postorder – הדפסת הערכים בסדר left,right,root | void Postorder(BinNode<T> root) |
| מחזיר את גובה העץ (מספר הקצוות הארוך ביותר מהשורש לעלים) | int Height(BinNode<T> root) |
דגשים לעבודה עם עצים
א. עצים בינאריים של חיפוש דורשים שסדר ההכנסה ישמור על כך שכל ערך בצד השמאלי קטן מהשורש וכל ערך בצד הימני גדול ממנו.
ב. גובה עץ מאוזן הוא בערך \(log_2n\), ולכן פעולות חיפוש והכנסה בעץ מאוזן הן \(O(log n)\). אם העץ אינו מאוזן, פעולות אלו עלולות להתדרדר ל‑\(O(n)\).
ג. ניתן להשתמש במחסנית או תור כדי לבצע טרוורסות בצורה איטרטיבית. לא בטוח למה שמישהו יעשה כזה דבר
טרוורסה איטרטיבית (בלי רקורסיה)
נראה כמה סוגים של טרוורסה איטרטיבית (בלי רקורסיה). למרות כל הדוגמאות - עדיף ברקורסיה.
-
טרוורסה ברוחב (Level-Order / BFS) בעזרת Queue הרעיון: שמים את השורש בתור, ואז כל פעם מוציאים חוליה מהתור, מדפיסים את הערך שלה, ומכניסים את ילדיה (שמאל, ימין) לסוף התור.
public static void BfsTraverse<T>(BinNode<T> root) { if (root == null) return; Queue<BinNode<T>> q = new Queue<BinNode<T>>(); q.Insert(root); // מכניסים את השורש לתור while (!q.IsEmpty()) { BinNode<T> node = q.Remove(); // מוציאים חוליה מהתור Console.WriteLine(node.GetValue()); if (node.HasLeft()) q.Insert(node.GetLeft()); // מכניסים בן שמאל if (node.HasRight()) q.Insert(node.GetRight()); // מכניסים בן ימין } }זה עובר על העץ לפי שכבות: קודם שורש, אחר כך כל הילדים שלו, אחר כך הנכדים, וכו’.
-
טרוורסה בעומק (DFS Pre-Order) בעזרת Stack
כאן נעשה Pre-Order (שורש → שמאל → ימין), אבל איטרטיבי.
הרעיון: שמים את השורש במחסנית; כל פעם מוציאים חוליה מהמחסנית, מדפיסים אותה, ואז דוחפים קודם את הימני ואחר כך את השמאלי – כדי שהשמאלי ייצא ראשון.
public static void PreOrderIterative<T>(BinNode<T> root) { if (root == null) return; Stack<BinNode<T>> s = new Stack<BinNode<T>>(); s.Push(root); while (!s.IsEmpty()) { BinNode<T> node = s.Pop(); Console.WriteLine(node.GetValue()); // "שורש" if (node.HasRight()) s.Push(node.GetRight()); // דוחפים קודם ימין if (node.HasLeft()) s.Push(node.GetLeft()); // ואז שמאל – ייצא ראשון } } -
טרוורסה In-Order איטרטיבית בעזרת Stack
In-Order קלאסי (שמאל → שורש → ימין):
public static void InOrderIterative<T>(BinNode<T> root) { Stack<BinNode<T>> s = new Stack<BinNode<T>>(); BinNode<T> curr = root; while (curr != null || !s.IsEmpty()) { while (curr != null) { s.Push(curr); curr = curr.GetLeft(); // יורדים שמאלה עד הסוף } curr = s.Pop(); Console.WriteLine(curr.GetValue()); // שורש curr = curr.GetRight(); // עוברים לתת־עץ ימני } }
תרגול וקישורים
ממש אל תנסו לממש פונקציות המחדירות ערך לעץ בינארי ושמירתו כעץ חיפוש. נסו לכתוב פונקציה שמחזירה את מספר העלים בעץ. תרגילים נוספים תוכלו למצוא במערכת ההגשות