فارسی

با اصول درختان جستجوی دودویی (BST) آشنا شوید و نحوه پیاده‌سازی کارآمد آن‌ها را در جاوااسکریپت بیاموزید. این راهنما ساختار، عملیات و مثال‌های کاربردی BST را پوشش می‌دهد.

درختان جستجوی دودویی: راهنمای جامع پیاده‌سازی در جاوااسکریپت

درختان جستجوی دودویی (BSTs) یک ساختار داده بنیادی در علوم کامپیوتر هستند که به طور گسترده برای جستجو، مرتب‌سازی و بازیابی کارآمد داده‌ها استفاده می‌شوند. ساختار سلسله مراتبی آن‌ها امکان پیچیدگی زمانی لگاریتمی را در بسیاری از عملیات فراهم می‌کند و آن‌ها را به ابزاری قدرتمند برای مدیریت مجموعه داده‌های بزرگ تبدیل می‌کند. این راهنما یک نمای کلی جامع از BST‌ها ارائه می‌دهد و پیاده‌سازی آن‌ها را در جاوااسکریپت، برای توسعه‌دهندگان در سراسر جهان، نمایش می‌دهد.

درک درختان جستجوی دودویی

درخت جستجوی دودویی چیست؟

درخت جستجوی دودویی یک ساختار داده مبتنی بر درخت است که در آن هر گره حداکثر دو فرزند دارد که به آنها فرزند چپ و فرزند راست گفته می‌شود. ویژگی کلیدی یک BST این است که برای هر گره معین:

این ویژگی تضمین می‌کند که عناصر در یک BST همیشه مرتب هستند و امکان جستجو و بازیابی کارآمد را فراهم می‌کند.

مفاهیم کلیدی

پیاده‌سازی درخت جستجوی دودویی در جاوااسکریپت

تعریف کلاس Node

ابتدا، کلاس `Node` را برای نمایش هر گره در BST تعریف می‌کنیم. هر گره شامل یک `key` برای ذخیره داده و اشاره‌گرهای `left` و `right` به فرزندان خود خواهد بود.


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

تعریف کلاس Binary Search Tree

سپس، کلاس `BinarySearchTree` را تعریف می‌کنیم. این کلاس شامل گره ریشه و متدهایی برای درج، جستجو، حذف و پیمایش درخت خواهد بود.


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

  // Methods will be added here
}

درج (Insertion)

متد `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);

جستجو (Searching)

متد `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));  // Output: true
console.log(bst.search(2));  // Output: false

حذف (Deletion)

متد `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 {
    // 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


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

مثال: پیمایش BST


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

مقادیر حداقل و حداکثر

پیدا کردن مقادیر حداقل و حداکثر در یک 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); // Output: 3
console.log(bst.max().key); // Output: 25

کاربردهای عملی درختان جستجوی دودویی

درختان جستجوی دودویی در انواع مختلفی از کاربردها استفاده می‌شوند، از جمله:

ملاحظات عملکردی

عملکرد یک BST به ساختار آن بستگی دارد. در بهترین حالت، یک BST متعادل امکان پیچیدگی زمانی لگاریتمی را برای عملیات درج، جستجو و حذف فراهم می‌کند. با این حال، در بدترین حالت (مثلاً یک درخت کج)، پیچیدگی زمانی می‌تواند به زمان خطی کاهش یابد.

درختان متعادل در مقابل نامتعادل

یک BST متعادل درختی است که در آن ارتفاع زیردرختان چپ و راست هر گره حداکثر یک واحد اختلاف داشته باشد. الگوریتم‌های خود-متعادل‌کننده، مانند درختان AVL و درختان قرمز-سیاه، تضمین می‌کنند که درخت متعادل باقی بماند و عملکرد ثابتی را ارائه دهد. مناطق مختلف ممکن است بر اساس بار روی سرور به سطوح بهینه‌سازی متفاوتی نیاز داشته باشند؛ متعادل‌سازی به حفظ عملکرد تحت استفاده بالای جهانی کمک می‌کند.

پیچیدگی زمانی

مفاهیم پیشرفته BST

درختان خود-متعادل‌کننده

درختان خود-متعادل‌کننده BSTهایی هستند که به طور خودکار ساختار خود را برای حفظ تعادل تنظیم می‌کنند. این امر تضمین می‌کند که ارتفاع درخت لگاریتمی باقی بماند و عملکرد ثابتی را برای همه عملیات فراهم کند. درختان خود-متعادل‌کننده رایج شامل درختان 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);
    }
  }
}

// Example Usage
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‌ها ارائه داده و ساختار، عملیات و پیاده‌سازی آن‌ها را در جاوااسکریپت پوشش داده است. با درک اصول و تکنیک‌های مورد بحث در این راهنما، توسعه‌دهندگان در سراسر جهان می‌توانند به طور مؤثر از BST‌ها برای حل طیف گسترده‌ای از مسائل در توسعه نرم‌افزار استفاده کنند. از مدیریت پایگاه‌های داده جهانی گرفته تا بهینه‌سازی الگوریتم‌های جستجو، دانش BST یک دارایی بی‌نظیر برای هر برنامه‌نویسی است.

همانطور که سفر خود را در علوم کامپیوتر ادامه می‌دهید، کاوش در مفاهیم پیشرفته‌تری مانند درختان خود-متعادل‌کننده و پیاده‌سازی‌های مختلف آنها، درک و توانایی‌های شما را بیشتر خواهد کرد. به تمرین و آزمایش با سناریوهای مختلف ادامه دهید تا در هنر استفاده مؤثر از درختان جستجوی دودویی مهارت پیدا کنید.