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

⚡ สรุปอย่างชาญฉลาด

AVL Trees คือต้นไม้ค้นหาแบบไบนารีที่ปรับสมดุลตัวเอง โดยความแตกต่างของความสูงระหว่างซับทรีด้านซ้ายและด้านขวาของแต่ละโหนดจะอยู่ภายใน -1, 0 หรือ +1 ซึ่งรับประกันประสิทธิภาพการค้นหา O(log n)

  • 🌲 ความหมาย: ต้นไม้ค้นหาแบบไบนารีซึ่งค่าสมดุลของทุกโหนดอยู่ในช่วง {-1, 0, +1} ตั้งชื่อตามผู้คิดค้นคือ Adelson-Velsky และ Landis
  • 🇧🇷 ปัจจัยสมดุล: คำนวณจากความสูง (ซ้าย) − ความสูง (ขวา); ค่าที่อยู่นอกช่วง {-1, 0, +1} จะทำให้เกิดการหมุนเพื่อคืนสู่สมดุล
  • 🔄 การหมุน: สี่กรณี ได้แก่ LL, RR, LR และ RL จะปรับแนวโหนดใหม่หลังจากมีการแทรกหรือลบที่ไม่สมดุล เพื่อรักษาระดับความสูงของต้นไม้ให้เป็นแบบลอการิทึม
  • การแทรก: การใส่ BST มาตรฐาน ตามด้วยการเดินขึ้นด้านบนซึ่งจะคำนวณปัจจัยสมดุลใหม่ และทำการหมุนเพียงครั้งเดียวหรือสองครั้งเท่านั้น
  • การลบ: เหมือนกับการลบ BST แต่Hอาจส่งผลให้เกิดการหมุนหลายครั้งขึ้นไปตามต้นไม้ เนื่องจากความสูงของซับทรีสามารถลดลงได้ที่บรรพบุรุษทุกตัว
  • 🚀 การใช้งาน: ฐานข้อมูล ดัชนีในหน่วยความจำ เมตาเดต้าของระบบไฟล์ และโครงสร้างการค้นหาของ AI ใช้ AVL Tree เพื่อการค้นหาตามลำดับที่รวดเร็ว

AVL ต้นไม้

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

AVL ต้นไม้ เป็นต้นไม้ค้นหาแบบไบนารี (Binary Search Tree: BST) ซึ่งความแตกต่างของความสูงระหว่างซับทรีด้านซ้ายและด้านขวาของทุกโหนดคือ -1, 0 หรือ +1 เป็น BST ที่ปรับสมดุลตัวเองได้และรักษาเวลาค้นหาแบบลอการิทึม โดยตั้งชื่อตามผู้คิดค้นคือ Adelson-Velsky และ Landis (AVL)

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

เพื่อให้เข้าใจว่าทำไม AVL Tree จึงมีอยู่ ลองดูว่าอะไรผิดพลาดเมื่อใช้ AVL Tree แบบธรรมดา ต้นไม้ค้นหาแบบไบนารีพิจารณาคีย์เหล่านี้ที่เสียบตามลำดับที่กำหนด:

งานต้นไม้ AVL

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

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

งานต้นไม้ AVL

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

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

ปัจจัยสมดุล (BF) tracกำหนดค่า ks เป็นความสูงของแต่ละโหนด เพื่อให้ต้นไม้สามารถปรับสมดุลได้เองโดยอัตโนมัติ

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

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

ทรีสมดุลปัจจัย AVL

  • ปัจจัยสมดุลคือผลต่างระหว่างความสูงของซับทรีด้านซ้ายและความสูงของซับทรีด้านขวา
  • Balance factor(node) = height(node->left) − height(node->right)
  • ค่าที่อนุญาตมีเพียง −1, 0 และ +1 เท่านั้น
  • ค่า −1 หมายความว่าซับทรีด้านขวามีระดับเพิ่มขึ้นอีกหนึ่งระดับ — โหนดนั้นมีโครงสร้างแบบหนักไปทางขวา
  • ค่า +1 หมายความว่าซับทรีด้านซ้ายมีระดับเพิ่มขึ้นอีกหนึ่งระดับ — โหนดนั้นมีโครงสร้างแบบเน้นด้านซ้าย (left-heavy)
  • ค่า 0 หมายความว่าทั้งสองด้านมีความสูงเท่ากัน — จุดนั้นสมดุลอย่างสมบูรณ์

การหมุน AVL

การหมุนเวียนจะเกิดขึ้นเมื่อใดก็ตามที่การแทรกหรือการลบทำลายกฎของปัจจัยสมดุล กรณีทั้งสี่ได้แก่ LL, RR, LR และ RL

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

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

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

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

มีการหมุนขวาเพียงครั้งเดียว กรณีนี้จะเกิดขึ้นเมื่อโหนดมีค่า BF +2 และโหนดลูกทางซ้ายมีค่า BF +1

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

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

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

