العربية

استكشف أساسيات أشجار البحث الثنائية (BSTs) وتعلم كيفية تنفيذها بكفاءة في جافاسكريبت. يغطي هذا الدليل بنية وعمليات BST وأمثلة عملية للمطورين في جميع أنحاء العالم.

أشجار البحث الثنائية: دليل التنفيذ الشامل في جافاسكريبت

أشجار البحث الثنائية (BSTs) هي بنية بيانات أساسية في علوم الحاسب، وتستخدم على نطاق واسع للبحث الفعال عن البيانات وفرزها واسترجاعها. يسمح هيكلها الهرمي بتعقيد زمني لوغاريتمي في العديد من العمليات، مما يجعلها أداة قوية لإدارة مجموعات البيانات الكبيرة. يقدم هذا الدليل نظرة شاملة على أشجار البحث الثنائية ويوضح تنفيذها في جافاسكريبت، وهو موجه للمطورين في جميع أنحاء العالم.

فهم أشجار البحث الثنائية

ما هي شجرة البحث الثنائية؟

شجرة البحث الثنائية هي بنية بيانات قائمة على الشجرة حيث يكون لكل عقدة طفلان على الأكثر، يشار إليهما بالطفل الأيسر والطفل الأيمن. الخاصية الرئيسية لشجرة البحث الثنائية هي أنه لأي عقدة معينة:

تضمن هذه الخاصية أن العناصر في شجرة البحث الثنائية مرتبة دائمًا، مما يتيح البحث والاسترجاع بكفاءة.

المفاهيم الأساسية

تنفيذ شجرة بحث ثنائية في جافاسكريبت

تعريف فئة العقدة (Node Class)

أولاً، نحدد فئة `Node` لتمثيل كل عقدة في شجرة البحث الثنائية. ستحتوي كل عقدة على `key` لتخزين البيانات ومؤشري `left` و `right` إلى أطفالها.


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

تعريف فئة شجرة البحث الثنائية (Binary Search Tree Class)

بعد ذلك، نحدد فئة `BinarySearchTree`. ستحتوي هذه الفئة على العقدة الجذرية وطرق لإدراج العقد والبحث عنها وحذفها واجتياز الشجرة.


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

  // Methods will be added here
}

الإدراج (Insertion)

تضيف طريقة `insert` عقدة جديدة بالمفتاح المحدد إلى شجرة البحث الثنائية. تحافظ عملية الإدراج على خاصية 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);
    }
  }
}

مثال: إدراج قيم في شجرة البحث الثنائية


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);

البحث (Searching)

تتحقق طريقة `search` مما إذا كانت هناك عقدة بالمفتاح المحدد موجودة في شجرة البحث الثنائية. إنها تجتاز الشجرة، وتقارن المفتاح بمفتاح العقدة الحالية وتنتقل إلى الشجرة الفرعية اليسرى أو اليمنى وفقًا لذلك.


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;
  }
}

مثال: البحث عن قيمة في شجرة البحث الثنائية


console.log(bst.search(9));  // Output: true
console.log(bst.search(2));  // Output: false

الحذف (Deletion)

تحذف طريقة `remove` عقدة بالمفتاح المحدد من شجرة البحث الثنائية. هذه هي العملية الأكثر تعقيدًا لأنها تحتاج إلى الحفاظ على خاصية 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 {
    // key is equal to node.key

    // case 1 - a leaf node
    if (node.left === null && node.right === null) {
      node = null;
      return node;
    }

    // case 2 - node has only 1 child
    if (node.left === null) {
      node = node.right;
      return node;
    } else if (node.right === null) {
      node = node.left;
      return node;
    }

    // case 3 - node has 2 children
    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.remove(7);
console.log(bst.search(7)); // Output: false

اجتياز الشجرة (Tree Traversal)

يتضمن اجتياز الشجرة زيارة كل عقدة في الشجرة بترتيب معين. هناك العديد من طرق الاجتياز الشائعة:


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 printNode = (value) => console.log(value);

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

القيم الدنيا والقصوى

يعد العثور على الحد الأدنى والحد الأقصى للقيم في شجرة البحث الثنائية أمرًا بسيطًا، بفضل طبيعتها المرتبة.


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); // Output: 3
console.log(bst.max().key); // Output: 25

التطبيقات العملية لأشجار البحث الثنائية

تستخدم أشجار البحث الثنائية في مجموعة متنوعة من التطبيقات، بما في ذلك:

اعتبارات الأداء

يعتمد أداء شجرة البحث الثنائية على هيكلها. في أفضل الحالات، تسمح شجرة البحث الثنائية المتوازنة بتعقيد زمني لوغاريتمي لعمليات الإدراج والبحث والحذف. ومع ذلك، في أسوأ الحالات (على سبيل المثال، شجرة منحرفة)، يمكن أن يتدهور التعقيد الزمني إلى وقت خطي.

الأشجار المتوازنة مقابل الأشجار غير المتوازنة

شجرة البحث الثنائية المتوازنة هي التي يختلف فيها ارتفاع الشجرتين الفرعيتين اليسرى واليمنى لكل عقدة بواحد على الأكثر. تضمن خوارزميات الموازنة الذاتية، مثل أشجار AVL وأشجار الأحمر-الأسود، أن تظل الشجرة متوازنة، مما يوفر أداءً ثابتًا. قد تتطلب المناطق المختلفة مستويات تحسين مختلفة بناءً على الحمل على الخادم؛ تساعد الموازنة في الحفاظ على الأداء في ظل الاستخدام العالمي المرتفع.

التعقيد الزمني (Time Complexity)

مفاهيم متقدمة في أشجار البحث الثنائية

الأشجار ذاتية التوازن

الأشجار ذاتية التوازن هي أشجار بحث ثنائية تقوم تلقائيًا بضبط هيكلها للحفاظ على التوازن. هذا يضمن أن ارتفاع الشجرة يظل لوغاريتميًا، مما يوفر أداءً ثابتًا لجميع العمليات. تشمل الأشجار ذاتية التوازن الشائعة أشجار AVL وأشجار الأحمر-الأسود.

أشجار AVL

تحافظ أشجار AVL على التوازن من خلال ضمان أن فرق الارتفاع بين الشجرتين الفرعيتين اليسرى واليمنى لأي عقدة هو واحد على الأكثر. عندما يتعطل هذا التوازن، يتم إجراء عمليات دوران لاستعادة التوازن.

أشجار الأحمر-الأسود

تستخدم أشجار الأحمر-الأسود خصائص اللون (أحمر أو أسود) للحفاظ على التوازن. إنها أكثر تعقيدًا من أشجار AVL ولكنها تقدم أداءً أفضل في سيناريوهات معينة.

مثال كود جافاسكريبت: تنفيذ كامل لشجرة البحث الثنائية


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 {
      // key is equal to node.key

      // case 1 - a leaf node
      if (node.left === null && node.right === null) {
        node = null;
        return node;
      }

      // case 2 - node has only 1 child
      if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
        return node;
      }

      // case 3 - node has 2 children
      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("اجتياز بالترتيب الداخلي:");
bst.inOrderTraverse(printNode);

console.log("اجتياز بالترتيب المسبق:");
bst.preOrderTraverse(printNode);

console.log("اجتياز بالترتيب اللاحق:");
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));

الخاتمة

أشجار البحث الثنائية هي بنية بيانات قوية ومتعددة الاستخدامات ولها العديد من التطبيقات. قدم هذا الدليل نظرة شاملة على أشجار البحث الثنائية، حيث غطى هيكلها وعملياتها وتنفيذها في جافاسكريبت. من خلال فهم المبادئ والتقنيات التي نوقشت في هذا الدليل، يمكن للمطورين في جميع أنحاء العالم استخدام أشجار البحث الثنائية بفعالية لحل مجموعة واسعة من المشاكل في تطوير البرمجيات. من إدارة قواعد البيانات العالمية إلى تحسين خوارزميات البحث، تعد معرفة أشجار البحث الثنائية رصيدًا لا يقدر بثمن لأي مبرمج.

بينما تواصل رحلتك في علوم الحاسب، فإن استكشاف المفاهيم المتقدمة مثل الأشجار ذاتية التوازن وتطبيقاتها المختلفة سيعزز فهمك وقدراتك. استمر في الممارسة والتجربة مع سيناريوهات مختلفة لإتقان فن استخدام أشجار البحث الثنائية بفعالية.