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

ต้นไม้ AVL คืออะไร?
AVL ต้นไม้ เป็นต้นไม้ค้นหาแบบไบนารี (Binary Search Tree: BST) ซึ่งความแตกต่างของความสูงระหว่างซับทรีด้านซ้ายและด้านขวาของทุกโหนดคือ -1, 0 หรือ +1 เป็น BST ที่ปรับสมดุลตัวเองได้และรักษาเวลาค้นหาแบบลอการิทึม โดยตั้งชื่อตามผู้คิดค้นคือ Adelson-Velsky และ Landis (AVL)
AVL Tree ทำงานอย่างไร
เพื่อให้เข้าใจว่าทำไม AVL Tree จึงมีอยู่ ลองดูว่าอะไรผิดพลาดเมื่อใช้ AVL Tree แบบธรรมดา ต้นไม้ค้นหาแบบไบนารีพิจารณาคีย์เหล่านี้ที่เสียบตามลำดับที่กำหนด:
การแสดงภาพต้นไม้ AVL
ต้นไม้จะเติบโตแบบเชิงเส้นเมื่อคีย์เข้ามาตามลำดับที่เพิ่มขึ้น ทำให้การค้นหากลายเป็น O(n) ซึ่งขัดกับวัตถุประสงค์ของ BST — มีเพียงต้นไม้ที่สมดุลเท่านั้นที่ทำให้การค้นหาเป็นแบบลอการิทึม ลองดูคีย์เดียวกันที่แทรกเข้ามาในลำดับที่แตกต่างกัน
การใช้คีย์เดียวกัน แต่ลำดับการแทรกต่างกัน จะทำให้โครงสร้างตื้นขึ้น ดังนั้นการค้นหาแต่ละครั้งจึงใช้เวลา O(log n) ต้นไม้ AVL บังคับใช้โครงสร้างนั้นโดยการตรวจสอบความสูงในการแทรกแต่ละครั้งและแก้ไขความไม่สมดุลโดยไม่ทำลายลำดับของ BST
ปัจจัยความสมดุลในต้นไม้ AVL
ปัจจัยสมดุล (BF) tracกำหนดค่า ks เป็นความสูงของแต่ละโหนด เพื่อให้ต้นไม้สามารถปรับสมดุลได้เองโดยอัตโนมัติ
คุณสมบัติของตัวประกอบสมดุล
ทรีสมดุลปัจจัย AVL
- ปัจจัยสมดุลคือผลต่างระหว่างความสูงของซับทรีด้านซ้ายและความสูงของซับทรีด้านขวา
Balance factor(node) = height(node->left) − height(node->right)- ค่าที่อนุญาตมีเพียง −1, 0 และ +1 เท่านั้น
- ค่า −1 หมายความว่าซับทรีด้านขวามีระดับเพิ่มขึ้นอีกหนึ่งระดับ — โหนดนั้นมีโครงสร้างแบบหนักไปทางขวา
- ค่า +1 หมายความว่าซับทรีด้านซ้ายมีระดับเพิ่มขึ้นอีกหนึ่งระดับ — โหนดนั้นมีโครงสร้างแบบเน้นด้านซ้าย (left-heavy)
- ค่า 0 หมายความว่าทั้งสองด้านมีความสูงเท่ากัน — จุดนั้นสมดุลอย่างสมบูรณ์
การหมุน AVL
การหมุนเวียนจะเกิดขึ้นเมื่อใดก็ตามที่การแทรกหรือการลบทำลายกฎของปัจจัยสมดุล กรณีทั้งสี่ได้แก่ LL, RR, LR และ RL
ซ้าย – การหมุนซ้าย
การหมุนนี้จะดำเนินการเมื่อมีการแทรกโหนดใหม่ที่ลูกด้านซ้ายของทรีย่อยด้านซ้าย
ต้นไม้ AVL ซ้าย – การหมุนซ้าย
มีการหมุนขวาเพียงครั้งเดียว กรณีนี้จะเกิดขึ้นเมื่อโหนดมีค่า BF +2 และโหนดลูกทางซ้ายมีค่า BF +1
ขวา - การหมุนขวา
การหมุนนี้จะดำเนินการเมื่อมีการแทรกโหนดใหม่ที่ลูกด้านขวาของแผนผังย่อยด้านขวา
มีการหมุนซ้ายเพียงครั้งเดียว กรณีนี้จะเกิดขึ้นเมื่อโหนดมี BF −2 และโหนดลูกทางด้านขวามี BF −1
หมุนขวา-ซ้าย
การหมุนนี้จะดำเนินการเมื่อมีการแทรกโหนดใหม่ที่ลูกด้านซ้ายของแผนผังย่อยด้านขวา
ฟังก์ชันนี้จะทำงานเมื่อ BF(node) = −2 และ BF(right-child) = +1 จากนั้นหมุนโหนดลูกทางขวาไปทางขวา แล้วหมุนโหนดหลักไปทางซ้าย
การหมุนซ้าย-ขวา
การหมุนนี้จะดำเนินการเมื่อมีการแทรกโหนดใหม่ที่ลูกด้านขวาของแผนผังย่อยด้านซ้าย
ฟังก์ชันนี้จะทำงานเมื่อ BF(node) = +2 และ BF(left-child) = −1 จากนั้นหมุนโหนดลูกทางซ้ายไปทางซ้าย แล้วหมุนโหนดหลักไปทางขวา
การแทรกใน AVL Trees
การแทรกข้อมูลแทบจะเหมือนกับการแทรกข้อมูลใน BST ทั่วไป หลังจากแทรกข้อมูลแต่ละครั้ง ต้นไม้จะเดินขึ้นไปและปรับสมดุลใหม่ การแทรกข้อมูลใช้เวลา O(log n) ในกรณีที่เลวร้ายที่สุด
การใช้งานการแทรกแผนผัง AVL
ขั้นตอนที่ 1: แทรกโหนดโดยใช้อัลกอริทึม BST มาตรฐาน ในตัวอย่างข้างต้น ให้แทรกโหนด 160
ขั้นตอนที่ 2: อัปเดตค่าตัวประกอบสมดุลของบรรพบุรุษทุกตัวตามเส้นทางการแทรก
ขั้นตอนที่ 3: หากโหนดบรรพบุรุษใดละเมิดช่วงค่าสมดุล ให้ทำการหมุนโหนดให้ตรงกัน ในตัวอย่างนี้ ค่าสมดุลของโหนด 350 ถูกละเมิด ดังนั้นการหมุนแบบ LL จะช่วยคืนความสมดุล
- If
BF(node) = +2และBF(left-child) = +1ทำการหมุน LL - If
BF(node) = −2และBF(right-child) = −1ทำการหมุน RR - If
BF(node) = −2และBF(right-child) = +1ทำการหมุน RL - 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
กรณีฮิต: ลบออกจากซับทรีด้านซ้าย
- 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
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); }
ตัวอย่างการใช้งานโค้ดด้านบน:
- คัดลอกโค้ดด้านบนแล้วบันทึกไว้ในไฟล์ชื่อ...
avl.cpp. - รวบรวมรหัส:
g++ avl.cpp -o run
- เรียกใช้รหัส
./run
ข้อดีของต้นไม้ AVL
- ความสูงของต้นไม้ AVL จะสมดุลอยู่เสมอและจะไม่สูงเกินระดับ N ที่กำหนดไว้
- การค้นหาเร็วกว่าโครงสร้างต้นไม้ค้นหาแบบไบนารีธรรมดา เนื่องจากต้นไม้ไม่สามารถเสื่อมสภาพได้
- ระบบจะปรับสมดุลอัตโนมัติ ไม่จำเป็นต้องทำการปรับแต่งใหม่
- ประสิทธิภาพที่แน่นอนเหมาะสำหรับระบบเรียลไทม์และดัชนีในหน่วยความจำ