มีการหมุนซ้ายเพียงครั้งเดียว กรณีนี้จะเกิดขึ้นเมื่อโหนดมี BF −2 และโหนดลูกทางด้านขวามี BF −1

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

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

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

ฟังก์ชันนี้จะทำงานเมื่อ BF(node) = −2 และ BF(right-child) = +1 จากนั้นหมุนโหนดลูกทางขวาไปทางขวา แล้วหมุนโหนดหลักไปทางซ้าย

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

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

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

ฟังก์ชันนี้จะทำงานเมื่อ BF(node) = +2 และ BF(left-child) = −1 จากนั้นหมุนโหนดลูกทางซ้ายไปทางซ้าย แล้วหมุนโหนดหลักไปทางขวา

การแทรกใน AVL Trees

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

การแทรกใน AVL Trees

การใช้งานการแทรกแผนผัง 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 Trees

การลบจะใช้ตรรกะเดียวกับ BST ทั่วไป และจะปรับสมดุลใหม่หลังจากนั้น

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

ขั้นตอนที่ 2: ลบโหนดโดยใช้วิธีการลบแบบมาตรฐานของ BST

ขั้นตอนที่ 3: มีความเป็นไปได้สองกรณี

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

  • 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 Trees

กรณีฮิต: ลบออกจากซับทรีด้านซ้าย

  • 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 Trees

C++ ตัวอย่างของต้นไม้ AVL

ด้านล่างคือ C++ โปรแกรมที่ใช้โครงสร้างข้อมูล AVL Trees:

#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 Tree คือต้นไม้ค้นหาแบบไบนารีที่ปรับสมดุลตัวเองได้ โดยที่ค่าสมดุลของทุกโหนดจะคงอยู่ในช่วง {-1, 0, +1} การหมุนโหนดจะคืนค่าคงที่นี้ทุกครั้งที่มีการแทรกหรือลบข้อมูลping ค้นหา แทรก และลบข้อมูลด้วยความเร็ว O(log n)

ค่าสมดุลของโหนดเท่ากับความสูง (ซับทรีด้านซ้าย) ลบด้วยความสูง (ซับทรีด้านขวา) ค่าต้องอยู่ในช่วง {-1, 0, +1} ค่าสมดุลที่เป็น +2 หรือ -2 แสดงว่าการแทรกหรือการลบทำให้โหนดนั้นเสียสมดุล และจำเป็นต้องหมุนโหนด

การหมุนทั้งสี่แบบได้แก่ LL, RR, LR และ RL โดย LL ใช้การหมุนขวาเพียงครั้งเดียว RR ใช้การหมุนซ้ายเพียงครั้งเดียว และ LR กับ RL เป็นการหมุนสองครั้งที่รวมการหมุนหนึ่งครั้งบนองค์ประกอบลูกกับการหมุนในทิศทางตรงกันข้ามบนโหนด

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

โครงสร้างข้อมูลแบบ AVL Tree มีความสมดุลอย่างเคร่งครัด โดยมีปัจจัยความสมดุลไม่เกินหนึ่ง ทำให้การค้นหาข้อมูลเร็วขึ้น ในขณะที่โครงสร้างข้อมูลแบบ Red-Black Tree อนุญาตให้มีความสมดุลที่หลวมกว่า ทำให้การแทรกและการลบข้อมูลมีต้นทุนต่ำกว่า แต่การค้นหาจะช้าลงเล็กน้อย ฐานข้อมูลนิยมใช้ Red-Black Tree สำหรับการโหลดข้อมูลที่มีการเขียนจำนวนมาก

โครงสร้างข้อมูล AVL Tree เป็นหัวใจสำคัญของดัชนีฐานข้อมูลในหน่วยความจำ เมตาเดตาของระบบไฟล์ คิวลำดับความสำคัญ การค้นหาในสมุดโทรศัพท์ โปรแกรมตรวจสอบการสะกดคำ และเวิร์กโหลดใดๆ ที่ต้องการการค้นหาแบบกำหนดได้ O(log n) บวกกับการท่องไปตามลำดับสำหรับการค้นหาแบบช่วง

ใช่แล้ว ระบบ AI ใช้ AVL Tree สำหรับตารางสัญลักษณ์ ที่เก็บคุณลักษณะแบบเรียงลำดับ การปรับสมดุล kd tree และการค้นหาเพื่อนบ้านที่ใกล้ที่สุดในข้อมูลที่มีโครงสร้าง นอกจากนี้ยังเป็นพื้นฐานของดัชนีการเรียกค้นแบบจัดอันดับในกระบวนการค้นหาอัจฉริยะอีกด้วย

ใช่แล้ว GitHub Copilot และผู้ช่วย AI ที่คล้ายกันจะสร้างโครงสร้างพื้นฐานสำหรับการแทรก ลบ และหมุนภาพใน GitHub C++, Javaหรือ Pythonและสร้างการทดสอบหน่วยที่ตรวจสอบว่าค่าสมดุลคงที่ในทุกการดำเนินการ

สรุปโพสต์นี้ด้วย: