User Tools

Site Tools


Sidebar

1. บทนำ

2. การวิเคราะห์ Algorithm

3. List, Stack and Queue

4. Tree

5. Hashing

6. Priority Queues

7. การจัดเรียง (Sorting)

8. The Disjoint Set

9. Graph Algorithms

dsa:tavltree

4.4 AVL Trees

AVL (Adelson-Velskii and Landis) tree เป็น binary search tree กำกับด้วยเงื่อนไขของการสมดุล โดยเงื่อนไขการสมดุลจะต้องสามารถจัดการได้ง่ายและต้องประกันได้ว่าความลึกของทรีจะเป็น $O(log\ n)$ รูปแบบอย่างง่ายที่สุดคือการกำหนดให้ทรีย่อยซ้ายและทรีย่อยขวาของรากมีความสูง (height) เท่ากันดังรูปที่ 4.28 ซึ่งไม่ทำให้ทรีตามข้อกำหนดนี้ตื้นได้


รูปที่ 4.28 ไบนารีทรีที่กำหนดให้มีสมดุลที่โนดรากเท่านั้น

เงื่อนไขการสมดุลที่เป็นไปได้อีกอย่าง คือ กำหนดให้ทุก ๆ โนดในทรีจะต้องมีทรีย่อยซ้ายและทรีย่อยขวามีความสูงเท่ากัน ถ้ากำหนดให้ความสูงของทรีย่อยว่างใด ๆ (empty subtree) มีค่าเป็น -1 (ซึ่งใช้กันทั่วไป) แล้ว นั่นหมายความว่าจะมีก็แต่ เฉพาะทรีที่เป็น perfect balanced ซึ่งมีจำนวนโนดทั้งสิ้น 2k - 1 โนดเท่านั้นที่เป็นไปตามข้อกำหนดนี้ เงื่อนไขการสมดุลแบบนี้เข้มงวดเกินไปจนอาจใช้ประโยชน์ได้ไม่ดี ดังนั้นจึงมีการผ่อนคลายเงื่อนไขความสมดุลลงบ้าง

AVL tree เหมือนกับ binary search tree ยกเว้นมีข้อกำหนดเพิ่มเติมว่าทุก ๆ โนดในทรีจะต้องมีความสูงของทรีย่อยซ้ายและทรีย่อยขวาแตกต่างกันไม่เกิน 1 (กำหนดให้ความสูงของ empty tree มีค่าเป็น -1) รูปที่ 4.29 แสดงทรีที่ภาพด้านซ้ายเป็น AVL แต่ภาพทางขวาไม่เป็น


รูปที่ 4.29 ทรีที่ภาพด้านซ้ายเป็น AVL แต่ภาพทางขวาไม่เป็น

ด้วยข้อกำหนดเรื่องความสูงของโนดในทรีนี้ทำให้เราต้องจัดเก็บค่าความสูงของแต่ละโนดไว้ในโครงสร้างของโนดเองด้วย ได้มีการแสดงให้เห็นว่าความสูงที่มากที่สุดของ AVL มีค่าประมาณ $1.44\ log\ (N+2) – 0.328 $ แต่ในทางปฏิบัติมันจะมีค่ามากกว่า $log\ N$ อยู่เล็กน้อยเท่านั้น เช่น AVL ที่มีความสูงเท่ากับ 9 ที่ประกอบด้วยโนดจำนวนน้อยที่สุด (143) แสดงในรูปที่ 4.30 ทรีนี้มีทรีย่อยซ้ายเป็น AVL ขนาดเล็กสุดที่มีความสูงเท่ากับ 7 และมีทรีย่อยขวาที่มีขนาดเล็กสุดที่มีความสูง 8 นั่นหมายความว่า AVL ขนาดเล็กสุด (มีโนดจำนวนน้อยที่สุด) ที่มีความสูงเป็น $h$ คือ $S(h)$ นั้น คือ $$S(h) = S(h – 1) + S(h – 2) + 1 \text{โนด}$$

สำหรับ $h = 0$, มี $S(h) = 1$ และ $h = 1$, มี $S(h) = 2$


รูปที่ 4.30 AVL ที่มีความสูงเท่ากับ 9 ที่ประกอบด้วยโนดจำนวนน้อยที่สุด

ดังนั้น จากที่กล่าวมาการดำเนินการทั้งหมดของทรีจึงใช้เวลาเป็น $O(log\ N)$ ยกเว้นการบรรจุค่า (เรากำหนดให้การลบเป็นแบบ lazy deletion) เมื่อมีการทำงานการบรรจุค่า เราจำเป็นจะต้องปรับปรุงสารสนเทศที่เกี่ยวกับความสมดุลของโนดทุกโนดตามเส้นทางย้อยกลับไปถึงโนดรากของทรี แต่สาเหตุที่เกิดความยุ่งยากในการบรรจุค่าลงในทรีเกิดขึ้นเพราะมันอาจจะไปทำลายคุณสมบัติความเป็น AVL-tree ได้ (เช่นเมื่อมีการบรรจุ 6 ลงใน AVL-tree รูปที่ 4.29 จะทำให้เงื่อนไขความสมดุลเสียหายที่โนด 8) ซึ่งจะต้องจัดการให้กลับสู่สภาพความเป็น AVL และสามารถทำได้ด้วยการดำเนินการที่เรียกว่า rotation

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

ถ้าให้โนดที่ต้องการปรับสมดุลใหม่ชื่อโนด a เนื่องจากว่าโนดใด ๆ ในทรีมีโนดลูกได้มากที่สุดเพียงสองโนดเท่านั้นดังนั้นความไม่สมดุลของความสูงคือเมื่อทรีย่อย 2 ทรีย่อยของ a แตกต่างกัน 2 ระดับ นอกจากนี้เราจะเห็นได้ว่าการเสียสมดุลดังกล่าวนี้อาจจะเกิดขึ้นได้ไม่กรณีใดก็กรณีหนึ่งจาก 4 กรณี คือเกิดจากการบบรจุค่าใหม่ลงในตำแหน่ง ดังนี้: 1. การบรรจุเกิดขึ้นใน left subtree ของ left child ของ a 2. การบรรจุเกิดขึ้นใน right subtree ของ left child ของ a 3. การบรรจุเกิดขึ้นใน left subtree ของ right child ของ a 4. การบรรจุเกิดขึ้นใน right subtree ของ right child ของ a

กรณี 1 และ 4 เป็นกรณีสมมาตร mirror image symmetries กันเมื่อเทียบกับ a, เช่นเดียวกับกรณี 2 และ 3 –ดังนั้นโดยพื้นฐานแล้วจึงกล่าวได้ว่ามีกรณีที่จะเกิดขึ้นได้เพียง 2 กรณีเท่านั้น อย่างไรก็ตามในแง่ของโปรแกรมแล้วก็ยังคงต้องพิจารณาเป็น 4 กรณีอยู่ดี

กรณีที่ 1 เกิดขึ้นเมื่อมีการบรรจุโนดของค่าใหม่ลงทางด้านนอก (“outside”) (กล่าวคือเป็นการบรรจุที่เรียกว่า left-left หรือ right-right), ซึ่งสามารถจัดการแก้ไขได้ด้วยการหมุนโนดของทรีที่เรียกว่า single rotation กรณีที่ 2 เกิดขึ้นเมื่อมีการบรรจุโนดของค่าใหม่ลงทางด้านใน (“inside”) (กล่าวคือเป็นการบรรจุที่เรียกว่า left-right หรือ right-left) ซึ่งสามารถจัดการแก้ไขได้ด้วยการหมุนโนดของทรีที่เรียกว่า double rotation การดำเนินการหมุนโนดในทรีเป็นการดำเนินการพื้นฐานสำหรับอัลกอริทึมที่ใช้เพื่อการปรับสมดุลของทรีที่พบเห็นได้บ่อย

4.4.1 Single Rotation

รูปที่ 4.31 แสดงรูปแบบการหมุนโนดแบบ single rotation ที่ใช้แก้ไขการเสียสมดุลในกรณีที่ 1 โดยภาพด้านซ้ายเป็นภาพแสดงสภาวะก่อนการหมุนโนดและภาพด้านขวาเป็นภาพหลังการหมุนโนด จากภาพซ้ายจะเห็นว่าโนด k2 ผิดเงื่อนไขความสมดุลของ AVL เนื่องจากทรีย่อยซ้ายของมันมีความสูง (หรือมีความลึก) มากกว่าของทรีย่อยขวาของมันอยู่ 2 ระดับ (ดูจากเส้นแสดงระดับที่แสดงด้วยเส้นประในรูปหมายถึงระดับ) ภาพด้านซ้ายเป็นภาพที่เกิดขึ้นหลังจากมีการบรรจุโนดใหม่ลงในทรีย่อย X ซึ่งทำให้ทรีย่อยนี้ขยายลงไปอีกหนึ่งระดับและจะมีความลึกมากกว่าทรีย่อย Z อยู่ 2 ระดับ


รูปที่ 4.31 รูปแบบการหมุนโนดแบบ single rotation

ด้วยคุณสมบัติที่มันเป็น binary search tree หมายความว่า k2 > k1 ดังนั้นหลังการหมุนโนดแล้ว k2 ต้องกลายมาเป็นลูกทางขวาของ k1 ของทรีใหม่ซึ่งทำให้ k1 กลายเป็นรากของทรีใหม่ ทรีย่อย X ยังคงเป็นทรีย่อยซ้ายของ k1 และทรีย่อย Z ยังคงเป็นทรีย่อยขวาของ k2 เช่นเดิม ส่วนทรีย่อย Y ซึ่งประกอบด้วยโนดที่มีค่าอยู่ระหว่าง k1 และ k2 ในทรีเดิมก็จะสามารถจัดให้เป็นทรีย่อยซ้ายของ k2 ในทรีใหม่ได้ จะเห็นได้ว่าการทำการหมุนโนดที่กล่าวมานั้นความจริงแล้วเป็นเพียงการปรับเปลี่ยน pointer สองถึงสามครั้งเท่านั้น และทำให้โครงสร้างของทรีนั้น ๆ เปลี่ยนไปโดยยังคงรักษาคุณสมบัติการเป็น search tree ไว้ รูปที่ 4.32 แสดงการบรรจุค่า 6 ลงในทรี AVL ของภาพด้านซ้ายซึ่งทำให้โนด 8 เสียความสมดุล เราสามารถแก้ไขได้ด้วยการทำ single rotation ระหว่างโนด 7 และ 8 และจะได้ทรีใหม่ตามภาพทางขวามือซึ่งเป็นทรี AVL ดังที่ได้กล่าวมาก่อนหน้านี้แล้วว่ากรณี 4 เป็นกรณีสมมาตรของกรณี 1 ดังนั้นในกรณี 4 นี้ก็ทำการหมุนโนดด้วย single rotation เช่นเดียวกับกรณี 1 รูปที่ 4.33 แสดง single rotation ของกรณี 4


รูปที่ 4.32 การบรรจุค่า 6 ลงในทรี AVL ภาพซ้าย


รูปที่ 4.33 single rotation ของกรณี 4

ถ้าเริ่มต้นด้วย AVL tree ว่างและดำเนินการบรรจุค่า 3, 2, 1 และ 4 ถึง 7 เรียงตามลำดับ ปํญหาเกิดขึ้นครั้งแรกเมื่อมีการบรรจุค่า 1 เพราะจะทำให้เสียสมดุลที่โนดราก เราต้องทำ single rotation ระหว่างโนดรากกับโนดลูกตัวซ้ายของมัน ดังรูปก่อนและหลังการหมุนดังนี้

จากนั้นบรรจุ 4 และตามด้วยการบรรจุ 5 ซึ่งทำให้เสียสมดุลที่โนด 3 และจะต้องแก้ไขด้วย single rotation นอกจากการเปลี่ยนแปลงจะเกิดขึ้นในส่วนของโนดที่มีการหมุดแล้ว เราต้องไม่ลืมว่าในโปรแกรมที่ใช้ในการทำงานจริงนั้นต้องมีการเปลี่ยนแปลงส่วนอื่น ๆ ของทรีเกิดขึ้นด้วย ในกรณีของทรีนี้ต้องมีการเปลี่ยนโนดลูกตัวขวาของ 2 จากเดิมคือ 3 เป็นโนดใหม่คือ 4 ด้วย ดังรูปข้างล่างนี้

ต่อไปเป็นการบรรจุ 6 ทำให้เสียสมดุลที่โนดราก เนื่องจากทรีย่อยซ้าย (มีโนด 1 เป็นรากของทรีย่อย) มีความสูง 0 ส่วนทรีย่อยขวามีสูง 2 ดังนั้นเราต้องทำ single rotation ที่รากระหว่างโนด 2 และโนด 4

จากนั้นบรรจุ 7 ซึ่งต้องหมุนอีกครั้ง ดังรูปข้างล่าง

4.4.2. Double Rotation

อัลกอริทึมที่กล่าวมาแล้วนั้นมีปัญหาอย่างหนึ่ง กล่าวคือ พิจารณาจากรูปที่ 4.34 ซึ่งไม่สามารถใช้ในการแก้ปัญหาสำหรับกรณีที่ 2 หรือ กรณีที่ 3 ปัญหาที่เกิดคือ ทรีย่อย Y อยู่ในระดับลึกเกินไป และการหมุนโนดแบบ single rotation ไม่ได้ช่วยลดระดับความลึกลงไปได้ ในกรณีนี้จะต้องแก้ปัญหาด้วยการหมุนโนดแบบ double rotation ดังรูปที่ 4.35

การที่สามารถบรรจุค่าลงในทรีย่อย Y ของทรีในรูปที่ 4.34 ได้ก็หมายความว่ามันมีหน่วยข้อมูล (โนด) อยู่แล้วคือไม่เป็นทรีย่อยว่าง ดังนั้นเราจึงอนุมานได้ว่ามันต้องประกอบด้วยโนดรากและทรีย่อย 2 ทรีย่อย ซึ่งก็หมายความว่าเราสามารถพิจารณาทรีในรูป 4.34 นี้ได้ว่าเป็นทรีที่ประกอบด้วย 4 ทรีย่อยที่เชื่อมต่อด้วยโนด 3 โนด (ดูรูปที่ 4.35) จากรูป 4.35 จะเห็นว่าทรีย่อย B และ C อยู่ในระดับที่ลึกกว่าทรีย่อย D อยู่ 2 ระดับ เพียงแต่ไม่สามารถระบุให้แน่นอนได้จะเป็นทรีย่อยไหนแต่ก็ไม่มีความสำคัญว่าจะเป็นทรีย่อยไหน ดังนั้นในรูปที่ 4.35 จะแสดงให้ทั้งทรีย่อย B และ C อยู่ลึกว่าทรีย่อย D อยู่ 2 ระดับ


รูปที่ 4.34 การหมุนโนดแบบ single rotation ไม่สามารถใช้ในการแก้ปัญหาสำหรับกรณีที่ 2 หรือ กรณีที่ 3


รูปที่ 4.35 การหมุนโนดแบบ left-right double rotation ในการแก้ปัญหาสำหรับกรณีที่ 2


รูปที่ 4.36 การหมุนโนดแบบ right-left double rotation ในการแก้ปัญหาสำหรับกรณีที่ 3

เพื่อปรับสภาพความสมดุลของทรี เราไม่สามารถให้ k3 เป็นรากของทรีได้และการหมุนโนดระหว่าง k3 และ k1 แก้ปัญหาไม่ได้ดังที่แสดงในรูป 4.34 ดังนั้นจึงเหลือทางเลือกเดียวคือจัดให้ k2 เป็นโนดรากใหม่ นี่จะทำให้ k1 กลายเป็นโนดลูกซ้ายของ k2 และจัดให้ k3 เป็นโนดลูกขวาของ k2 ซึ่งจะเป็นตัวที่จะกำหนดตำแหน่งของทรีย่อยทั้งสี่ ทรีผลลัพธ์ที่ได้จะเป็นไปตามคุณสมบัติของ AVL รูปที่ 4.36 แสดงทรีของกรณี 3 ซึ่งแก้ไขได้ด้วย double rotation

ถ้าต้องบรรจุค่า 16, 15, .., 10 และตามด้วยค่า 8 และ 9 ตามลำดับลงใน AVL ต่อเนื่องจากตัวอย่าง AVL ที่ได้แสดงมาข้างบนนั้น เริ่มด้วยบรรจุ 16 ซึ่งไม่ทำให้คุณสมบัติความเป็น AVL เปลี่ยนแปลงไป ตามด้วยการบรรจุ 15 ซึ่งทำให้เสียสมดุลที่โนด 7 ในกรณีนี้คือกรณี 3 ซึ่งแก้ไขได้ด้วย right-left double rotation มีโนด 7, 16 และ 15 ในกรณีนี้โนด k1 ก็คือโนด 7, k3 คือโนด 16 และ k2 คือโนด 15 มีทรีย่อย A, B, C และ D เป็นทรีว่าง ดังนี้

จากนั้นบรรจุค่า 14 ซึ่งทำให้ต้องใช้ right-left double rotation ของโนด 6, 15, และ7 และในกรณีนี้โนด 6 จะเป็น k1 โนด 7 เป็น k2 และโนด 15 เป็นโนด k3 มีทรีย่อย A ที่มีโนดรากเป็นโนด 5 มีทรีย่อย B เป็นทรีย่อยว่างซึ่งเดิมก็คือลูกทางซ้ายของโนด 7 นั่นเอง ทรีย่อย C คือทรีที่มีโนดรากเป็นโนด 14 และสุดท้ายคือทรีย่อย D คือทรีที่มีโนดรากเป็นโนด 10 ดังรูปต่อไปนี้

เมื่อบรรจุ 13 จะทำให้เสียสมดุลที่รากของทรี จะเห็นว่าเนื่องจากค่า 13 ที่บบรจุใหม่นี้ไม่ใช่เป็นค่าที่อยู่ระหว่าง 4 กับ 7 ทำให้เรารู้ได้ว่าการหมุนโนดที่จะใช้คือการหมุนแบบ single rotation เท่านั้น ดังนี้

การบรรจุ 12 ต้องหมุนโนดด้วย single rotation ดังนี้

เมื่อบรรจุ 11 ต้องใช้ single rotation และหลังจากบรรจุ 10 ตามมาก็จะต้องทำ single rotation เช่นเดียวกัน จากนั้นบรรจุค่า 8 โดยไม่ต้องทำการหมุนโนดซึ่งจะได้ทรีดังนี้

สุดท้ายเป็นการบรรจุค่า 9 ซึ่งจะทำให้โนด 10 เสียสภาพสมดุล และจะเห็นว่าค่า 9 นี้เป็นค่าที่อยู่ระหว่างโนด 10 และ 8 ดังนั้นจึงต้องใช้ double rotation ระหว่างโนด 10, 8 และ 9 ซึ่งทำให้ได้ทรีดังนี้

การเขียนโปรแกรมค่อนข้างจะตรงไปตรงมาไม่ซับซ้อนเพียงแต่มีกรณีการเลือกหลายทางเลือกเท่านั้น ในการบรรจุโนดใหม่ที่มีค่า X เข้าใน AVL tree T เราจะทำงานในแบบ recursive เพื่อการบรรจุค่าใหม่นี้ลงใน subtree ของ T (ต่อไปเรียกว่า TLR) ถ้าความสูง (height) ของ TLR ไม่มีการเปลี่ยนแปลงก็เป็นอันเสร็จสิ้นการทำงาน แต่หากเกิดความไม่สมดุลของความสูงขึ้นใน T เราก็จะทำการ single หรือ double rotation แล้วแต่กรณีขึ้นอยู่กับค่า X และข้อมูลใน T และ TLR, จากนั้นจะทำการปรับปรุงค่าความสูงและจบสิ้นการทำงาน เนื่องจากโดยปกติแล้วการหมุนโนดเพียงครั้งเดียวก็เป็นการเพียงพอแล้ว ดังนั้นการเขียนโปรแกรมโดยไม่ต้องใช้รูปแบบ recursive ก็ทำงานได้เร็วกว่าโปรแกรมในแบบ recursive แต่ก็เขียนโปรแกรมได้ยากกว่าแบบ recursive

รูปที่ 4.37 แสดงโครงสร้างของโนดของ AVL ชื่อ AvlNode รูปที่ 4.38 แสดงฟังก์ชันเพื่อคำนวณค่าความสูงของโนด และรูปที่ 4.39 แสดงฟังก์ชันการบรรจุโนดของค่าใหม่ลงในทรี (ส่วนของโปรแกรมส่วนอื่น ๆ ของ AVLที่ไม่ได้กล่าวถึงในที่นี้ได้แสดงไว้ท้ายบทนี้)

class AvlNode 
    {   AvlNode( Comparable theElement )  {   this( theElement, null, null );  }
        AvlNode( Comparable theElement, AvlNode lt, AvlNode rt ) 
    {  element  = theElement;  left     = lt; right    = rt;  height   = 0;  }
        Comparable element;      	// The data in the node
        AvlNode    	left;         	// Left child
        AvlNode    	right;        	// Right child
        int        	height;       	// Height
    }

รูปที่ 4.37 การประกาศโนดของ AVL trees

private static int height( AvlNode t )
{   return t == null ? -1 : t.height;   } 

รูปที่ 4.38 Method สำหรับคำนวณความสูงของ AVL tree

private AvlNode insert( Comparable x, AvlNode t )
{   if  ( t == null )  
        t = new AvlNode( x, null, null );
    else  if ( x.compareTo( t.element ) < 0 )
    {   t.left = insert( x, t.left );
            if	(height(t.left) - height( t.right ) == 2 )
                if (x.compareTo(t.left.element) < 0 ) 
                    t = rotateWithLeftChild( t );
                else
                    t = doubleWithLeftChild( t );     
    }
    else  if ( x.compareTo( t.element ) > 0 ) 
    { t.right = insert( x, t.right );
        if (height(t.right) - height(t.left) == 2 )
            if (x.compareTo(t.right.element) >0)
                t = rotateWithRightChild( t );
            else
                t = doubleWithRightChild( t ); 
    }
    else
       ;  // Duplicate; do nothing
   t.height =  max( height( t.left ),
   height( t.right ) ) + 1;
   return t;
} 

รูปที่ 4.39 การบรรจุค่าลงใน AVL tree

รูปที่ 4.40 Single rotation

/*       * For AVL trees, this is a single rotation for case 4.
         * Update heights, then return new root.     */
        private static AvlNode rotateWithRightChild( AvlNode k1 )
        {
            AvlNode k2 = k1.right;
            k1.right = k2.left;
            k2.left = k1;
            k1.height = max( height( k1.left ), height( k1.right ) ) + 1;
            k2.height = max( height( k2.right ), k1.height ) + 1;
            return k2;
        } 

รูปที่ 4.41 ฟังก์ชันการหมุนโนด single rotation

รูปที่ 4.41 แสดงฟังก์ชันการหมุนโนด rotateWithLeftChild() ซึ่งจะเปลี่ยนทรีของรูปที่ 4.40 จากทรีซ้ายมือไปเป็นทรีในภาพขวามือและให้ค่ากลับเป็น reference ของรากของทรีใหม่


รูปที่ 4.42 Double rotation

/** For AVL trees, this is a double rotation for case 2.
* Update heights, then return new root.  */
 
private static  AvlNode doubleWithLeftChild ( AvlNode k3 )
{   k3.left = rotateWithRightChild( k3.left );
    return rotateWithLeftChild( k3 );
} 

รูปที่ 4.43 ฟังก์ชันการหมุนโนดแบบ double rotation

รูปที่ 4.43 แสดงฟังก์ชันการหมุนโนดแบบ double rotation ซึ่งจะเปลี่ยนทรีของรูปที่ 4.42 จากทรีซ้ายมือไปเป็นทรีในภาพขวามือและให้ค่ากลับเป็น reference ของรากของทรีใหม่

dsa/tavltree.txt · Last modified: 2023/09/04 09:16 by admin

Page Tools