ต้นไม้ AVL: การหมุน การแทรก การลบด้วย C++ ตัวอย่าง

ต้นไม้ AVL คืออะไร?

ต้นไม้เอวีแอล เป็นแผนผังการค้นหาแบบไบนารีซึ่งความแตกต่างระหว่างความสูงของแผนผังย่อยด้านซ้ายและขวาเป็น -1, 0 หรือ +1

ต้นไม้ AVL เรียกอีกอย่างว่าแผนผังการค้นหาแบบไบนารีที่ปรับสมดุลในตัวเอง ต้นไม้เหล่านี้ช่วยรักษาเวลาในการค้นหาลอการิทึม ตั้งชื่อตามนักประดิษฐ์ (AVL) Adelson, Velsky และ Landis

AVL Tree ทำงานอย่างไร

เพื่อให้เข้าใจถึงความจำเป็นของแผนผัง AVL ได้ดีขึ้น เรามาดูข้อเสียบางประการของแผนผังการค้นหาแบบไบนารีอย่างง่ายกัน

พิจารณาคีย์ต่อไปนี้ที่ถูกแทรกตามลำดับที่กำหนดในต้นไม้การค้นหาแบบไบนารี


งานต้นไม้ AVL
การแสดงภาพต้นไม้ AVL

ความสูงของต้นไม้จะเพิ่มขึ้นแบบเชิงเส้นเมื่อเราใส่คีย์ตามลำดับค่าที่เพิ่มขึ้น ดังนั้น การค้นหาในกรณีที่แย่ที่สุดจะใช้ค่า O(n)

ต้องใช้เวลาเป็นเส้นตรงในการค้นหาองค์ประกอบ จึงไม่มีประโยชน์ในการใช้ ต้นไม้ค้นหาแบบไบนารี โครงสร้าง ในทางกลับกัน หากความสูงของต้นไม้สมดุล เราก็จะได้เวลาค้นหาที่ดีขึ้น

ตอนนี้เรามาดูคีย์เดียวกันแต่แทรกในลำดับที่ต่างกัน

งานต้นไม้ AVL

ในกรณีนี้ กุญแจจะเหมือนกัน แต่เนื่องจากพวกมันถูกแทรกในลำดับที่ต่างกัน พวกมันจึงอยู่ในตำแหน่งที่แตกต่างกัน และความสูงของต้นไม้ยังคงสมดุล ดังนั้นการค้นหาจะใช้เวลาไม่เกิน O(log n) สำหรับองค์ประกอบใดๆ ของแผนผัง ปัจจุบันเห็นได้ชัดว่าหากแทรกอย่างถูกต้อง ความสูงของต้นไม้ก็จะสามารถรักษาสมดุลได้

ในต้นไม้ AVL เราจะตรวจสอบความสูงของต้นไม้ระหว่างการดำเนินการแทรก มีการปรับเปลี่ยนเพื่อรักษาความสูงที่สมดุลโดยไม่ละเมิดคุณสมบัติพื้นฐานของต้นไม้ค้นหาแบบไบนารี

ปัจจัยความสมดุลในต้นไม้ AVL

Balance Factor (BF) เป็นคุณลักษณะพื้นฐานของทุกโหนดในแผนผัง AVL ซึ่งช่วยในการตรวจสอบความสูงของแผนภูมิ

คุณสมบัติของตัวประกอบสมดุล


ปัจจัยความสมดุลในต้นไม้ AVL

ทรีสมดุลปัจจัย AVL
  • ปัจจัยความสมดุลเรียกว่าความแตกต่างระหว่างความสูงของแผนผังย่อยด้านซ้ายและแผนผังย่อยด้านขวา
  • ปัจจัยความสมดุล (โหนด) = ความสูง (โหนด -> ซ้าย) – ความสูง (โหนด -> ขวา)
  • ค่าที่อนุญาตของ BF คือ –1, 0 และ +1
  • ค่า –1 บ่งชี้ว่าแผนผังย่อยด้านขวามีรายการพิเศษอีกหนึ่งรายการ กล่าวคือ ต้นไม้มีน้ำหนักมาก
  • ค่า +1 บ่งชี้ว่าแผนผังย่อยด้านซ้ายมีรายการพิเศษรายการหนึ่ง กล่าวคือ ต้นไม้เหลือหนัก
  • ค่า 0 แสดงว่าต้นไม้มีโหนดเท่ากันในแต่ละด้าน กล่าวคือ ต้นไม้มีความสมดุลอย่างสมบูรณ์แบบ

