أشجار AVL: التدوير والإدراج والحذف C++ مثال

⚡ ملخص ذكي

أشجار AVL هي أشجار بحث ثنائية متوازنة ذاتيًا حيث يظل فرق الارتفاع بين الأشجار الفرعية اليسرى واليمنى لكل عقدة ضمن -1 أو 0 أو +1، مما يضمن أداء بحث O(log n).

  • 🌲 فريف: شجرة بحث ثنائية يكون فيها عامل التوازن لكل عقدة ضمن {-1، 0، +1}، ​​سميت على اسم المخترعين أديلسون-فيلسكي ولانديس.
  • 🇧🇷 عامل التوازن: يتم حسابها على أنها الارتفاع (يسار) - الارتفاع (يمين)؛ القيم خارج {-1، 0، +1} تؤدي إلى دوران لاستعادة التوازن.
  • 🔄 الدورات: أربع حالات - LL و RR و LR و RL - تعيد محاذاة العقد بعد عمليات الإدخال أو الحذف غير المتوازنة للحفاظ على ارتفاع الشجرة لوغاريتميًا.
  • ➕ إدراج: إدخال BST القياسي متبوعًا بمشي تصاعدي يعيد حساب عوامل التوازن ويؤدي دورة واحدة أو دورتين على الأكثر.
  • ➖ حذف: يشبه حذف BST ولكنه قد يؤدي إلى دورات متعددة لأعلى الشجرة لأن ارتفاع الشجرة الفرعية يمكن أن يتقلص عند كل سلف.
  • 🚀 التطبيقات: تستخدم قواعد البيانات والفهارس الموجودة في الذاكرة وبيانات تعريف نظام الملفات وهياكل البحث في الذكاء الاصطناعي أشجار AVL لإجراء عمليات بحث سريعة ومنظمة.

الأشجار AVL

ما هي أشجار AVL؟

الأشجار AVL هي أشجار بحث ثنائية يكون فيها فرق الارتفاع بين الشجرة الفرعية اليسرى واليمنى لكل عقدة هو -1 أو 0 أو +1. وهي أشجار بحث ثنائية متوازنة ذاتيًا تحافظ على وقت بحث لوغاريتمي، وقد سميت على اسم المخترعين أديلسون-فيلسكي ولانديس (AVL).

كيف تعمل شجرة AVL؟

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

عمل شجرة AVL

تصور شجرة AVL

تنمو الشجرة خطيًا عند وصول المفاتيح بترتيب تصاعدي، مما يُقلل زمن البحث إلى O(n). وهذا يُفقد الغرض من شجرة البحث الثنائية (BST) - فالشجرة المتوازنة فقط هي التي تُبقي زمن البحث لوغاريتميًا. الآن، انظر إلى نفس المفاتيح مُدخلة بترتيب مختلف.

عمل شجرة AVL

يؤدي استخدام نفس المفاتيح مع اختلاف ترتيب الإدخال إلى شكل أقل عمقًا، وبالتالي يتم تنفيذ كل عملية بحث في زمن O(log n). تحافظ أشجار AVL على هذا الشكل من خلال مراقبة الارتفاع عند كل عملية إدخال وتصحيح أي اختلال دون الإخلال بترتيب شجرة البحث الثنائية.

عامل التوازن في أشجار AVL

عامل التوازن (BF) tracks ارتفاع كل عقدة حتى تتمكن الشجرة من تحقيق التوازن الذاتي أثناء التشغيل.

خصائص عامل التوازن

عامل التوازن في أشجار AVL

شجرة عامل التوازن AVL

  • عامل التوازن هو الفرق بين ارتفاع الشجرة الفرعية اليسرى وارتفاع الشجرة الفرعية اليمنى.
  • Balance factor(node) = height(node->left) − height(node->right)
  • القيم المسموح بها فقط هي -1 و 0 و +1.
  • قيمة -1 تعني أن الشجرة الفرعية اليمنى تحتوي على مستوى إضافي واحد - العقدة ثقيلة من اليمين.
  • قيمة +1 تعني أن الشجرة الفرعية اليسرى تحتوي على مستوى إضافي واحد - العقدة ثقيلة من اليسار.
  • قيمة 0 تعني أن كلا الجانبين لهما نفس الارتفاع - العقدة متوازنة تمامًا.

دورات AVL

تُجرى عمليات التدوير كلما أدى إدخال عنصر أو حذفه إلى الإخلال بقاعدة عامل التوازن. الحالات الأربع هي: LL، RR، LR، وRL.

اليسار - دوران اليسار

يتم إجراء هذا التدوير عند إدراج عقدة جديدة عند الفرع الفرعي الأيسر للشجرة الفرعية اليسرى.

شجرة AVL لليسار – الدوران لليسار

شجرة AVL لليسار – الدوران لليسار

يتم إجراء دوران واحد لليمين. يتم تفعيل هذه الحالة عندما يكون للعقدة BF +2 ويكون لابنها الأيسر BF +1.

يمين - دوران يمين

يتم تنفيذ هذا التدوير عند إدراج عقدة جديدة في الفرع الأيمن للشجرة الفرعية اليمنى.

شجرة AVL لليمين - التدوير لليمين

يتم إجراء دوران واحد لليسار. يتم تفعيل هذه الحالة عندما يكون للعقدة BF −2 ويكون لابنها الأيمن BF −1.

اليمين - دوران اليسار

يتم تنفيذ هذا التدوير عند إدراج عقدة جديدة عند الفرع الأيسر للشجرة الفرعية اليمنى.

شجرة AVL لليمين - الدوران لليسار

يتم تشغيلها عندما يكون BF(node) = −2 و BF(right-child) = +1. قم بتدوير الابن الأيمن إلى اليمين، ثم قم بتدوير العقدة إلى اليسار.

اليسار - دوران اليمين

يتم تنفيذ هذا التدوير عند إدراج عقدة جديدة عند الفرع الأيمن للشجرة الفرعية اليسرى.

شجرة AVL لليسار – الدوران لليمين

يتم تشغيلها عندما يكون BF(node) = +2 و BF(left-child) = −1. قم بتدوير الابن الأيسر إلى اليسار، ثم قم بتدوير العقدة إلى اليمين.

الإدراج في أشجار AVL

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

الإدراج في أشجار AVL

تنفيذ إدراج شجرة AVL

الخطوة 1 : أدرج العقدة باستخدام خوارزمية BST القياسية. في المثال أعلاه، أدرج 160.

الخطوة 2 : قم بتحديث عامل التوازن لكل سلف على طول مسار الإدخال.

الخطوة 3 : إذا تجاوز أي سلف نطاق عامل التوازن، فقم بإجراء التدوير المطابق. في المثال، تم تجاوز عامل التوازن للعقدة 350، لذا فإن تدوير LL يعيد التوازن.

  1. If BF(node) = +2 و BF(left-child) = +1قم بإجراء دوران LL.
  2. If BF(node) = −2 و BF(right-child) = −1قم بإجراء التناوب RR.
  3. If BF(node) = −2 و BF(right-child) = +1قم بإجراء دوران RL.
  4. If BF(node) = +2 و BF(left-child) = −1قم بإجراء دوران LR.

الحذف في أشجار AVL

تتبع عملية الحذف نفس منطق البحث الثنائي البسيط، ثم تعيد التوازن بعد ذلك.

الخطوة 1 : ابحث عن العنصر الموجود في الشجرة

الخطوة 2 : احذف العقدة باستخدام عملية حذف BST القياسية.

الخطوة 3 : هناك حالتان محتملتان.

حالة 1: الحذف من الشجرة الفرعية الصحيحة.

  • 1A. If BF(node) = +2 و BF(left-child) = +1قم بإجراء دوران LL.
  • 1B. If BF(node) = +2 و BF(left-child) = −1قم بإجراء دوران LR.
  • 1C. If BF(node) = +2 و BF(left-child) = 0قم بإجراء دوران LL.

الحذف في أشجار AVL

