با اصول درختان جستجوی دودویی (BST) آشنا شوید و نحوه پیادهسازی کارآمد آنها را در جاوااسکریپت بیاموزید. این راهنما ساختار، عملیات و مثالهای کاربردی BST را پوشش میدهد.
درختان جستجوی دودویی: راهنمای جامع پیادهسازی در جاوااسکریپت
درختان جستجوی دودویی (BSTs) یک ساختار داده بنیادی در علوم کامپیوتر هستند که به طور گسترده برای جستجو، مرتبسازی و بازیابی کارآمد دادهها استفاده میشوند. ساختار سلسله مراتبی آنها امکان پیچیدگی زمانی لگاریتمی را در بسیاری از عملیات فراهم میکند و آنها را به ابزاری قدرتمند برای مدیریت مجموعه دادههای بزرگ تبدیل میکند. این راهنما یک نمای کلی جامع از BSTها ارائه میدهد و پیادهسازی آنها را در جاوااسکریپت، برای توسعهدهندگان در سراسر جهان، نمایش میدهد.
درک درختان جستجوی دودویی
درخت جستجوی دودویی چیست؟
درخت جستجوی دودویی یک ساختار داده مبتنی بر درخت است که در آن هر گره حداکثر دو فرزند دارد که به آنها فرزند چپ و فرزند راست گفته میشود. ویژگی کلیدی یک BST این است که برای هر گره معین:
- تمام گرهها در زیردرخت چپ دارای کلیدهایی کوچکتر از کلید گره هستند.
- تمام گرهها در زیردرخت راست دارای کلیدهایی بزرگتر از کلید گره هستند.
این ویژگی تضمین میکند که عناصر در یک BST همیشه مرتب هستند و امکان جستجو و بازیابی کارآمد را فراهم میکند.
مفاهیم کلیدی
- گره (Node): یک واحد اصلی در درخت، شامل یک کلید (داده) و اشارهگرهایی به فرزندان چپ و راست خود.
- ریشه (Root): بالاترین گره در درخت.
- برگ (Leaf): گرهای که هیچ فرزندی ندارد.
- زیردرخت (Subtree): بخشی از درخت که ریشه آن یک گره خاص است.
- ارتفاع (Height): طول طولانیترین مسیر از ریشه تا یک برگ.
- عمق (Depth): طول مسیر از ریشه تا یک گره خاص.
پیادهسازی درخت جستجوی دودویی در جاوااسکریپت
تعریف کلاس 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 را حفظ کند. سه حالت برای در نظر گرفتن وجود دارد:
- حالت ۱: گره مورد نظر برای حذف، یک گره برگ است. به سادگی آن را حذف کنید.
- حالت ۲: گره مورد نظر برای حذف یک فرزند دارد. گره را با فرزندش جایگزین کنید.
- حالت ۳: گره مورد نظر برای حذف دو فرزند دارد. جانشین inorder (کوچکترین گره در زیردرخت راست) را پیدا کنید، گره را با جانشین جایگزین کرده و سپس جانشین را حذف کنید.
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)
پیمایش درخت شامل بازدید از هر گره در درخت به یک ترتیب خاص است. چندین روش پیمایش رایج وجود دارد:
- پیمایش میانی (In-order): ابتدا زیردرخت چپ، سپس گره و سپس زیردرخت راست را بازدید میکند. این کار منجر به بازدید گرهها به ترتیب صعودی میشود.
- پیمایش پیشوندی (Pre-order): ابتدا گره، سپس زیردرخت چپ و سپس زیردرخت راست را بازدید میکند.
- پیمایش پسوندی (Post-order): ابتدا زیردرخت چپ، سپس زیردرخت راست و سپس گره را بازدید میکند.
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ها، مانند B-treeها، برای مکانیابی کارآمد رکوردها استفاده میکنند. مقیاس جهانی پایگاههای داده مورد استفاده توسط شرکتهای چندملیتی را در نظر بگیرید؛ بازیابی کارآمد دادهها از اهمیت بالایی برخوردار است.
- کامپایلرها: جداول نماد، که اطلاعات مربوط به متغیرها و توابع را ذخیره میکنند.
- سیستمهای عامل: زمانبندی فرآیندها و مدیریت حافظه.
- موتورهای جستجو: نمایهسازی صفحات وب و رتبهبندی نتایج جستجو.
- سیستمهای فایل: سازماندهی و دسترسی به فایلها. یک سیستم فایل روی سروری را تصور کنید که به صورت جهانی برای میزبانی وبسایتها استفاده میشود؛ یک ساختار مبتنی بر BST به خوبی سازماندهی شده به ارائه سریع محتوا کمک میکند.
ملاحظات عملکردی
عملکرد یک BST به ساختار آن بستگی دارد. در بهترین حالت، یک BST متعادل امکان پیچیدگی زمانی لگاریتمی را برای عملیات درج، جستجو و حذف فراهم میکند. با این حال، در بدترین حالت (مثلاً یک درخت کج)، پیچیدگی زمانی میتواند به زمان خطی کاهش یابد.
درختان متعادل در مقابل نامتعادل
یک BST متعادل درختی است که در آن ارتفاع زیردرختان چپ و راست هر گره حداکثر یک واحد اختلاف داشته باشد. الگوریتمهای خود-متعادلکننده، مانند درختان AVL و درختان قرمز-سیاه، تضمین میکنند که درخت متعادل باقی بماند و عملکرد ثابتی را ارائه دهد. مناطق مختلف ممکن است بر اساس بار روی سرور به سطوح بهینهسازی متفاوتی نیاز داشته باشند؛ متعادلسازی به حفظ عملکرد تحت استفاده بالای جهانی کمک میکند.
پیچیدگی زمانی
- درج: O(log n) به طور متوسط، O(n) در بدترین حالت.
- جستجو: O(log n) به طور متوسط، O(n) در بدترین حالت.
- حذف: O(log n) به طور متوسط، O(n) در بدترین حالت.
- پیمایش: O(n)، که در آن n تعداد گرههای درخت است.
مفاهیم پیشرفته 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 یک دارایی بینظیر برای هر برنامهنویسی است.
همانطور که سفر خود را در علوم کامپیوتر ادامه میدهید، کاوش در مفاهیم پیشرفتهتری مانند درختان خود-متعادلکننده و پیادهسازیهای مختلف آنها، درک و تواناییهای شما را بیشتر خواهد کرد. به تمرین و آزمایش با سناریوهای مختلف ادامه دهید تا در هنر استفاده مؤثر از درختان جستجوی دودویی مهارت پیدا کنید.