269 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			269 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| ---
 | ||
| title: Binary Search Trees
 | ||
| localeTitle: أشجار البحث الثنائي
 | ||
| ---
 | ||
| ## أشجار البحث الثنائي
 | ||
| 
 | ||
| 
 | ||
| 
 | ||
| الشجرة هي بنية بيانات تتكون من عقد لها الخصائص التالية:
 | ||
| 
 | ||
| 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 التي تحتوي على بعض البيانات ، مع الإشارة إلى عقد الطفل اليمنى واليسرى.
 | ||
| 
 | ||
| ```c
 | ||
| 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 
 | ||
| ` |