حالة 2: حذف من الشجرة الفرعية اليسرى.

  • 2A. If BF(node) = −2 و BF(right-child) = −1قم بإجراء التناوب RR.
  • 2B. If BF(node) = −2 و BF(right-child) = +1قم بإجراء دوران RL.
  • 2C. If BF(node) = −2 و BF(right-child) = 0قم بإجراء التناوب RR.

الحذف في أشجار AVL

C++ مثال على أشجار AVL

وفيما يلي C++ برنامج ينفذ أشجار AVL:

#include <iostream>
#include <queue>
#include <unordered_map>
using namespace std;

struct node {
    struct node *left;
    int data;
    int height;
    struct node *right;
};

class AVL {
public:
    struct node *root;

    AVL() {
        this->root = NULL;
    }

    int calheight(struct node *p) {
        if (p->left && p->right) {
            if (p->left->height < p->right->height)
                return p->right->height + 1;
            else
                return p->left->height + 1;
        }
        else if (p->left && p->right == NULL) {
            return p->left->height + 1;
        }
        else if (p->left == NULL && p->right) {
            return p->right->height + 1;
        }
        return 0;
    }

    int bf(struct node *n) {
        if (n->left && n->right)
            return n->left->height - n->right->height;
        else if (n->left && n->right == NULL)
            return n->left->height;
        else if (n->left == NULL && n->right)
            return -n->right->height;
        return 0;
    }

    struct node *llrotation(struct node *n) {
        struct node *p = n;
        struct node *tp = p->left;
        p->left = tp->right;
        tp->right = p;
        return tp;
    }

    struct node *rrrotation(struct node *n) {
        struct node *p = n;
        struct node *tp = p->right;
        p->right = tp->left;
        tp->left = p;
        return tp;
    }

    struct node *rlrotation(struct node *n) {
        struct node *p = n;
        struct node *tp = p->right;
        struct node *tp2 = p->right->left;
        p->right = tp2->left;
        tp->left = tp2->right;
        tp2->left = p;
        tp2->right = tp;
        return tp2;
    }

    struct node *lrrotation(struct node *n) {
        struct node *p = n;
        struct node *tp = p->left;
        struct node *tp2 = p->left->right;
        p->left = tp2->right;
        tp->right = tp2->left;
        tp2->right = p;
        tp2->left = tp;
        return tp2;
    }

    struct node *insert(struct node *r, int data) {
        if (r == NULL) {
            r = new struct node;
            r->data = data;
            r->left = r->right = NULL;
            r->height = 1;
            return r;
        }
        if (data < r->data)
            r->left = insert(r->left, data);
        else
            r->right = insert(r->right, data);

        r->height = calheight(r);

        if (bf(r) == 2 && bf(r->left) == 1)       r = llrotation(r);
        else if (bf(r) == -2 && bf(r->right) == -1) r = rrrotation(r);
        else if (bf(r) == -2 && bf(r->right) == 1)  r = rlrotation(r);
        else if (bf(r) == 2 && bf(r->left) == -1)   r = lrrotation(r);

        return r;
    }

    void levelorder_newline() {
        if (this->root == NULL) {
            cout << "\nEmpty tree\n";
            return;
        }
        levelorder_newline(this->root);
    }

    void levelorder_newline(struct node *v) {
        queue<struct node *> q;
        struct node *cur;
        q.push(v);
        q.push(NULL);
        while (!q.empty()) {
            cur = q.front();
            q.pop();
            if (cur == NULL && q.size() != 0) {
                cout << "\n";
                q.push(NULL);
                continue;
            }
            if (cur != NULL) {
                cout << " " << cur->data;
                if (cur->left != NULL)  q.push(cur->left);
                if (cur->right != NULL) q.push(cur->right);
            }
        }
    }

    struct node *deleteNode(struct node *p, int data) {
        if (p->left == NULL && p->right == NULL) {
            if (p == this->root) this->root = NULL;
            delete p;
            return NULL;
        }
        struct node *q;
        if (p->data < data)      p->right = deleteNode(p->right, data);
        else if (p->data > data) p->left  = deleteNode(p->left, data);
        else {
            if (p->left != NULL) {
                q = inpre(p->left);
                p->data = q->data;
                p->left = deleteNode(p->left, q->data);
            } else {
                q = insuc(p->right);
                p->data = q->data;
                p->right = deleteNode(p->right, q->data);
            }
        }

        if (bf(p) == 2 && bf(p->left) == 1)         p = llrotation(p);
        else if (bf(p) == 2 && bf(p->left) == -1)    p = lrrotation(p);
        else if (bf(p) == 2 && bf(p->left) == 0)     p = llrotation(p);
        else if (bf(p) == -2 && bf(p->right) == -1)  p = rrrotation(p);
        else if (bf(p) == -2 && bf(p->right) == 1)   p = rlrotation(p);
        else if (bf(p) == -2 && bf(p->right) == 0)   p = rrrotation(p);

        return p;
    }

    struct node *inpre(struct node *p) {
        while (p->right != NULL) p = p->right;
        return p;
    }

    struct node *insuc(struct node *p) {
        while (p->left != NULL) p = p->left;
        return p;
    }

    ~AVL() {}
};

int main() {
    AVL b;
    int c, x;
    do {
        cout << "\n1.Display levelorder on newline";
        cout << "\n2.Insert";
        cout << "\n3.Delete\n";
        cout << "\n0.Exit\n";
        cout << "\nChoice: ";
        cin >> c;
        switch (c) {
        case 1: b.levelorder_newline(); break;
        case 2:
            cout << "\nEnter no. "; cin >> x;
            b.root = b.insert(b.root, x);
            break;
        case 3:
            cout << "\nWhat to delete? "; cin >> x;
            b.root = b.deleteNode(b.root, x);
            break;
        case 0: break;
        }
    } while (c != 0);
}

مثال عملي للكود أعلاه:

  1. انسخ الكود أعلاه واحفظه في ملف باسم avl.cpp.
  2. قم بتجميع الكود:
g++ avl.cpp -o run
  1. قم بتشغيل الكود.
./run

C++ مثال على أشجار AVL

مزايا أشجار AVL

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

الأسئلة الشائعة

شجرة AVL هي شجرة بحث ثنائية متوازنة ذاتيًا، حيث يبقى عامل التوازن لكل عقدة ضمن النطاق {-1، 0، +1}. وتعيد عمليات التدوير هذا الثابت عند كل عملية إدراج أو حذف.ping البحث والإدراج والحذف في O(log n).

يُحسب عامل التوازن للعقدة بطرح ارتفاع الشجرة الفرعية اليمنى من ارتفاع الشجرة الفرعية اليسرى. يجب أن تقع القيم ضمن النطاق {-1، 0، +1}. يشير عامل التوازن +2 أو -2 إلى أن عملية الإضافة أو الحذف قد أخلّت بتوازن تلك العقدة، مما يستدعي تدويرها.

الدورانات الأربعة هي LL وRR وLR وRL. يستخدم LL دورانًا واحدًا لليمين، ويستخدم RR دورانًا واحدًا لليسار، أما LR وRL فهما دورانان مزدوجان يجمعان بين دوران واحد على الابن ودوران معاكس على العقدة.

تُجرى عملية الإضافة وفقًا لقاعدة شجرة البحث الثنائية القياسية، ثم تعود الشجرة إلى الأعلى مع تحديث الارتفاعات. إذا أخلّ أي سلف بقاعدة التوازن، فإن دورة واحدة أو دورتين تُعيد التوازن. ولا يلزم أكثر من دورة واحدة لكل عملية إضافة.

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

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

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

نعم. يقوم برنامج GitHub Copilot ومساعدو الذكاء الاصطناعي المماثلون بإنشاء إجراءات الإدراج والحذف والتدوير في C++, Java أو Python، وإنشاء اختبارات وحدة تتحقق من ثبات عامل التوازن في كل عملية.

تلخيص هذه التدوينة بـ: