265 lines
14 KiB
Markdown
265 lines
14 KiB
Markdown
---
|
||
title: Binary Search Trees
|
||
localeTitle: أشجار البحث الثنائي
|
||
---
|
||
## أشجار البحث الثنائي
|
||
|
||
![شجرة البحث الثنائية](https://cdn-images-1.medium.com/max/1320/0*x5o1G1UpM1RfLpyx.png)
|
||
|
||
الشجرة هي بنية بيانات تتكون من عقد لها الخصائص التالية:
|
||
|
||
1. كل شجرة لديها عقدة جذرية (في الجزء العلوي) لديها بعض القيمة.
|
||
2. العقدة الجذرية لديها صفر أو أكثر من العقد التابعة.
|
||
3. كل عقدة طفل لديها صفر أو أكثر العقد التابعة ، وهلم جرا. هذا إنشاء شجرة فرعية في الشجرة. تحتوي كل عقدة على شجرة فرعية خاصة بها تتكون من أطفاله وأطفالهم ، إلخ. وهذا يعني أن كل عقدة بمفردها يمكن أن تكون شجرة.
|
||
|
||
تضيف شجرة البحث الثنائية (BST) هاتين الخاصيتين:
|
||
|
||
1. كل عقدة لديها حد أقصى يصل إلى طفلين.
|
||
2. لكل عقدة ، تكون قيم العقد المتسلسلة اليسرى أقل من عقدة العقدة الحالية ، والتي بدورها تكون أقل من العقد التناسلية الصحيحة (إن وجدت).
|
||
|
||
يتم إنشاء BST على فكرة خوارزمية [البحث الثنائي](https://guide.freecodecamp.org/algorithms/search-algorithms/binary-search) ، والتي تسمح [بالبحث](https://guide.freecodecamp.org/algorithms/search-algorithms/binary-search) السريع وإدخال وإزالة العقد. إن الطريقة التي يتم بها إعدادها تعني أنه في المتوسط ، تسمح كل مقارنة للعمليات بتخطي حوالي نصف الشجرة ، بحيث يستغرق كل بحث أو إدراج أو حذف وقتًا متناسبًا مع لوغاريتم عدد العناصر المخزنة في الشجرة ، `O(log n)` . ومع ذلك ، في بعض الأحيان يمكن أن يحدث أسوأ الحالات ، عندما تكون الشجرة غير متوازنة ويكون تعقيد الوقت هو `O(n)` لكل هذه الوظائف الثلاثة. هذا هو السبب في أشجار التوازن الذاتي (AVL ، أحمر أسود ، وما إلى ذلك) هي أكثر فعالية من BST الأساسية.
|
||
|
||
**أسوأ مثال على سيناريو الحالة:** يمكن أن يحدث هذا عند الاستمرار في إضافة العقد التي تكون _دائمًا_ أكبر من العقدة قبل (إنه الأصل) ، ويمكن أن يحدث نفس الشيء عندما تقوم دائمًا بإضافة العقد ذات القيم الأقل من أولياء أمورهم.
|
||
|
||
### العمليات الأساسية على BST
|
||
|
||
* إنشاء: ينشئ شجرة فارغة.
|
||
* إدراج: إدراج عقدة في الشجرة.
|
||
* بحث: يبحث عن عقدة في الشجرة.
|
||
* حذف: لحذف عقدة من الشجرة.
|
||
|
||
#### خلق
|
||
|
||
في البداية يتم إنشاء شجرة فارغة بدون أي عقد. تتم تهيئة المتغير / المعرف الذي يجب أن يشير إلى عقدة الجذر بقيمة `NULL` .
|
||
|
||
#### بحث
|
||
|
||
تبدأ دائمًا في البحث عن الشجرة في عقدة الجذر والنزول من هناك. يمكنك مقارنة البيانات في كل عقدة مع تلك التي تبحث عنها. إذا لم تتطابق العقدة المقارنة ، فإما أن تنتقل إلى الطفل الصحيح أو الطفل الأيسر ، والذي يعتمد على نتيجة المقارنة التالية: إذا كانت العقدة التي تبحث عنها أقل من تلك التي كنت تقارنها بها ، تنتقل إلى الطفل الأيسر ، وإلا (إذا كان أكبر) ، فانتقل إلى الطفل الصحيح. لماذا ا؟ نظرًا لأن BST منظم (وفقًا لتعريفه) ، فإن الطفل المناسب دائمًا يكون أكبر من الأصل وأن الطفل الأيسر يكون دائمًا أقل.
|
||
|
||
#### إدراج
|
||
|
||
انها تشبه الى حد بعيد وظيفة البحث. تبدأ مرة أخرى عند جذر الشجرة وتنزل بشكل متكرر ، حيث تبحث عن المكان المناسب لإدخال العقدة الجديدة ، بنفس الطريقة الموضحة في وظيفة البحث. إذا كانت العقدة ذات القيمة نفسها موجودة بالفعل في الشجرة ، يمكنك اختيار إما إدراج التكرار أم لا. بعض الأشجار تسمح بوجود نسخ مكررة ، والبعض الآخر لا يسمح بذلك. ذلك يعتمد على تنفيذ معين.
|
||
|
||
#### حذف
|
||
|
||
هناك 3 حالات يمكن أن تحدث عندما تحاول حذف عقدة. إذا كان لديه،
|
||
|
||
1. لا توجد شجرة فرعية (لا يوجد أطفال): هذا هو الأسهل. يمكنك ببساطة حذف العقدة ، دون أي إجراءات إضافية مطلوبة.
|
||
2. شجرة فرعية واحدة (طفل واحد): يجب عليك التأكد من أنه بعد حذف العقدة ، يتم توصيل الطفل التابع له إلى أصل العقدة المحذوفة.
|
||
3. صفحتان فرعيتان (طفلين): عليك البحث عن واستبدال العقدة التي تريد حذفها مع خلفها (العقدة الفاتنة في الشجرة الفرعية الصحيحة).
|
||
|
||
وقت تعقيد إنشاء الشجرة هو `O(1)` . يعتمد تعقيد وقت البحث عن عقدة أو إدخالها أو حذفها على ارتفاع الشجرة `h` ، لذا فإن الحالة الأسوأ هي `O(h)` .
|
||
|
||
#### سلف عقدة
|
||
|
||
يمكن وصف الإصدارات السابقة بأنها العقدة التي ستظهر قبل العقدة التي أنت فيها حاليًا. للعثور على سابق العقدة الحالية ، ابحث عن العقدة اليمنى الأكبر / الأكبر في الشجرة الفرعية اليسرى.
|
||
|
||
#### خلف العقدة
|
||
|
||
يمكن وصف الخلفاء على أنهم العقدة التي تأتي بعد العقدة التي أنت فيها حاليًا. للعثور على خليفة العقدة الحالية ، انظر إلى العقدة اليسرى / أقصى اليسار في الشجرة الفرعية اليمنى.
|
||
|
||
### أنواع خاصة من BT
|
||
|
||
* كومة
|
||
* شجرة حمراء سوداء
|
||
* B-شجرة
|
||
* شجرة سبلاي
|
||
* شجرة N-ary
|
||
* تري (شجرة الجذر)
|
||
|
||
### وقت التشغيل
|
||
|
||
**هيكل البيانات: صفيف**
|
||
|
||
* أسوأ حالة أداء: `O(log n)`
|
||
* أداء أفضل أداء: `O(1)`
|
||
* متوسط الأداء: `O(log n)`
|
||
* أسوأ تعقيد في الفضاء: `O(1)`
|
||
|
||
حيث `n` هو عدد العقد في BST.
|
||
|
||
### تنفيذ BST
|
||
|
||
وإليك تعريفًا لعقدة BST التي تحتوي على بعض البيانات ، مع الإشارة إلى عقد الطفل اليمنى واليسرى.
|
||
|
||
`struct node {
|
||
int data;
|
||
struct node *leftChild;
|
||
struct node *rightChild;
|
||
};
|
||
`
|
||
|
||
#### عملية البحث
|
||
|
||
عندما يتم البحث عن عنصر ، ابدأ البحث من العقدة الجذرية. ثم إذا كانت البيانات أقل من قيمة المفتاح ، فابحث عن العنصر في الشجرة الفرعية اليسرى. خلاف ذلك ، ابحث عن العنصر في الشجرة الفرعية الصحيحة. اتبع نفس الخوارزمية لكل عقدة.
|
||
|
||
`struct node* search(int data){
|
||
struct node *current = root;
|
||
printf("Visiting elements: ");
|
||
|
||
while(current->data != data){
|
||
|
||
if(current != NULL) {
|
||
printf("%d ",current->data);
|
||
|
||
//go to left tree
|
||
if(current->data > data){
|
||
current = current->leftChild;
|
||
}//else go to right tree
|
||
else {
|
||
current = current->rightChild;
|
||
}
|
||
|
||
//not found
|
||
if(current == NULL){
|
||
return NULL;
|
||
}
|
||
}
|
||
}
|
||
return current;
|
||
}
|
||
`
|
||
|
||
#### أدخل العملية
|
||
|
||
عندما يتم إدراج عنصر ، حدد موقعه الصحيح أولاً. ابدأ البحث من العقدة الجذرية ، ثم إذا كانت البيانات أقل من قيمة المفتاح ، ابحث عن الموقع الفارغ في الشجرة الفرعية اليسرى وأدخل البيانات. وإلا ، فابحث عن الموقع الفارغ في الشجرة الفرعية الصحيحة وأدخل البيانات.
|
||
|
||
`void insert(int data) {
|
||
struct node *tempNode = (struct node*) malloc(sizeof(struct node));
|
||
struct node *current;
|
||
struct node *parent;
|
||
|
||
tempNode->data = data;
|
||
tempNode->leftChild = NULL;
|
||
tempNode->rightChild = NULL;
|
||
|
||
//if tree is empty
|
||
if(root == NULL) {
|
||
root = tempNode;
|
||
} else {
|
||
current = root;
|
||
parent = NULL;
|
||
|
||
while(1) {
|
||
parent = current;
|
||
|
||
//go to left of the tree
|
||
if(data < parent->data) {
|
||
current = current->leftChild;
|
||
//insert to the left
|
||
|
||
if(current == NULL) {
|
||
parent->leftChild = tempNode;
|
||
return;
|
||
}
|
||
}//go to right of the tree
|
||
else {
|
||
current = current->rightChild;
|
||
|
||
//insert to the right
|
||
if(current == NULL) {
|
||
parent->rightChild = tempNode;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
`
|
||
|
||
تتيح لنا أيضًا أشجار البحث الثنائي (BSTs) الوصول السريع إلى الأسلاف والخلفاء. يمكن وصف الإصدارات السابقة بأنها العقدة التي ستظهر قبل العقدة التي أنت فيها حاليًا.
|
||
|
||
* للعثور على سابق العقدة الحالية ، انظر إلى العقدة الموجودة في أقصى اليمين / أعلى ورقة في الشجرة الفرعية اليسرى. يمكن وصف الخلفاء على أنهم العقدة التي تأتي بعد العقدة التي أنت فيها حاليًا.
|
||
* للعثور على خليفة العقدة الحالية ، انظر إلى العقدة في أقصى اليسار / أصغر ورقة في الشجرة الفرعية اليمنى.
|
||
|
||
### دعونا ننظر في اثنين من الإجراءات التي تعمل على الأشجار.
|
||
|
||
بما أن الأشجار يتم تعريفها بشكل متكرر ، فمن الشائع جدًا كتابة الإجراءات الروتينية التي تعمل على أشجار متكررة.
|
||
|
||
على سبيل المثال ، إذا أردنا حساب ارتفاع الشجرة ، وهذا هو ارتفاع العقدة الجذرية ، يمكننا المضي قدمًا وبشكل متكرر ، من خلال الانتقال إلى الشجرة. لذلك يمكننا القول:
|
||
|
||
* على سبيل المثال ، إذا كان لدينا شجرة صفراء ، فإن ارتفاعها يبلغ 0.
|
||
* بخلاف ذلك ، نحن 1 زائد الحد الأقصى لشجرة الطفل اليسرى والشجرة الفرعية المناسبة.
|
||
* لذلك إذا نظرنا إلى ورقة على سبيل المثال ، فإن هذا الارتفاع سيكون 1 لأن ارتفاع الطفل الأيسر هو صفر ، هو 0 ، وارتفاع الطفل الأيمن صفر هو أيضا 0. لذا فإن الحد الأقصى لذلك هو 0 ، ثم 1 زائد 0.
|
||
|
||
#### خوارزمية الارتفاع (شجرة)
|
||
|
||
`if tree = nil:
|
||
return 0
|
||
return 1 + Max(Height(tree.left),Height(tree.right))
|
||
`
|
||
|
||
#### هنا هو رمز في C ++
|
||
|
||
`int maxDepth(struct node* node)
|
||
{
|
||
if (node==NULL)
|
||
return 0;
|
||
else
|
||
{
|
||
int rDepth = maxDepth(node->right);
|
||
int lDepth = maxDepth(node->left);
|
||
|
||
if (lDepth > rDepth)
|
||
{
|
||
return(lDepth+1);
|
||
}
|
||
else
|
||
{
|
||
return(rDepth+1);
|
||
}
|
||
}
|
||
}
|
||
`
|
||
|
||
يمكن أن ننظر أيضا في حساب حجم الشجرة التي هي عدد العقد.
|
||
|
||
* مرة أخرى ، إذا كان لدينا شجرة لا شيء ، لدينا عقد الصفر.
|
||
* خلاف ذلك ، لدينا عدد العقد في الطفل الأيسر بالإضافة إلى 1 لأنفسنا بالإضافة إلى عدد العقد في الطفل الصحيح. 1 بالإضافة إلى حجم الشجرة اليسرى بالإضافة إلى حجم الشجرة الصحيحة.
|
||
|
||
#### خوارزمية الحجم (شجرة)
|
||
|
||
`if tree = nil
|
||
return 0
|
||
return 1 + Size(tree.left) + Size(tree.right)
|
||
`
|
||
|
||
#### هنا هو رمز في C ++
|
||
|
||
`int treeSize(struct node* node)
|
||
{
|
||
if (node==NULL)
|
||
return 0;
|
||
else
|
||
return 1+(treeSize(node->left) + treeSize(node->right));
|
||
}
|
||
`
|
||
|
||
### مقاطع الفيديو ذات الصلة على قناة YouTube freeCodeCamp
|
||
|
||
* [شجرة البحث الثنائية](https://youtu.be/5cU1ILGy6dM)
|
||
* [شجرة البحث الثنائية: عبور والطول](https://youtu.be/Aagf3RyK3Lw)
|
||
|
||
### فيما يلي الأنواع الشائعة من الأشجار الثنائية:
|
||
|
||
Full Binary Tree / Strict Binary Tree: شجرة ثنائية ممتلئة أو صارمة إذا كان لكل عقدة 0 أو 2 أطفال.
|
||
|
||
` 18
|
||
/ \
|
||
15 30
|
||
/ \ / \
|
||
40 50 100 40
|
||
`
|
||
|
||
في شجرة الثنائي الكاملة ، عدد العقد الورقية يساوي عدد العقد الداخلية زائد واحد.
|
||
|
||
إكمال Binary Tree: A Binary Tree اكتمال Binary Tree إذا تمت تعبئة كافة المستويات تمامًا باستثناء المستوى الأخير ، بينما يحتوي المستوى الأخير على كافة المفاتيح التي تم تركها قدر الإمكان
|
||
|
||
` 18
|
||
/ \
|
||
15 30
|
||
/ \ / \
|
||
40 50 100 40
|
||
/ \ /
|
||
8 7 9
|
||
` |