עברית

גלו את יסודות עצי החיפוש הבינאריים (BST) ולמדו לממש אותם ביעילות ב-JavaScript. המדריך כולל מבנה, פעולות ודוגמאות מעשיות למפתחים ברחבי העולם.

עצי חיפוש בינאריים: מדריך מימוש מקיף ב-JavaScript

עצי חיפוש בינאריים (BSTs) הם מבנה נתונים יסודי במדעי המחשב, הנמצא בשימוש נרחב לחיפוש, מיון ואחזור נתונים יעיל. המבנה ההיררכי שלהם מאפשר סיבוכיות זמן לוגריתמית בפעולות רבות, מה שהופך אותם לכלי רב עוצמה לניהול מערכי נתונים גדולים. מדריך זה מספק סקירה מקיפה של BST ומדגים את מימושם ב-JavaScript, ופונה למפתחים ברחבי העולם.

הבנת עצי חיפוש בינאריים

מהו עץ חיפוש בינארי?

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

תכונה זו מבטיחה שהאלמנטים ב-BST תמיד מסודרים, ומאפשרת חיפוש ואחזור יעילים.

מושגי מפתח

מימוש עץ חיפוש בינארי ב-JavaScript

הגדרת מחלקת הצומת (Node)

ראשית, נגדיר מחלקת `Node` כדי לייצג כל צומת ב-BST. כל צומת יכיל `key` לאחסון הנתונים ומצביעי `left` ו-`right` לילדיו.


class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

הגדרת מחלקת עץ החיפוש הבינארי

לאחר מכן, נגדיר את מחלקת `BinarySearchTree`. מחלקה זו תכיל את צומת השורש ומתודות להכנסה, חיפוש, מחיקה וסריקה של העץ.


class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  // מתודות יתווספו כאן
}

הכנסה

מתודת `insert` מוסיפה צומת חדש עם המפתח הנתון ל-BST. תהליך ההכנסה שומר על תכונת ה-BST על ידי הצבת הצומת החדש במיקום המתאים ביחס לצמתים קיימים.


insert(key) {
  const newNode = new Node(key);

  if (this.root === null) {
    this.root = newNode;
  } else {
    this.insertNode(this.root, newNode);
  }
}

insertNode(node, newNode) {
  if (newNode.key < node.key) {
    if (node.left === null) {
      node.left = newNode;
    } else {
      this.insertNode(node.left, newNode);
    }
  } else {
    if (node.right === null) {
      node.right = newNode;
    } else {
      this.insertNode(node.right, newNode);
    }
  }
}

דוגמה: הכנסת ערכים ל-BST


const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);

חיפוש

מתודת `search` בודקת אם קיים צומת עם המפתח הנתון ב-BST. היא סורקת את העץ, משווה את המפתח עם מפתח הצומת הנוכחי ועוברת לתת-העץ השמאלי או הימני בהתאם.


search(key) {
  return this.searchNode(this.root, key);
}

searchNode(node, key) {
  if (node === null) {
    return false;
  }

  if (key < node.key) {
    return this.searchNode(node.left, key);
  } else if (key > node.key) {
    return this.searchNode(node.right, key);
  } else {
    return true;
  }
}

דוגמה: חיפוש ערך ב-BST


console.log(bst.search(9));  // פלט: true
console.log(bst.search(2));  // פלט: false

מחיקה

מתודת `remove` מוחקת צומת עם המפתח הנתון מה-BST. זוהי הפעולה המורכבת ביותר מכיוון שהיא צריכה לשמור על תכונת ה-BST תוך כדי הסרת הצומת. ישנם שלושה מקרים שיש לשקול:


remove(key) {
  this.root = this.removeNode(this.root, key);
}

removeNode(node, key) {
  if (node === null) {
    return null;
  }

  if (key < node.key) {
    node.left = this.removeNode(node.left, key);
    return node;
  } else if (key > node.key) {
    node.right = this.removeNode(node.right, key);
    return node;
  } else {
    // המפתח שווה למפתח הצומת

    // מקרה 1 - צומת עלה
    if (node.left === null && node.right === null) {
      node = null;
      return node;
    }

    // מקרה 2 - לצומת יש ילד אחד בלבד
    if (node.left === null) {
      node = node.right;
      return node;
    } else if (node.right === null) {
      node = node.left;
      return node;
    }

    // מקרה 3 - לצומת יש 2 ילדים
    const aux = this.findMinNode(node.right);
    node.key = aux.key;
    node.right = this.removeNode(node.right, aux.key);
    return node;
  }
}

