עץ
(אמרנו שבמקרה הגרוע עץ בינארי יכול להיות פשוט רשימה מקושרת ואז זמן החיפוש יהיה
אנחנו נלמד על סוגים של עצים מאוזנים בהמשך , אבל בגדול מטרתם כמו
המימוש של
תזכורת : גובה העץ הוא בעצם מספר הרמות בעץ פחות 1 (לכן עבור קודקוד בלי ילדים גובה העץ הוא 0 ולא 1, אפשר להגדיר את זה גם כמרחק הגדול ביותר בין עלה לשורש, מרחק בעץ = מספר הקשתות בין קודקוד מסויים לשורש).
הגדרת עץ AVL - עץ ייקרא עץ AVL אם הוא עץ בינארי ולכל קודקוד בעץ ההפרש בין גובה תת העץ הימני לתת העץ השמאלי בערך מוחלט הוא לכל היותר אחד.
הבחנה אם בעץ
דוגמאות לעץ

דוגמה לעץ לא מאוזן :

משפט : העומק של עץ
איך בכלל מוכיחים דבר כזה? אולי אינדוקציה על מספר הקודקודים? הבעיה היא שגובה העץ הוא מספר טבעי אבל הפעלת לוג על מספר הקודקודים לא תיתן לנו מספר טבעי ולכן אנחנו עלולים לקבל מספרים לא נכונים על ידי שימוש באינדוקצייה למשל על 1000 ו 1001 קודקודים נקבל את אותו גובה
כשרוצים להוכיח נכונות של פרמטר מסויים הרבה פעמים קל להוכיח את הפונקצייה ההפיכה על הפרמטר הנגדי בוא משתמשים
למת עזר : עץ
כלומר יש יחס אקספוננציאלי בין מספר הקודקודים לעומק העץ .

כלומר יש פונקצייה הפיכה בין השניים .
בגלל שזאת פונקצייה הפיכה בין תכונות העץ נוכל להסתכל על הבעיה מזווית אחרת ולהוכיח את למת העזר מה שיגרור באופן מיידי את הטענה המרכזית שלנו.
כעת, על הפרמטר הזה כן נוכל להוכיח באינדוקצייה .
נגדיר
בסיס עבור
עבור
עבור
זה נראה כאילו היחס הוא
באופן כללי, עבור עץ
סה״כ כעת אנחנו יודעים שלעץ מינימלי מספר הקודקודים יקיים
אנדקדוטה נסתכל על סדרת פיבונצי
ישנו מונח הנקרא יחס הזהב שאומר שכל שני איברים עוקבים בסדרה שואפים אליו , יחס זה הוא
ומתקיים
בגלל שהשורש השלישי קטן מאחד כשנעלה בחזקה גבוהה במיוחד הוא יהיה זניח (חשוב להמשך).
לא אפרט יותר מדי על המשך ההוכחה אך הקשר בין יש יחס בין עץ avl מינימלי בגובה

נשתמש בזה כדי לתאר קשר כללי :
וכעת מהקירוב הנ״ל
כאן דיברנו על עץ avl מינימלי ויתקיים שבמקרה הגרוע עומק של עץ סך הכל גדול יותר ב
עכשיו כשאנחנו יודע שההגבלות על עץ
אנחנו נרצה לדעת יותר מידע על כל קודקוד בעץ ולשם כך נגדיר פרמטר חדש לקודקוד . נקרא לו
במילים זה ההפרש בין גובה תת העץ הימני לגובה תת העץ השמאלי ואנחנו יודעים
מהגדרת עץ

נגדיר שאם המאזן הוא
כמו כן נניח שעצי
כיוון שעץ
במילים אחרות , נרצה לבדוק את המאזן על הקודקודים שבמסלול ההכנסה.
נשאלת השאלה איך אנחנו נסרוק שגיאות לאחר שהגענו לקודקוד האחרון לפני הכנסה בעץ בינארי? הרי לקודקוד אין רפרנס לאביו בעץ... והתשובה פשוטה, נכניס את הקודקודים במסלול ההכנסה ל מחסנית
(נשים לב, מבחינת מימוש בקוד לא בהכרח חייב להשתמש במחסנית, ניתן לממש את הקוד באופן ריקורסיבי כך שברגע שהכנסה מסתיימת נעלה למעלה בהירכייה ונקבל בחינם את המצביעים שעברנו במעלה הדרך , הכל תלוי באיך מימשנו הכנסה של עץ חיפוש בינארי) .
משיטת ההכנסה של #עץ חיפוש בינארי אנחנו יודעים שהקודקוד שייכנס תמיד יהיה עלה. יתרה מכך, אם המאזן של קודקוד האבא שלו הוא 0 סימן שלפני ההכנסה אותו קודקוד אב היה עלה בעצמו.
נסמן את קודקוד האב כ
כעת נמשיך לעלות במחסנית ששם נשמר הנתיב שממנו באנו ,

מסתבר, שהפעולות איזון שצריך לבצע שונות בהתאם לאיזה מתתי העץ של
לצערנו הרב, נצטרך לעשות zoom-in פנימה לתוך העץ
__לפני שנחלק למקרים, אנחנו יודעים להגיב בוודאות ש המאזן לפני הכנסה של
על מנת לסדר את האיזון נצטרך להבין עם איזה מקרה של ״שבירה״ אנחנו מתמודדים, היוצרים של


__את שני המקרים האלו ניתן לפתור על ידי רוטציה בין השורש לבין בנו הימני אם אנחנו ב RR , או בנו השמאלי במקרה שאנחנו ב LL באופן הבא:
לדוגמה , אם
נשמע קצת מבלבל, נדגים ויזואלית
LL :

RR :

נשים לב שתמיד אנחנו נרצה לאזן את העץ ולשחק עם המשקלים, כלומר תמיד תהיה החלפה של השורש באיזשהו אופן .


שימו לב שזה לא משנה איפה בתת עץ של תת העץ הגדול יותר מכניסים את האיבר כי אין שם שבירה בשני המקרים
את שני המקרים הללו ניתן לפתור על ידי רוטציה כפולה. הרוטצייה תעבוד ככה :
לדוגמא :

__באופן דומה עושים על
ולסיכום
ברגע שהכנסו קודקוד חדש בהכנסה כמו בעץ חיפוש רגיל, עולה במסלול שלנו ובודקים את המאזן , אם הוא
// An AVL tree node
class Node
{
public:
int key;
Node *left;
Node *right;
int height;
};
// A utility function to get the
// height of the tree
int height(Node *N)
{
if (N == NULL)
return 0;
return N->height;
}
/* Helper function that allocates a
new node with the given key and
NULL left and right pointers. */
Node* newNode(int key)
{
Node* node = new Node();
node->key = key;
node->left = NULL;
node->right = NULL;
node->height = 1; // new node is initially
// added at leaf
return(node);
}
// A utility function to right
// rotate subtree rooted with y
Node *rightRotate(Node *y)
{
Node *x = y->left;
Node *T2 = x->right;
// Perform rotation
x->right = y;
y->left = T2;
// Update heights
y->height = max(height(y->left),
height(y->right)) + 1;
x->height = max(height(x->left),
height(x->right)) + 1;
// Return new root
return x;
}
// A utility function to left
// rotate subtree rooted with x
Node *leftRotate(Node *x)
{
Node *y = x->right;
Node *T2 = y->left;
// Perform rotation
y->left = x;
x->right = T2;
// Update heights
x->height = max(height(x->left),
height(x->right)) + 1;
y->height = max(height(y->left),
height(y->right)) + 1;
// Return new root
return y;
}
// Get Balance factor of node N
int getBalance(Node *N)
{
if (N == NULL)
return 0;
return height(N->left) - height(N->right);
}
// Recursive function to insert a key
// in the subtree rooted with node and
// returns the new root of the subtree.
Node* insert(Node* node, int key)
{
/* 1. Perform the normal BST insertion */
if (node == NULL)
return(newNode(key));
if (key < node->key)
node->left = insert(node->left, key);
else if (key > node->key)
node->right = insert(node->right, key);
else // Equal keys are not allowed in BST
return node;
/* 2. Update height of this ancestor node */
node->height = 1 + max(height(node->left),
height(node->right));
/* 3. Get the balance factor of this ancestor
node to check whether this node became
unbalanced */
int balance = getBalance(node);
// If this node becomes unbalanced, then
// there are 4 cases
// Left Left Case
if (balance > 1 && key < node->left->key)
return rightRotate(node);
// Right Right Case
if (balance < -1 && key > node->right->key)
return leftRotate(node);
// Left Right Case
if (balance > 1 && key > node->left->key)
{
node->left = leftRotate(node->left);
return rightRotate(node);
}
// Right Left Case
if (balance < -1 && key < node->right->key)
{
node->right = rightRotate(node->right);
return leftRotate(node);
}
/* return the (unchanged) node pointer */
return node;
}
סיבוכיות הכנסה חסומה בגובה העץ שבמקרה הזה הוא חסום בלוג של מספר האיברים, כמו כן הפעולות היו נשמעות מורכבות אבל אפשר לראות בקוד שאלו פעולות הנעשות בזמן ריצה קבוע . לכן סך הכל פעולת ההכנסה חסומה ב
כמו שדיברנו על #מחיקה בעץ חיפוש, זאת לא בהכרח פעולה סימטרית להכנסה כשמדובר בעץ. הכנסה תמיד מכניסה עלה אבל לא תמיד נרצה למחוק עלה.
נניח שנרצה למחוק קודקוד
כעת צריך לבדוק את המאזן בכל קודקוד במסלול ממנו באנו ולעדכן בהתאם, לא אפרט פה את הליך הבדיקה כי הוא זהה לנ״ל. ההבדל הוא שיש מקרים שמחיקת איברים תשנה את תהפוך עץ ממצב לא מאוזן תקין למצב מאוזן תקין , דבר שיגרום לגובה העץ לקטון ב 1 ולכן במצב זה צריך לבדוק גם את הקודקוד הבא ולא לעצור כמו שהיינו עושים בהכנסה.
זה סך הכל ההבדל אך בדיקת מקדם האיזון וביצוע הרוטציות יהיה בידיוק אותו דבר. מבחינת פתרון ריקורסיבי זה סך הכל להוסיף עוד 2 שורות לאחר פעולת המחיקה .
לסיכום קצר, העקרון של הוצאה דומה להכנסה במקרה הזה, בדיקה המסלול מלמטה למעלה, אך תנאי העצירה מעט שונים , אם הקטנו את גובה העץ ב

הפתרון הריקורסיבי קצת פחות יעיל , למרות שחסומים באותו דבר, נבצע עוד פעולות במצב הריקורסיבי כי אין את תנאי העצירה שדיברתי עליהם, עם זאת, לפעמים עדיף קוד קריא מאשר קוד יעיל, בטח ובטח כשהפעולות חסומות באותו big O .
__גם זמן הריצה של מחיקה הוא