การหมุน AVL

เพื่อให้ AVL Tree มีความสมดุล เมื่อมีการแทรกหรือลบโหนดออกจากแผนผัง การหมุนจะดำเนินการ

เราจะดำเนินการหมุน LL, หมุน RR, หมุน LR และหมุน RL ดังต่อไปนี้

  • ซ้าย – การหมุนซ้าย
  • ขวา - การหมุนขวา
  • หมุนขวา-ซ้าย
  • การหมุนซ้าย-ขวา

ซ้าย – การหมุนซ้าย

การหมุนนี้จะดำเนินการเมื่อมีการแทรกโหนดใหม่ที่ลูกด้านซ้ายของทรีย่อยด้านซ้าย


ต้นไม้ AVL ซ้าย – การหมุนซ้าย

ต้นไม้ AVL ซ้าย – การหมุนซ้าย

หมุนขวาเพียงครั้งเดียว การหมุนประเภทนี้จะถูกระบุเมื่อโหนดมีปัจจัยสมดุลเป็น +2 และลูกด้านซ้ายมีปัจจัยสมดุลเป็น +1

ขวา - การหมุนขวา

การหมุนนี้จะดำเนินการเมื่อมีการแทรกโหนดใหม่ที่ลูกด้านขวาของแผนผังย่อยด้านขวา

AVL Tree ขวา – การหมุนขวา

หมุนซ้ายเพียงครั้งเดียว การหมุนประเภทนี้จะถูกระบุเมื่อโหนดมีปัจจัยที่สมดุลเป็น -2 และลูกทางขวามีปัจจัยความสมดุลเป็น -1

หมุนขวา-ซ้าย

การหมุนนี้จะดำเนินการเมื่อมีการแทรกโหนดใหม่ที่ลูกด้านขวาของแผนผังย่อยด้านซ้าย

AVL Tree หมุนขวา – ซ้าย

การหมุนนี้จะดำเนินการเมื่อโหนดมีปัจจัยความสมดุลเป็น –2 และลูกด้านขวามีปัจจัยความสมดุลเป็น +1

การหมุนซ้าย-ขวา

การหมุนนี้จะดำเนินการเมื่อมีการแทรกโหนดใหม่ที่ลูกด้านซ้ายของแผนผังย่อยด้านขวา

การหมุนต้นไม้ AVL ซ้าย – ขวา

การหมุนนี้จะดำเนินการเมื่อโหนดมีปัจจัยความสมดุลเป็น +2 และลูกด้านซ้ายมีปัจจัยความสมดุลเป็น -1

การแทรกใน AVL Trees

การดำเนินการแทรกนั้นเกือบจะเหมือนกันกับการค้นหาแบบไบนารีแบบธรรมดา หลังจากแทรกทุกครั้ง เราจะปรับความสูงของทรีให้สมดุล การดำเนินการแทรกใช้เวลา O(log n) ซึ่งมีความซับซ้อนของเวลาที่สุด


การแทรกใน AVL Trees

การใช้งานการแทรกแผนผัง AVL

ขั้นตอนที่ 1: แทรกโหนดในแผนผัง AVL โดยใช้อัลกอริธึมการแทรกเดียวกันกับ BST ในตัวอย่างข้างต้น ให้แทรก 160

ขั้นตอนที่ 2: เมื่อเพิ่มโหนดแล้ว ปัจจัยความสมดุลของแต่ละโหนดจะได้รับการอัปเดต หลังจากแทรก 160 แล้ว ตัวประกอบความสมดุลของทุกโหนดจะได้รับการอัปเดต

ขั้นตอนที่ 3: ตอนนี้ตรวจสอบว่าโหนดใดละเมิดช่วงของปัจจัยความสมดุลหรือไม่ หากปัจจัยด้านความสมดุลถูกละเมิด จากนั้นทำการหมุนโดยใช้กรณีด้านล่าง ในตัวอย่างข้างต้น มีการละเมิดปัจจัยความสมดุลที่ 350 และกรณีที่ 1 มีผลบังคับใช้ เราจะดำเนินการหมุน LL และต้นไม้จะมีความสมดุลอีกครั้ง

  1. ถ้า BF(node) = +2 และ BF(node ​​-> left-child) = +1 ให้ดำเนินการหมุน LL
  2. ถ้า BF(node) = -2 และ BF(node ​​-> right-child) = 1 ให้ดำเนินการหมุนเวียน RR
  3. ถ้า BF(node) = -2 และ BF(node ​​-> right-child) = +1 ให้ดำเนินการหมุน RL
  4. ถ้า BF(node) = +2 และ BF(node ​​-> left-child) = -1 ให้ดำเนินการหมุน LR

การลบใน AVL Trees

การลบยังตรงไปตรงมามาก เราลบโดยใช้ตรรกะเดียวกันกับในแผนผังการค้นหาแบบไบนารีธรรมดา หลังจากลบ เราจะปรับโครงสร้างต้นไม้ใหม่ หากจำเป็น เพื่อรักษาความสูงที่สมดุล

ขั้นตอนที่ 1: ค้นหาองค์ประกอบในแผนภูมิ

ขั้นตอนที่ 2: ลบโหนด ตามการลบ BST

ขั้นตอนที่ 3: เป็นไปได้ 2 กรณี คือ:-

กรณีฮิต: การลบออกจากทรีย่อยที่ถูกต้อง

  • 1เอ ถ้า BF(node) = +2 และ BF(node ​​-> left-child) = +1 ให้ดำเนินการหมุน LL
  • 1B. ถ้า BF(node) = +2 และ BF(node ​​-> left-child) = -1 ให้ดำเนินการหมุน LR
  • 1ซี ถ้า BF(node) = +2 และ BF(node ​​-> left-child) = 0 ให้ดำเนินการหมุน LL

การลบใน AVL Trees

2 กรณี: กำลังลบจากแผนผังย่อยด้านซ้าย

  • 2เอ ถ้า BF(node) = -2 และ BF(node ​​-> right-child) = -1 ให้ดำเนินการหมุนเวียน RR
  • 2B. ถ้า BF(node) = -2 และ BF(node ​​-> right-child) = +1 ให้ดำเนินการหมุน RL
  • 2ซี. ถ้า BF(node) = -2 และ BF(node ​​-> right-child) = 0 ให้ดำเนินการหมุนเวียน RR

การลบใน AVL Trees

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
{
private:
    
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;
            }
    }

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

        p->left = tp->right;
        tp->right = p;

        return tp; 
    }


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

        p->right = tp->left;
        tp->left = p;

        return tp; 
    }


    struct node * rlrotation(struct node *n){
        struct node *p;
        struct node *tp;
        struct node *tp2;
        p = n;
        tp = p->right;
        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;
        struct node *tp;
        struct node *tp2;
        p = n;
        tp = p->left;
        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){
            struct node *n;
            n = new struct node;
            n->data = data;
            r = n;
            r->left = r->right = NULL;
            r->height = 1; 
            return r;             
        }
        else{
            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<<"\n"<<"Empty 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 *t;
        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 = llrotation(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();
            // to print the tree in level order
            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 จะสมดุลเสมอ ความสูงไม่เคยสูงเกิน log N โดยที่ N คือจำนวนโหนดทั้งหมดในแผนผัง
  • ทำให้มีความซับซ้อนของเวลาในการค้นหาดีขึ้นเมื่อเปรียบเทียบกับการค้นหาแบบไบนารีแบบธรรมดา
  • ต้นไม้ AVL มีความสามารถในการปรับสมดุลในตัวเอง

สรุป

  • ต้นไม้ AVL เป็นแผนผังการค้นหาแบบไบนารีที่ปรับสมดุลในตัวเอง
  • ปัจจัยสมดุลเป็นคุณลักษณะพื้นฐานของแผนผัง AVL
  • ปัจจัยความสมดุลของโหนดถูกกำหนดให้เป็นความแตกต่างระหว่างความสูงของแผนผังย่อยด้านซ้ายและขวาของโหนดนั้น
  • ค่าที่ถูกต้องของตัวประกอบความสมดุลคือ -1, 0 และ +1
  • การดำเนินการแทรกและการลบต้องดำเนินการหมุนหลังจากละเมิดปัจจัยสมดุล
  • ความซับซ้อนของเวลาในการแทรก การลบ และการค้นหาคือ O(log N)
  • ทรี AVL เป็นไปตามคุณสมบัติทั้งหมดของ Binary Search Trees
  • ทรีย่อยด้านซ้ายมีโหนดที่น้อยกว่าโหนดรูท ทรีย่อยที่ถูกต้องมีโหนดที่ใหญ่กว่าโหนดรูทเสมอ
  • มีการใช้ต้นไม้ AVL ในกรณีที่การดำเนินการค้นหาเกิดขึ้นบ่อยกว่าเมื่อเทียบกับการแทรกและการลบ