findMinNode(node) {
  let current = node;
  while (current != null && current.left != null) {
    current = current.left;
  }
  return current;
}

דוגמה: הסרת ערך מה-BST


bst.remove(7);
console.log(bst.search(7)); // פלט: false

סריקת עצים

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


inOrderTraverse(callback) {
  this.inOrderTraverseNode(this.root, callback);
}

inOrderTraverseNode(node, callback) {
  if (node !== null) {
    this.inOrderTraverseNode(node.left, callback);
    callback(node.key);
    this.inOrderTraverseNode(node.right, callback);
  }
}

preOrderTraverse(callback) {
  this.preOrderTraverseNode(this.root, callback);
}

preOrderTraverseNode(node, callback) {
  if (node !== null) {
    callback(node.key);
    this.preOrderTraverseNode(node.left, callback);
    this.preOrderTraverseNode(node.right, callback);
  }
}

postOrderTraverse(callback) {
  this.postOrderTraverseNode(this.root, callback);
}

postOrderTraverseNode(node, callback) {
  if (node !== null) {
    this.postOrderTraverseNode(node.left, callback);
    this.postOrderTraverseNode(node.right, callback);
    callback(node.key);
  }
}

דוגמה: סריקת ה-BST


const printNode = (value) => console.log(value);

bst.inOrderTraverse(printNode);   // פלט: 3 5 8 9 10 11 12 13 14 15 18 20 25
bst.preOrderTraverse(printNode);  // פלט: 11 5 3 9 8 10 15 13 12 14 20 18 25
bst.postOrderTraverse(printNode); // פלט: 3 8 10 9 12 14 13 18 25 20 15 11

ערכים מינימליים ומקסימליים

מציאת הערכים המינימליים והמקסימליים ב-BST היא פשוטה, הודות למבנה המסודר שלו.


min() {
  return this.minNode(this.root);
}

minNode(node) {
  let current = node;
  while (current !== null && current.left !== null) {
    current = current.left;
  }
  return current;
}

max() {
  return this.maxNode(this.root);
}

maxNode(node) {
  let current = node;
  while (current !== null && current.right !== null) {
    current = current.right;
  }
  return current;
}

דוגמה: מציאת ערכים מינימליים ומקסימליים


console.log(bst.min().key); // פלט: 3
console.log(bst.max().key); // פלט: 25

יישומים מעשיים של עצי חיפוש בינאריים

עצי חיפוש בינאריים משמשים במגוון רחב של יישומים, כולל:

שיקולי ביצועים

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

עצים מאוזנים לעומת עצים לא מאוזנים

עץ BST מאוזן הוא עץ שבו גובה תתי-העצים השמאלי והימני של כל צומת נבדל בלכל היותר אחד. אלגוריתמים לאיזון-עצמי, כגון עצי AVL ועצי אדום-שחור, מבטיחים שהעץ נשאר מאוזן, ומספקים ביצועים עקביים. אזורים שונים עשויים לדרוש רמות אופטימיזציה שונות בהתבסס על העומס על השרת; איזון מסייע לשמור על ביצועים תחת שימוש גלובלי גבוה.

סיבוכיות זמן

מושגי BST מתקדמים

עצים בעלי איזון-עצמי

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

עצי AVL

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

עצי אדום-שחור

עצי אדום-שחור משתמשים בתכונות צבע (אדום או שחור) כדי לשמור על איזון. הם מורכבים יותר מעצי AVL אך מציעים ביצועים טובים יותר בתרחישים מסוימים.

דוגמת קוד JavaScript: מימוש מלא של עץ חיפוש בינארי


class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  insert(key) {
    const newNode = new Node(key);

    if (this.root === null) {
      this.root = newNode;
    } else {
      this.insertNode(this.root, newNode);
    }
  }

  insertNode(node, newNode) {
    if (newNode.key < node.key) {
      if (node.left === null) {
        node.left = newNode;
      } else {
        this.insertNode(node.left, newNode);
      }
    } else {
      if (node.right === null) {
        node.right = newNode;
      } else {
        this.insertNode(node.right, newNode);
      }
    }
  }

  search(key) {
    return this.searchNode(this.root, key);
  }

  searchNode(node, key) {
    if (node === null) {
      return false;
    }

    if (key < node.key) {
      return this.searchNode(node.left, key);
    } else if (key > node.key) {
      return this.searchNode(node.right, key);
    } else {
      return true;
    }
  }

  remove(key) {
    this.root = this.removeNode(this.root, key);
  }

  removeNode(node, key) {
    if (node === null) {
      return null;
    }

    if (key < node.key) {
      node.left = this.removeNode(node.left, key);
      return node;
    } else if (key > node.key) {
      node.right = this.removeNode(node.right, key);
      return node;
    } else {
      // המפתח שווה למפתח הצומת

      // מקרה 1 - צומת עלה
      if (node.left === null && node.right === null) {
        node = null;
        return node;
      }

      // מקרה 2 - לצומת יש ילד אחד בלבד
      if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
        return node;
      }

      // מקרה 3 - לצומת יש 2 ילדים
      const aux = this.findMinNode(node.right);
      node.key = aux.key;
      node.right = this.removeNode(node.right, aux.key);
      return node;
    }
  }

  findMinNode(node) {
    let current = node;
    while (current != null && current.left != null) {
      current = current.left;
    }
    return current;
  }

  min() {
    return this.minNode(this.root);
  }

  minNode(node) {
    let current = node;
    while (current !== null && current.left !== null) {
      current = current.left;
    }
    return current;
  }

  max() {
    return this.maxNode(this.root);
  }

  maxNode(node) {
    let current = node;
    while (current !== null && current.right !== null) {
      current = current.right;
    }
    return current;
  }

  inOrderTraverse(callback) {
    this.inOrderTraverseNode(this.root, callback);
  }

  inOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.inOrderTraverseNode(node.left, callback);
      callback(node.key);
      this.inOrderTraverseNode(node.right, callback);
    }
  }

  preOrderTraverse(callback) {
    this.preOrderTraverseNode(this.root, callback);
  }

  preOrderTraverseNode(node, callback) {
    if (node !== null) {
      callback(node.key);
      this.preOrderTraverseNode(node.left, callback);
      this.preOrderTraverseNode(node.right, callback);
    }
  }

  postOrderTraverse(callback) {
    this.postOrderTraverseNode(this.root, callback);
  }

  postOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.postOrderTraverseNode(node.left, callback);
      this.postOrderTraverseNode(node.right, callback);
      callback(node.key);
    }
  }
}

// דוגמת שימוש
const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);

const printNode = (value) => console.log(value);

console.log("סריקה תוכית (In-order):");
bst.inOrderTraverse(printNode);

console.log("סריקה קדמית (Pre-order):");
bst.preOrderTraverse(printNode);

console.log("סריקה סופית (Post-order):");
bst.postOrderTraverse(printNode);

console.log("ערך מינימלי:", bst.min().key);
console.log("ערך מקסימלי:", bst.max().key);

console.log("חיפוש אחר 9:", bst.search(9));
console.log("חיפוש אחר 2:", bst.search(2));

bst.remove(7);
console.log("חיפוש אחר 7 לאחר הסרה:", bst.search(7));

סיכום

עצי חיפוש בינאריים הם מבנה נתונים חזק ורב-תכליתי עם יישומים רבים. מדריך זה סיפק סקירה מקיפה של BST, המכסה את המבנה שלהם, פעולות, ומימוש ב-JavaScript. על ידי הבנת העקרונות והטכניקות שנדונו במדריך זה, מפתחים ברחבי העולם יכולים להשתמש ביעילות ב-BST כדי לפתור מגוון רחב של בעיות בפיתוח תוכנה. מניהול מסדי נתונים גלובליים ועד לאופטימיזציה של אלגוריתמי חיפוש, הידע על BST הוא נכס שלא יסולא בפז עבור כל מתכנת.

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