7. SORTING
ในบทนี้กล่าวถึงการจัดเรียงค่า (sorting) ใน array เพื่อให้เข้าใจปัญหาง่ายขึ้น สมาชิกใน array เป็นเลขจำนวนเต็ม จำนวนข้อมูลสามารถบรรจุลงในหน่วยความจำหลักได้ทั้งหมด นั่นคือ การจัดเรียงสามารถทำได้ในหน่วยความจำ เรียกว่า internal sorting การจัดเรียงที่ไม่สามารถทำได้ในหน่วยความจำแต่ต้องทำใน disk หรือ tape เรียกว่า external sorting จะกล่าวในตอนท้ายของบท
7 SORTING สำหรับ internal sorting จะแสดง · Algorithm อย่างง่ายที่ใช้ O(N2) เช่น insertion sort · Algorithm ที่ค่อนข้างง่าย คือ Shellsort มี running time เป็น o(N2) · Algorithm ที่ค่อนข้างซับซ้อนที่มี running time O(N log N)
7.1 บทนำ (Preliminaries) Algorithm ที่ใช้ได้รับการส่งผ่าน array ที่บรรจุสมาชิกอยู่ในทุกตำแหน่งที่จะทำการจัดเรียง และประกอบด้วยจำนวนสมาชิก N ตัว object ที่จะจัดเรียงนั้นเป็นชนิด (type) Comparable ดังนั้นจึงใช้ CompareTo method เพื่อจัดลำดับของ input และเป็นการดำเนินการเดียวที่มีได้กับ input data นอกจาก assignments การจัดเรียงแบบนี้เรียกว่า comparison-based sorting
7.2. Insertion Sort 7.2.1. Algorithm ประกอบด้วยการทำงาน N – 1 รอบ (passes) สำหรับจากรอบที่ p = 2 ถึง N ก็จะประกันได้ว่าสมาชิกที่อยู่ในตำแหน่ง 1 ถึง p จะถูกจัดอยู่ในลำดับที่ถูกต้องแล้ว นำค่า ตำแหน่ง p ไปใส่ในที่ที่เหมาะสม ในตำแหน่งที่ 1 ถึง p - 1 ที่ถูกจัดเรียงเรียบร้อยแล้ว Sorted partial result unsorted data ≤𝑥 >𝑥 𝑥 … Sorted partial result unsorted data ≤𝑥 𝑥 >𝑥 …
Figure 7.1 Insertion sort after each pass 7.2.1. The Algorithm จากรูป Figure 7.1 ในรอบ p เราเคลื่อนสมาชิกของตำแหน่ง p ไปทางซ้ายจนพบตำแหน่งที่ถูกต้องในระหว่างสมาชิก p +1 ตัวแรก Original 34 8 64 51 32 21 Positions Moved ------------------------------------------------------------------------------ After p = 1 8 34 64 51 32 21 1 After p = 2 8 34 64 51 32 21 0 After p = 3 8 34 51 64 32 21 1 After p = 4 8 32 34 51 64 21 3 After p = 5 8 21 32 34 51 64 4 Figure 7.1 Insertion sort after each pass
7.2.1. The Algorithm public static void insertionSort( Comparable [ ] a ) { int j; /* 1*/ for( int p = 1; p < a.length; p++ ) /* 2*/ Comparable tmp = a[ p ]; /* 3*/ for (j=p; j>0 && tmp.compareTo(a[j-1])<0; j--) /* 4*/ a[ j ] = a[ j - 1 ]; /* 5*/ a[ j ] = tmp; } Figure 7.2 Insertion sort
7.2.2. การวิเคราะห์ Insertion Sort เนื่องจากโปรแกรมใช้ loop ซ้อน และแต่ละคำสั่ง loop อาจทำงานเป็นจำนวน N รอบ ดังนั้น insertion sort เป็น O(N2) running time นี้เป็นขอบเขตที่มั่นคง เนื่องจากถึงแม้อินพุตจะมีลำดับที่เรียงกลับกันก็จะยังได้ running time นี้ การทดสอบค่าในบรรทัดที่ 3 ทำมากที่สุด p+1 ครั้งสำหรับแต่ละค่า p เมื่อรวมทุกค่า p จะได้ 𝑖=2 𝑁 𝑖 =2+3+4+…+𝑁=Θ( 𝑁 2 )
7.2.2. Analysis of Insertion Sort อีกด้านหนึ่ง ถ้าอินพุตมีการจัดเรียงไว้เรียบร้อยแล้วแต่แรก อัลกอริทึมนี้ก็จะมี running time เป็น O(N) เนื่องจากว่าคำสั่งทดสอบในลูป for ด้านในจะล้มเหลวในทันที ความจริงแล้วถ้าอินพุตถูกจัดเรียงมาก่อนเกือบทั้งหมดแล้ว insertion sort ก็จะทำงานเร็วมาก เนื่องจากความหลากหลายของข้อมูลอินพุตเองทำให้การวิเคราะห์กรณีเฉลี่ยได้ประโยชน์มากกว่า และสำหรับกรณีเฉลี่ยแล้วอัลกอริทึมนี้จะมี running time เป็น (N2) ซึ่งจะกล่าวถึงต่อไป
7.3 ขอบเขตล่าง (Lower Bound) ของอัลกอริทึมอย่างง่ายของการจัดเรียง คำว่า inversion ใน array ของตัวเลขคือ คู่อันดับใด ๆ (ordered pair) (i, j) ที่มีคุณสมบัติ i < j แต่มี a[i] > a[j] จากตัวอย่าง Figure 7.1 อินพุตคือ 34, 8, 64, 51, 32, 21 มี inversions อยู่ 9 ตัว คือ (34,8), (34,32), (34,21), (64,51), (64,32), (64,21), (51,32), (51,21) และ (32,21) คือจำนวนครั้งที่ต้องใช้ในการสลับค่าที่ใช้ใน insertion sort ซึ่งเป็นจริงเสมอ เนื่องจากการสลับตำแหน่งสมาชิกสองตัวที่อยู่ติดกันจะทำให้ inversion ตัวหนึ่งหายไป และข้อมูลที่มีการจัดเรียงเป็นข้อมูลที่ไม่มี inversions
7.3. A Lower Bound for Simple Sorting Algorithms เนื่องจากใน algorithm ต้องทำงานอื่น ๆ อีก O(N) ดังนั้น running time ของ insertion sort จึงเป็น O(I + N), เมื่อ I คือจำนวน inversions ของอินพุต ดังนั้น insertion sort จึงใช้เวลาเป็น linear time ถ้าจำนวน inversions เป็น O(N) เราสามารถคำนวณขอบเขตกรณีเฉลี่ยของ running time ของ insertion sort ได้แม่นยำมากขึ้นด้วยการคำนวณหาจำนวนเฉลี่ยของ inversions ใน permutation สมมุติให้ไม่มีค่าซ้ำ เราสามารถสมมุติฐานได้ว่าอินพุต ก็คือ permutation ของตัวเลขจำนวนเต็มจำนวน N ตัว และตำแหน่งของตัวเลขมีโอกาสเกิดได้เท่า ๆ กัน ด้วยสมมุติฐานนี้ จึงได้ทฤษฎีต่อไปนี้:
7.3. A Lower Bound for Simple Sorting Algorithms THEOREM 7.1. ค่าเฉลี่ยของจำนวน inversions ในอะเรย์ของตัวเลขจำนวน N ตัวที่ไม่ซ้ำกัน คือ N (N – 1) / 4 PROOF: สำหรับรายการ, L, ของตัวเลข ให้พิจารณา Lr, ซึ่งเป็นรายการที่มีค่าเรียงย้อนกลับกัน (reverse order) เช่นรายการ 21, 32, 51, 64, 34, 8 เป็นค่าเรียงย้อนกลับของตัวอย่างที่แล้ว คู่ของตัวเลขใด ๆ ในรายการ (x, y) โดยที่ y > x นั้นแน่นอนว่าต้องเป็นของ L หรือ Lr ตัวใดตัวหนึ่ง และคู่อันดับนี้ คือ inversion นั่นเอง จำนวนทั้งหมดของคู่อันดับเหล่านี้อยู่ในรายการ L และ reverse ของมัน (คือ Lr) มีจำนวนเท่ากับ N(N - 1)/2 ดังนั้นค่าเฉลี่ยของมันคือ ครึ่งหนึ่งซึ่งก็คือ N(N -1)/4 นั่นเอง
7.3. A Lower Bound for Simple Sorting Algorithms จากทฤษฎีหมายความว่า insertion sort มี running time สำหรับกรณีเฉลี่ยเป็น quadratic และยังเป็นขอบเขตล่างของอัลกอริทึมการจัดเรียงใด ๆ ก็ตามที่ใช้การสลับค่าของรายการที่อยู่ติดกันด้วย THEOREM 7.2 อัลกอริทึมการจัดเรียงที่ใช้การสลับค่าที่อยู่ติดกันจะมี running time กรณีเฉลี่ยเป็น (N2) PROOF: จำนวนเฉลี่ยของ inversions คือ N(N - 1)/4 ซึ่งก็คือ (N2) การสลับค่าแต่ละครั้งจะลด inversion ลงหนึ่งค่า ดังนั้นทั้งหมดจึงต้องใช้การสลับค่า (N2) ครั้ง
7.3. A Lower Bound for Simple Sorting Algorithms ขอบเขตล่างที่กล่าวมานั้นแสดงให้เห็นว่าถ้าต้องการจัดเรียงด้วย running time ที่ต่ำกว่า quadratic (subquadratic) หรือ o(N2) อัลกอริทึมนั้นจะต้องทำการเปรียบเทียบและสลับคู่ของค่าสมาชิกที่อยู่ห่างกันออกไป อัลกอริทึมการจัดเรียงที่กำจัด inversions และทำงานได้อย่างมีประสิทธิภาพกว่าจะต้องเป็นอัลกอริทึมที่สามารถกำจัด inversion ได้มากกว่าหนึ่งต่อการสลับค่าหนึ่งครั้ง
7.4 Shellsort Shellsort ตั้งตามชื่อของผู้คิดค้นการจัดเรียงแบบนี้ คือ Donald Shell และเป็นอัลกอริทึมแรกที่ทำลายขอบเขตเวลาที่เป็น quadratic ในขณะทำงานแต่ละ phase นั้น shell sort ใช้การเปรียบเทียบค่าที่อยู่ในตำแหน่งที่ห่างกัน ระยะห่างดังกล่าวนี้จะลดลงลง เรื่อยๆ จนกระทั่งถึงขั้นตอนสุดท้ายที่เป็นการเปรียบเทียบค่าที่อยู่ติดกัน ด้วยเหตุที่ระยะห่างของค่าที่นำมาเปรียบเทียบกันลดลงในระหว่างการทำงานของอัลกอริทึมนี้เอง จึงเรียกShellsort อีกอย่างว่า diminishing increment sort
รูป Figure 7.3 แสดงอะเรย์หลังการทำงานเฟสต่าง ๆ ของ Shellsort Shellsort ใช้การลำดับของ h1, h2, . . . , ht ซึ่งเรียกว่า increment sequence และลำดับที่ใช้จะมีค่าลักษณะใดก็ได้เพียงแต่มีเงื่อนไขว่า h1 = 1 เท่านั้น แต่แน่นอนว่าบางลำดับจะทำงานได้ดีกว่าบางลำดับ (จะกล่าวถึงอีกครั้ง) ในการทำงานแต่ละ phase ที่ใช้ลำดับการเพิ่ม hk ผลที่ได้ คือ สำหรับแต่ละค่า i เราจะได้ว่า a[i] a[i + hk] กล่าวคือสมาชิกทุกตัวที่มีระยะห่างกัน hk จะอยู่ในลำดับที่มีการจัดเรียงอย่างถูกต้อง ซึ่งเรียกว่า hk-sorted file รูป Figure 7.3 แสดงอะเรย์หลังการทำงานเฟสต่าง ๆ ของ Shellsort
Figure 7.3 Shellsort หลังการทำงานแต่ละ pass คุณสมบัติที่สำคัญของ Shellsort คือ การทำ hk-sorted แล้วตามด้วย hk-1-sorted นั้นยังคงสภาพของ hk-sorted Original 81 94 11 96 12 35 17 95 28 58 41 75 15 -------------------------------------------------------------------------------------- After 5-sort 35 17 11 28 12 41 75 15 96 58 81 94 95 After 3-sort 28 12 11 35 15 41 58 17 94 75 81 96 95 After 1-sort 11 12 15 17 28 35 41 58 75 81 94 95 96 Figure 7.3 Shellsort หลังการทำงานแต่ละ pass
รูป Figure 7.4 เป็นโปรแกรมที่ใช้ลำดับการเพิ่มนี้ 7.4. Shellsort วิธีการทั่วไปในการทำ hk-sort ก็คือ สำหรับแต่ละตำแหน่ง i ใน hk , hk + 1, hk + 2, . . . , N – 1 ให้จัดวางสมาชิกในตำแหน่งที่ถูกต้องภายในตำแหน่ง i, i - hk, i - 2hk, etc. สิ่งสำคัญคือ การทำงานของ hk-sort นั้นเป็นการทำ insertion sort กับอะเรย์ย่อย hk ที่เป็นอิสระกัน ลำดับการเพิ่มที่นิยมใช้ (แต่ไม่ดี) คือลำดับการเพิ่มที่เสนอโดย Shell คือลำดับ ht = N/2, และ hk = hk+1/2 รูป Figure 7.4 เป็นโปรแกรมที่ใช้ลำดับการเพิ่มนี้ จะกล่าวถึงลำดับการเพิ่มที่ให้ประสิทธิภาพการทำงานที่ดีกว่าในตอนต่อไป
Figure 7.4 Shellsort routine ใช้ลำดับการเพิ่มของ Shell public static void shellsort( Comparable [ ] a ) { int j; /* 1*/ for( int gap = a.length / 2; gap > 0; gap /= 2 ) /* 2*/ for( int i = gap; i < a.length; i++ ) /* 3*/ Comparable tmp = a[ i ]; /* 4*/ for( j = i; j >= gap && tmp.compareTo( a[ j - gap ] ) < 0; j -= gap ) /* 5*/ a[ j ] = a[ j - gap ]; /* 6*/ a[ j ] = tmp; }
7.4.1. การวิเคราะห์กรณี Worst-Case ของ Shellsort running time ของ Shellsort ขึ้นอยู่กับการเลือกใช้ลำดับการเพิ่ม และการพิสูจน์ก็มีความยุ่งยากมาก การพิสูจน์กรณีเฉลี่ยของ Shellsort ก็ยังเป็นที่ถกเถียงกันอยู่ยกเว้นกรณีที่ใช้ลำดับการเพิ่มพื้น ๆ เท่านั้น เราจะกล่าวถึงขอบเขตของกรณี worst-case สำหรับลำดับการเพิ่ม 2 แบบ
7.4.1. การวิเคราะห์กรณี Worst-Case ของ Shellsort THEOREM 7.3 running time กรณี worst-case ของ Shellsort ที่ใช้ลำดับการเพิ่มของ Shell คือ (N2) PROOF: ในการพิสูจน์ไม่เพียงแต่ต้องแสดงให้เห็นถึงขอบเขตบนของ running time กรณี worst-case เท่านั้น แต่ยังต้องแสดงว่ามีบางอินพุตที่ต้องใช้เวลาในการทำงานเป็น (N2) ด้วย ในตอนแรกจะพิสูจน์ขอบเขตล่างก่อน (lower bound) โดยการสร้างกรณีข้อมูลไม่ดีขึ้น ก่อนอื่น เลือกค่า N ให้เป็นเลขยกกำลังของ 2 ซึ่งจะทำให้ลำดับการเพิ่มทั้งหมดเป็นเลขคู่ ยกเว้นลำดับการเพิ่มสุดท้ายที่มีค่าเท่ากับ 1
7.4.1. การวิเคราะห์กรณี Worst-Case ของ Shellsort และกำหนดให้อินพุตเป็นอะเรย์ input_data ที่มีกลุ่มที่มีค่ามากที่สุดจำนวน N/2 ตัวอยู่ในตำแหน่งคู่และกลุ่มที่มีค่าน้อยที่สุดอีกจำนวน N/2 ตัวอยู่ในตำแหน่งเลขคี่ เมื่อการทำงานมาถึงรอบสุดท้ายกลุ่มตัวเลขที่มากที่สุด N/2 ตัวก็ยังคงอยู่ในตำแหน่งเลขคู่ และกลุ่มตัวเลขที่น้อยที่สุดจำนวน N/2 ตัวก็ยังคงอยู่ในตำแหน่งคี่ ดังนั้นก่อนการเริ่มทำงานในรอบสุดท้ายค่าตำแหน่งที่ i ซึ่ง i <= N/2 มีค่าเป็นตำแหน่ง 2i – 1 เพื่อที่จะทำให้สมาชิกในตำแหน่งที่ i อยู่ในตำแหน่งที่ถูกต้องของมันเราต้องเคลื่อนย้ายมันเป็นระยะ i – 1 ตำแหน่งภายในอะเรย์
7.4.1. การวิเคราะห์กรณี Worst-Case ของ Shellsort ดังนั้น เพื่อจัดวางให้ตัวเลขที่น้อยที่สุดจำนวน N/2 ตัวอยู่ในตำแหน่งที่ถูกต้องของมัน เราต้องใช้การทำงานอย่างน้อย 𝑖=1 𝑁/2 𝑖−1 =Ω( 𝑁 2 ) รูป Figure 7.5 แสดงอินพุตเมื่อ N = 16 จำนวน inversion ยังคงมีอยู่ภายหลัง 2-sort คือ 1+2+3+4+5+6+7 = 28 ซึ่งในการทำงานรอบสุดท้ายจะยังคงใช้เวลามากทีเดียว
Figure 7.5 Bad case for Shellsort with Shell's increments Start 1 9 2 10 3 11 4 12 5 13 6 14 7 15 8 16 -------------------------------------------------------------------------------------- After 8-sort 1 9 2 10 3 11 4 12 5 13 6 14 7 15 8 16 After 4-sort 1 9 2 10 3 11 4 12 5 13 6 14 7 15 8 16 After 2-sort 1 9 2 10 3 11 4 12 5 13 6 14 7 15 8 16 After 1-sort 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
7.4.1. การวิเคราะห์กรณี Worst-Case ของ Shellsort สำหรับขอบเขตบน คือ O(N2) นั้น ดังที่ได้กล่าวแล้วว่า รอบที่มีลำดับการเพิ่มเป็น hk นั้นใช้ hk insertion sort ที่ทำกับสมาชิกจำนวน N/hk ตัว เนื่องจาก insertion sort เป็น quadratic ดังนั้นในแต่ละรอบจึงใช้เวลาทั้งหมดเป็น O(hk(N/hk)2) = O(N2/hk) ดังนั้นเมื่อรวมทั้งหมดแล้วจะได้ Ο( 𝑖=1 𝑡 𝑁 2 ℎ 𝑖 ) =Ο( 𝑁 2 𝑖=1 𝑡 1 ℎ 𝑖 ) และเนื่องจากรูปแบบลำดับการเพิ่มเป็น geometric series ที่มี common ratio เป็น 2 และเทอมที่มากที่สุดคือ h1 = 1 และ ดังนั้นจึงได้ O(N2)
7.4.1. Worst-Case Analysis of Shellsort Hibbard เสนอลำดับการเพิ่มที่แตกต่างไปเล็กน้อยซึ่งให้ผลในทางปฏิบัติที่ดีกว่าเล็กน้อยลำดับการเพิ่มดังกล่าว คือ 1, 3, 7, . . . , 2k – 1 THEOREM 7.4 running time กรณี worst-case ของ Shellsort ที่ใช้ลำดับการเพิ่มของ Hibbard คือ (N3/2)
ล7.4.1. Worst-Case Analysis of Shellsort นอกจากนี้ยังมีผู้เสนอลำดับการเพิ่มแบบอื่น ๆ ซึ่งให้ผล running time ที่แตกต่างกันออกไปมากบ้างน้อยบ้าง อีกทั้งการวิเคราะห์ก็มีความซับซ้อนมากต้องใช้ทฤษฎีขั้นสูงทางคณิตศาสตร์ เช่น ลำดับการเพิ่มที่เสนอโดย Sedgewick มี running time เป็น O(N4/3) ในกรณี worst-case ประสิทธิภาพการทำงานของ Shellsort เป็นที่ยอมรับกันในทางปฏิบัติ ถึงแม้ขนาดอินพุตจะมีจำนวนหลายหมื่นหน่วยข้อมูล การที่ Shellsort เขียนโปรแกรมง่ายทำให้เป็นที่นิยมใช้เพื่อจัดเรียงข้อมูลที่มีขนาดไม่ใหญ่มากนัก
7.5. Heapsort ในบทที่ 6 ได้กล่าวแล้วว่าเราอาจใช้ priority queues ในการจัดเรียงด้วยเวลา O(N log N) algorithm ที่ใช้พื้นฐานแนวคิดนี้ เรียกว่า heapsort และมี Big-Oh running time ดีกว่า algorithmอื่น ๆ ที่กล่าวมาแล้ว วิธีการ คือ สร้าง binary heap (มีสมาชิก N ตัว) ซึ่งใช้เวลา O(N) จากนั้นทำ deleteMin N ครั้ง สมาชิกที่ถูกย้ายออกไปจาก heap ตัวแรก คือตัวที่มีค่าน้อยที่สุด และเรียงลำดับตามค่าไป แล้วนำไปเก็บใน array อีกตัวหนึ่งจากนั้นก็คัดลอกกลับไปยัง array เดิมซึ่งก็จะเป็นคำตอบในการจัดเรียง เนื่องจากการทำ deleteMin แต่ละตัวใช้ O(log N) ดังนั้น running time รวมทั้งหมด คือ O(N log N)
7.5. Heapsort ปัญหาของ algorithmนี้ คือ ต้องใช้อะเรย์เพิ่มเข้ามา นั่นคือต้องใช้พื้นที่หน่วยความจำเพิ่มเป็นสองเท่า และอาจก่อปัญหาได้ในบางกรณี เพื่อหลีกเลี่ยงการใช้ array ตัวที่สองดังกล่าว เราพบว่าหลังจากการ deleteMin แต่ละครั้งนั้นขนาดของ heap ก็จะลดลง 1 ตัวด้วย ดังนั้นตำแหน่งเซลล์ใน heap ที่ว่างนั้นก็สามารถนำมาเก็บค่าตัวที่เพิ่งจะถูกย้ายออกไปนั้นได้ เช่น ถ้าเรามี heap ที่มีสมาชิก 6 ตัว การทำ deleteMin ครั้งแรกจะได้ a1 และทำให้ heap เหลือสมาชิก 5 ตัว ดังนั้นเราก็สามารถบรรจุ a1 ลงในตำแหน่งที่ 6 ที่ว่างนั้นได้ การ deleteMin ถัดมาได้ a2 และ heap จะมีสมาชิกเหลือเพียง 4 ตัว และเราสามารถบรรจุ a2 ลงในตำแหน่ง 5 ได้
เราจะใช้ (max)heap ในการ implement ของเราและยังคงใช้ array เช่นเดิม 7.5. Heapsort หลังการทำงานจนเสร็จ array ที่ได้ก็จะเป็น array ของสมาชิกที่จัดเรียงจากมากไปน้อย ถ้าต้องการจัดเรียงจากน้อยไปมาก เราก็จะใช้ heap ที่ parent มีค่ามากกว่า child ของมัน นั่นคือ (max)heap เราจะใช้ (max)heap ในการ implement ของเราและยังคงใช้ array เช่นเดิม ขั้นแรกสร้าง heap ด้วย linear time จากนั้นทำ deleteMaxes จำนวน N - 1 ครั้ง ด้วยการสลับสมาชิกตัวสุดท้ายใน heap กับสมาชิกตัวแรก แล้วลดขนาดของ heap ลงแล้วทำการ percolating down หลังจบ จะได้ array ที่มีสมาชิกเรียงตามลำดับค่า
Figure 7.6 (Max) heap หลังการ build_heap 97 59 53 59 53 58 26 41 58 31 26 41 31 97 97 53 59 26 41 58 31 1 2 3 4 5 6 7 8 9 10 59 53 58 26 41 31 97 1 2 3 4 5 6 7 8 9 10 Figure 7.7 Heap หลัง deleteMax ครั้งแรก Figure 7.6 (Max) heap หลังการ build_heap
private static int leftChild( int i ) { return 2 private static int leftChild( int i ) { return 2 * i + 1; } private static void percDown( Comparable [ ] a, int i, int n ) { int child; Comparable tmp; /* 1*/ for ( tmp = a[ i ]; leftChild( i ) < n; i = child ) /* 2*/ child = leftChild( i ); /* 3*/ if ( child != n - 1 && a[ child ].compareTo( a[ child + 1 ] ) < 0 ) /* 4*/ child++; /* 5*/ if ( tmp.compareTo( a[ child ] ) < 0 ) /* 6*/ a[ i ] = a[ child ]; else /* 7*/ break; } /* 8*/ a[ i ] = tmp; Figure 7.8 Heapsort
/. Standard heapsort. @param a an array of Comparable items /** * Standard heapsort. * @param a an array of Comparable items. */ public static void heapsort( Comparable [ ] a ) { /* 1*/ for( int i = a.length / 2; i >= 0; i-- ) /* buildHeap */ /* 2*/ percDown( a, i, a.length ); /* 3*/ for ( int i = a.length - 1; i > 0; i-- ) /* 4*/ swapReferences( a, 0, i ); /* deleteMax */ /* 5*/ percDown( a, 0, i ); } Figure 7.8 Heapsort
7.6 Mergesort Mergesort มี running time เป็น O(N log N) กรณี worst-case และใช้จำนวนครั้งในการเทียบค่าที่เกือบจะ optimal และเป็นตัวอย่างที่ดีของอัลกอริทึมแบบ recursive การทำงานพื้นฐาน คือการทำการ merge (ควบรวม)รายการที่มีการจัดเรียงไว้แล้ว 2 รายการ เนื่องจากรายการที่จะนำมา merge นั้นเป็นรายการที่มีการจัดเรียงไว้แล้ว ดังนั้นจึงสามารถทำได้ด้วยการอ่าน input เข้าเพียงรอบเดียวและนำผลไปไว้ในรายการที่สามแยกไปต่างหาก
รูปต่อไปนี้เป็นตัวอย่างของการควบรวมอินพุตสองชุด 7.6. Mergesort Algorithm ในการ merge จะใช้ array ที่เป็น input สองชุดคือ array a และ b , array สำหรับ outputคือ array c, และตัวนับสามตัว คือ aptr, bptr, และ cptr ซึ่งตั้งค่าให้อยู่ที่จุดเริ่มต้นของarray แต่ละตัวตามลำดับ ทำการคัดลอกค่าที่น้อยกว่าระหว่างค่าของ a[aptr] และ b[bptr] ไปใส่ลงใน c แล้วเพิ่มค่าของตัวนับตัวที่ถูกคัดลอกและตัวนับของ c เมื่อสิ้นสุด array ตัวใดตัวหนึ่งก็ให้ทำการคัดลอกสมาชิกที่เหลือของอีก array หนึ่งไปไว้ใน c รูปต่อไปนี้เป็นตัวอย่างของการควบรวมอินพุตสองชุด
7.6. Mergesort 1 13 24 26 2 15 27 38 aptr bptr cptr 1 13 24 26 2 15 27 38 aptr bptr cptr 1 13 24 26 2 15 27 38 aptr bptr cptr 1 13 24 26 2 15 27 38 aptr bptr cptr 1 13 24 26 2 15 27 38 aptr bptr cptr
7.6. Mergesort 1 13 24 26 2 15 27 38 aptr bptr cptr 1 13 24 26 2 15 27 38 aptr bptr cptr 1 13 24 26 2 15 27 38 aptr bptr cptr
อัลกอริทึมของ mergesort อธิบายได้ดังนี้ เวลาที่ใช้ในการ merge รายการสองรายการที่จัดเรียงไว้แล้วเป็นแบบ linear เนื่องจากต้องใช้การเปรียบเทียบค่าอย่างมากที่สุด N - 1 ครั้งเมื่อ N คือจำนวนสมาชิกทั้งหมด อัลกอริทึมของ mergesort อธิบายได้ดังนี้ ถ้า N = 1 คือมีสมาชิกตัวเดียวก็จะได้คำตอบในทันที ถ้า N > 1 ก็จะทำ mergesort แบบ recursive โดยทำกับครึ่งแรกก่อนแล้วจึงทำกับครึ่งหลัง การทำเช่นนี้จะได้รายการที่มีการจัดเรียงครึ่งหนึ่ง 2 ชุดซึ่งสามารถนำมา merge กันด้วย algorithm สำหรับการ merge ที่กล่าวมาแล้วข้างบน
Figure 7.9 Mergesort routine public static void mergeSort( Comparable [ ] a ) { Comparable [ ] tmpArray = new Comparable[ a.length ]; mergeSort( a, tmpArray, 0, a.length - 1 ); } private static void mergeSort( Comparable [ ] a, Comparable [ ] tmpArray, int left, int right ) if( left < right ) int center = ( left + right ) / 2; mergeSort( a, tmpArray, left, center ); mergeSort( a, tmpArray, center + 1, right ); merge( a, tmpArray, left, center + 1, right ); Figure 7.9 Mergesort routine
private static void merge( Comparable [ ] a, Comparable [ ] tmpArray, int leftPos, int rightPos, int rightEnd ) { int leftEnd = rightPos - 1; int tmpPos = leftPos; int numElements = rightEnd - leftPos + 1; while( leftPos <= leftEnd && rightPos <= rightEnd ) // Main loop if( a[ leftPos ].compareTo( a[ rightPos ] ) <= 0 ) tmpArray[ tmpPos++ ] = a[ leftPos++ ]; else tmpArray[ tmpPos++ ] = a[ rightPos++ ]; while( leftPos <= leftEnd ) // Copy rest of first half while( rightPos <= rightEnd ) // Copy rest of right half for( int i = 0; i < numElements; i++, rightEnd-- ) // Copy tmpArray back a[ rightEnd ] = tmpArray[ rightEnd ]; } Figure 7.10 Merge routine
7.6.1 การวิเคราะห์ Mergesort เราต้องหา recurrence relation ของ running time สมมุติให้ N เป็นเลขยกกำลังของ 2 ดังนั้นจึงสามารถแบ่งครึ่งได้เสมอ สำหรับ N = 1 เวลาที่ใช้ใน mergesort เป็นค่าคงที่ ซึ่งเราจะให้เป็น 1 ถ้า N > 1 เวลาที่ใช้ใน mergesort สำหรับ N จำนวนจะเท่ากับเวลาที่ใช้ในการทำ mergesorts แบบ recursive จำนวนสองครั้งกับข้อมูลขนาด N/2 บวกกับเวลาที่ใช้ในการควบรวมซึ่งเป็น linear ดังนั้น จะได้สมการดังนี้ T(1) = 1 T(N) = 2T(N/2) + N = O(N log N) (บทที่ 2)
7.7. Quicksort quicksort ใช้อัลกอริทึมแบบ recursive ที่เรียกว่า divide-and-conquer เช่นเดียวกับ mergesort และมี average running time เป็น O(N log N) อย่างไรก็ตามกรณี worst-case ใช้ O(N2) อัลกอริทึมที่ใช้ในการจัดเรียงอะเรย์ S มี 4 ขั้นตอน ดังนี้: 1. ถ้าจำนวนสมาชิกของ S มี 0 หรือ 1 ก็จบการทำงาน (return) 2. เลือกสมาชิกตัวหนึ่ง (v) ใน S ใช้เป็น pivot 3. ให้แบ่งสมาชิกที่เหลือใน S - {v} ออกเป็นสองกลุ่มคือ S1 = {x S - {v}| x v}, และ S2 = {x S - {v}| x v} 4. ให้ค่ากลับเป็น { quicksort(S1) ตามด้วย v และตามด้วย quicksort(S2)}
Figure 7.11 แสดง quicksort ของกลุ่มตัวเลขหนึ่ง เราคาดหวังว่าจำนวนสมาชิกประมาณครึ่งหนึ่งจะมีค่าน้อยกว่าค่า pivot จะถูกย้ายไปไว้ใน S1 และอีกครึ่งหนึ่งจะถูกย้ายไปไว้ใน S2 Figure 7.11 แสดง quicksort ของกลุ่มตัวเลขหนึ่ง เลือก pivot (บังเอิญ) เป็น 65 แบ่งสมาชิกที่เหลือออกเป็นสองกลุ่ม จัดเรียงกลุ่มข้อมูลย่อยทางด้านน้อยกว่าแบบ recursive จะได้ 0, 13, 26, 31, 43, 57 จัดเรียงกลุ่มข้อมูลย่อยทางด้านมากกว่าในลักษณะเดียวกัน ก็จะได้ข้อมูลจัดเรียงทั้งหมด
Figure 7.11 ขั้นตอน quicksort 75 81 43 57 31 13 92 65 26 Select pivot 75 81 43 57 31 13 92 65 26 partition 43 75 13 31 65 81 26 92 57 quicksort quicksort 0 13 26 31 43 57 65 75 81 92 0 13 26 31 43 57 65 75 81 92 Figure 7.11 ขั้นตอน quicksort
7.7.1 การเลือก Pivot การเลือกที่ผิด 7.7. Quicksort 7.7.1 การเลือก Pivot การเลือกที่ผิด คือ การเลือกสมาชิกตัวแรกเป็น pivot การเลือกแบบนี้อาจจะยอมรับได้ถ้าข้อมูลเป็นแบบสุ่มจริง ๆ แต่ถ้าข้อมูลเริ่มต้นเป็นข้อมูลที่ได้รับการจัดเรียงหรือจัดเรียงย้อนกลับมาก่อนแล้ว การเลือกค่า pivot นี้ก็เป็นการเลือกที่แย่มาก เพราะสมาชิกทั้งหมดจะถูกย้ายเข้าไปใน S1 หรือใน S2 ผลที่เกิดขึ้นก็คือ quicksort จะใช้เวลาเป็น quadratic ซึ่งเป็นสิ่งไม่พึงปรารถนา การเลือก pivot ให้เป็นค่าเฉลี่ยของสองค่าแรกก็ให้ผลที่ไม่แตกต่างจากที่กล่าวมาแล้ว
7.7.1 การเลือก Pivot วิธีที่ปลอดภัย วิธีที่ดูเหมือนปลอดภัยในการเลือก pivot คือใช้การเลือกแบบสุ่ม วิธีนี้เป็นวิธีที่ปลอดภัยที่สุดถ้าหากว่า random number generator ไม่มีข้อบกพร่องใด ๆ (ซึ่งความจริงมักจะเกิดขึ้นบ่อย ๆ) แต่ในอีกด้านหนึ่ง โดยทั่วไปแล้ว random number generation เป็นการทำงานที่ต้องเสียค่าใช้จ่าย (เวลา) มากและทำให้ไม่ช่วยในการลดเวลาการทำงานของอัลกอริทึมโดยรวม
7.7. Quicksort ใช้ Median-of-Three วิธีที่ดีที่สุดในการเลือก pivot น่าจะใช้ median ของข้อมูล แต่โชคไม่ดีที่เป็นการคำนวณที่ยุ่งยากมากและจะทำให้การทำงานของ quicksort ช้าลงอย่างมาก วิธีที่ใช้กันทั่วไป คือใช้ค่า median ของสมาชิกตัวซ้ายสุด, ตัวขวาสุดและตัวที่อยู่ตำแหน่งกลางของข้อมูล เช่น อินพุต 8, 1, 4, 9, 6, 3, 5, 2, 7, 0 มีสมาชิกตัวซ้ายสุดคือ 8 ตัวขวาสุด คือ 0 และตัวกลาง (ตำแหน่ง (left + right)/2) คือ 6 ดังนั้น pivot คือ v = 6
7.7. Quicksort 7.7.2 การแบ่ง Partition การแบ่งอะเรย์อินพุตมีวิธีการทำอยู่หลายวิธี วิธีที่เสนอนี้เป็นเพียงวิธีการหนึ่งเท่านั้นที่ให้ผลค่อนข้างดี ขั้นแรกเป็นการย้ายค่า pivot ออกจากอะเรย์โดยการสลับค่ามันกับสมาชิกตัวสุดท้าย ให้ i เริ่มต้นที่สมาชิกตัวแรกและ j เริ่มต้นที่สมาชิกรองจากตัวสุดท้าย รูปข้างล่างนี้แสดงสถานะในขณะนี้ 8 1 4 9 0 3 5 2 7 6 i j
7.7.2 การแบ่ง Partition สิ่งที่ต้องการในการทำ partition คือ ต้องการย้ายสมาชิกที่มีค่าน้อยกว่า pivot ทั้งหมดไปอยู่ทางด้านซ้ายของ array และสมาชิกทุกตัวที่มีค่ามากกว่าค่า pivot ไปอยู่ทางด้านขวาของ array ในขณะที่ i อยู่ด้ายซ้ายของ j เราจะเคลื่อน i ไปทางขวาโดยจะผ่านค่าที่น้อยกว่า pivot ไป และ จะเลื่อน j ไปทางซ้ายโดยจะผ่านค่าที่มากกว่า pivot ไป เมื่อ i และ j หยุดเลื่อนหมายความว่าเวลานั้น i อยู่ที่ค่าที่มากกว่า pivot และ j อยู่ที่ค่าที่น้อยกว่า pivot ถ้า i ยังคงอยู่ทางซ้ายของ j ก็ให้สลับค่าระหว่างค่าที่ i และ j จากรูป i จะไม่เลื่อนแต่ j จะเลื่อนไปหนึ่งตำแหน่ง ดังนี้
7.7.2 การแบ่ง Partition 8 1 4 9 0 3 5 2 7 6 i j After First Swap 2 1 4 9 0 3 5 8 7 6 i j Before Second Swap i j After Second Swap 2 1 4 5 0 3 9 8 7 6 i j
7.7.2 การแบ่ง Partition Before Third Swap 2 1 4 5 0 3 9 8 7 6 j i ณ เวลานี้ i และ j เลื่อนผ่านกัน ดังนั้นจึงไม่ต้องสลับค่า ขั้นตอนสุดท้ายคือการสลับค่าระหว่าง pivot กับค่าที่ i After Swap with Pivot 2 1 4 5 0 3 6 8 7 9 i pivot
7.7.2 การแบ่ง Partition มีรายละเอียดที่สำคัญที่ยังไม่ได้กล่าวถึงเลย คือ จะทำอย่างไรกับค่าที่เท่ากันกับค่าของ pivot ปัญหาก็คือ i และ j ควรจะหยุดหรือไม่เมื่อมันพบกับสมาชิกที่มีค่าเท่ากับ pivot ความจริงแล้วทั้ง i และ j ควรจะทำเหมือน ๆ กัน เพื่อไม่ให้เกิดการเอนเอียงในการทำ partition กล่าวคือ ถ้าให้ i หยุดแล้วไม่ให้ j หยุด ก็จะทำให้ค่าทั้งหมดที่เท่ากับ pivot ถูกย้ายไปใน S2
พิจารณากรณีที่ทุกค่าเท่ากันหมด ดังนี้ 7.7.2 การแบ่ง Partition พิจารณากรณีที่ทุกค่าเท่ากันหมด ดังนี้ ถ้าทั้ง i และ j หยุด running time ก็จะเป็น O(N log N) ถ้าทั้ง i และ j ไม่หยุดก็จะไม่มีการสลับค่าใด ๆ เกิดขึ้นและจะทำให้การแบ่งส่วนข้อมูลไม่สม่ำเสมอ และจะมี running time เป็น O(N2) ดังนั้น ทางที่ดีจึงควรต้องหยุดและทำการสลับค่า (ถึงแม้ค่าจะเท่ากันก็ตาม) เพื่อจะได้แบ่งข้อมูลออกเป็นสองส่วนเท่า ๆ กัน และ running time ไม่เป็น quadratic
จำนวนที่เหมาะสมที่เหลือ คือเมื่อ N อยู่ระหว่าง 5 ถึง 20 7.7. Quicksort 7.7.3 ข้อมูลขนาดเล็ก สำหรับข้อมูลขนาดเล็ก (N 20) quicksort ทำงานได้ไม่ดีเท่า insertion sort ดังนั้น จึงไม่ควรใช้ quicksort ควรใช้ quicksort จนกระทั่งเหลือข้อมูลจำนวนน้อย ๆ จำนวนหนึ่งที่ไม่ถูกจัดเรียงแล้วใช้ insertion sort ทำงานต่อ เนื่องจาก insertion sort ทำงานได้ดีกับข้อมูลที่มีการจัดเรียงเกือบจะทั้งหมดแล้ว และการทำแบบนี้ทำให้ทำงานเร็วขึ้นประมาณ 15% จำนวนที่เหมาะสมที่เหลือ คือเมื่อ N อยู่ระหว่าง 5 ถึง 20
Figure 7.12 Driver สำหรับ quicksort รูป Figure 7.13 เป็นโปรแกรมเพื่อทำ median-of-three โปรแกรมรูป Figure 7.14 เป็นส่วนที่ทำ quicksort รวมทั้งการ partition และการเรียกใช้แบบ recursive public static void quicksort( Comparable [ ] a ) { quicksort( a, 0, a.length - 1 ); } Figure 7.12 Driver สำหรับ quicksort
Figure 7.13 โปรแกรมสำหรับ median-of-three private static Comparable median3( Comparable [ ] a, int left, int right ) { int center = ( left + right ) / 2; if( a[ center ].compareTo( a[ left ] ) < 0 ) swapReferences( a, left, center ); if( a[ right ].compareTo( a[ left ] ) < 0 ) swapReferences( a, left, right ); if( a[ right ].compareTo( a[ center ] ) < 0 ) swapReferences( a, center, right ); swapReferences( a, center, right - 1 ); // Place pivot at right - 1 return a[ right - 1 ]; }
swapReferences /** * Method to swap to elements in an array. * @param a an array of objects. * @param index1 the index of the first object. * @param index2 the index of the second object. */ public static final void swapReferences( Object [ ] a, int index1, int index2 ) { Object tmp = a[ index1 ]; a[ index1 ] = a[ index2 ]; a[ index2 ] = tmp; }
Figure 7.14 Main quicksort routine private static void quicksort( Comparable [ ] a, int left, int right ) { /* 1*/ if( left + CUTOFF <= right ) /* 2*/ Comparable pivot = median3( a, left, right ); // Begin partitioning /* 3*/ int i = left, j = right - 1; /* 4*/ for( ; ; ) /* 5*/ while( a[ ++i ].compareTo( pivot ) < 0 ) { } /* 6*/ while( a[ --j ].compareTo( pivot ) > 0 ) { } /* 7*/ if( i < j ) /* 8*/ swapReferences( a, i, j ); else /* 9*/ break; } Figure 7.14 Main quicksort routine
Figure 7.14 Main quicksort routine /*10*/ swapReferences( a, i, right - 1 ); // Restore pivot /*11*/ quicksort( a, left, i - 1 ); // Sort small elements /*12*/ quicksort( a, i + 1, right ); // Sort large elements } else // Do an insertion sort on the subarray /*13*/ insertionSort( a, left, right ); /* 3*/ int i = left + 1, j = right - 2; /* 4*/ for( ; ; ) { /* 5*/ while( a[ i ].compareTo( pivot ) < 0 ) i++;} /* 6*/ while( a[ j ].compareTo( pivot ) > 0 ) j--; /* 7*/ if( i < j ) /* 8*/ swapReferences( a, i, j ); else /* 9*/ break; } Figure 7.15
Internal insertion sort routine /** * Internal insertion sort routine for subarrays used by quicksort. * @param a an array of Comparable items. * @param left the left-most index of the subarray. * @param right the right-most index of the subarray. */ private static void insertionSort( Comparable [ ] a, int left, int right ) { for( int p = left + 1; p <= right; p++ ) { Comparable tmp = a[ p ]; int j; for( j = p; j > left && tmp.compareTo( a[ j - 1 ] ) < 0; j-- ) a[ j ] = a[ j - 1 ]; a[ j ] = tmp; } Internal insertion sort routine
7.7.5 การวิเคราะห์ Quicksort ในการวิเคราะห์ต้องแก้สมการ recurrence สมมุติให้ใช้ pivot แบบสุ่ม (คือ ไม่ใช้ median-of-three partitioning) และไม่ใช้ cutoff สำหรับข้อมูลขนาดเล็ก ให้ T(0) = T(1) = 1 running time ของ quicksort เท่ากับ running time ของการเรียกใช้แบบ recursive สองครั้ง บวกกับ linear time ที่ใช้สำหรับการทำ partition (การเลือก pivot ใช้เวลาคงที่) ได้ relation สำหรับ quicksort พื้นฐาน ดังนี้ T(N) = T( i ) + T( N - i – 1 ) + cN (7.1)
7.7.5. Analysis of Quicksort Worst-Case Analysis เกิดขึ้นได้เมื่อค่า pivot เป็นค่าที่น้อยที่สุดตลอดเวลา ดังนั้น i = 0 และถ้าเราไม่สนใจค่าที่ T(0) = 1 ซึ่งไม่มีความสำคัญ จะได้สมการ recurrence ดังนี้ T(N) = T(N - 1) + cN, N > 1 (7.2) ทำ telescope สมการ (7.2) ดังนี้ T(N - 1) = T(N - 2) + c(N - 1) T(N - 2) = T(N - 3) + c(N - 2) ... T(2) = T(1) + c(2) รวมทุกสมการเข้าด้วยกัน จะได้ 𝑇 𝑁 =𝑇 1 +𝑐 𝑖=2 𝑁 𝑖 =𝑂( 𝑁 2 )
Best-Case Analysis ในกรณี best case มีค่า pivot อยู่ตรงกลาง เพื่อให้ง่ายขึ้น เราให้ array ทั้งสองเท่ากับครึ่งหนึ่งของ array เดิม ดังนั้น T(N) = 2T(N/2) + cN (7.7) T(N) = cN log N + N = O(N log N)
Average-Case Analysis กรณีเฉลี่ยเป็นกรณีที่ยุ่งยากมาก สมมุติให้ขนาดต่างๆ ที่จะเกิดขึ้นได้ของ S1 นั้นมีโอกาสเกิดขึ้นได้เท่า ๆ กันทุกขนาด และดังนั้นจึงมีความน่าจะเป็น (probability) 1/N การสมมุติเช่นนี้หมายความว่าการ partition และการเลือก pivot เป็นแบบสุ่ม จากข้อสมมุติเช่นนี้ ค่าเฉลี่ยของ T(i), และ (เช่นกัน) ของ T(N - i -1), คือ ( 1 𝑁 ) 𝑗=0 𝑁−1 𝑇(𝑗) และจากสมการ (7.1) จะได้ 𝑇 𝑁 = 2 𝑁 𝑗=0 𝑁−1 𝑇(𝑗) +𝑐𝑁 (7.14)
Average-Case Analysis พิจารณาประเด็นต่อไปนี้ ถ้า T(N) เป็นค่าเฉลี่ยของค่าใช้จ่าย quicksort จำนวนสมาชิก N ตัว ค่าเฉลี่ยของค่าใช้จ่ายในการเรียกใช้แบบ recursive แต่ละครั้งเท่ากับค่าเฉลี่ยของของความเป็นไปได้ทั้งหมดของขนาดทุกขนาดของปัญหาย่อย ดังนั้น ค่าเฉลี่ยของการเรียกใช้แบบ recursive ของปัญหาย่อยทางซ้ายและขวาจึ่งเป็น: 𝑇 𝐿 =𝑇 𝑅 = 𝑇 0 +𝑇 1 +𝑇 2 +…𝑇(𝑁−1) 𝑁
Average-Case Analysis 𝑁𝑇 𝑁 =2 𝑗=0 𝑁−1 𝑇(𝑗) +𝑐 𝑁 2 (7.15) ทำ telescope สมการข้างบนหนึ่งครั้ง (𝑁−1)𝑇 𝑁−1 =2 𝑗=0 𝑁−2 𝑇(𝑗) +𝑐 (𝑁−1) 2 (7.16) ลบ (7.16) ออกจาก (7.15), จะได้ (7.17) 𝑁𝑇 𝑁 − 𝑁−1 𝑇 𝑁−1 =2𝑇 𝑁−1 +2𝑐𝑁 −𝑐 จัดเทอมและตัดเทอมที่ไม่สำคัญ คือ -c ออก จะได้
Average-Case Analysis (7.18) 𝑁𝑇 𝑁 = 𝑁+1 𝑇 𝑁−1 +2𝑐𝑁 ได้สูตรสำหรับ T(N) ในเทอมของ T(N -1) เท่านั้น จากนี้ใช้ telescope แต่ต้องจัดรูปของสมการ (7.18) ให้ถูกต้องก่อนด้วยการหารสมการ (7.18) ด้วย N(N + 1): 𝑇(𝑁) 𝑁+1 = 𝑇(𝑁−1) 𝑁 + 2𝑐 𝑁+1 (7.19) จากนี้ ทำ telescope ต่อไป
Average-Case Analysis 𝑇(𝑁) 𝑁+1 = 𝑇(𝑁−1) 𝑁 + 2𝑐 𝑁+1 = 𝑇(𝑁−2) 𝑁−1 + 2 𝑁 + 2𝑐 𝑁+1 = ⋮ = 𝑇(1) 2 +2𝑐 𝑖=3 𝑁+1 1 𝑖 ≈2 𝑖=1 𝑁 1 𝑖 =2 ln 𝑁 = O(n log n)
7.7.6 Linear-Expected-Time ของปัญหาการเลือก เราสามารถปรับปรุง quicksort เพื่อแก้ปัญหาการเลือก (selection problem) ที่กล่าวถึงก่อนหน้านี้ การใช้ priority queue สามารถค้นหาค่าที่มากที่สุดอันดับที่ k ด้วยเวลา O(N + k log N) ในที่นี้จะกล่าวถึงอัลกอริทึมที่เกือบจะเหมือนกับอัลกอริทึมของ quicksort เพื่อใช้ในการค้นหาค่าที่น้อยเป็นอันดับ k ในกลุ่มข้อมูล S โดยที่สามขั้นตอนแรกของอัลกอริทึมนั้นเหมือนกันกับของ quicksort และเรียกอัลกอริทึมนี้ว่า quickselect
7.7.6 Linear-Expected-Time ของปัญหาการเลือก ให้ |Si| เป็นจำนวนสมาชิกใน Si อัลกอริทึมของ quickselect คือ 1. ถ้า |S| = 1, นั่นคือ k = 1 และให้ค่ากลับเป็นสมาชิกใน S เป็นคำตอบ ถ้าใช้การ cutoff สำหรับข้อมูลขนาดเล็กและ |S| CUTOFF ก็ให้จัดเรียง S และให้ค่ากลับเป็นสมาชิกตัวที่น้อยเป็นอันดับ k เลือกค่า pivot ซึ่ง v S. ทำ Partition S - {v} ไปเป็น S1 และ S2, ดังที่ทำกับ quicksort
7.7.6. A Linear-Expected-Time Algorithm for Selection ถ้า k |S1| นั่นคือสมาชิกที่มีค่าน้อยอันดับ k ต้องอยู่ใน S1 กรณีนี้ให้ส่งค่ากลับเป็น quickselect (S1, k) ถ้า k = 1 + |S1| นั่นคือ ค่า pivot เป็นค่าที่น้อยเป็นอันดับ k ก็ให้ค่ากลับเป็นคำตอบ ถ้าไม่เป็นไปตามกรณีข้างบน ก็หมายความว่า ค่าที่น้อยเป็นอันดับ k นั้นอยู่ใน S2 ซึ่งเป็นสมาชิกตัวที่น้อยเป็นอันดับที่ (k - |S1| - 1) ใน S2 ให้ทำการ recursive call และให้ค่ากลับเป็น quickselect (S2, k - |S1| - 1)
7.7.6. A Linear-Expected-Time Algorithm for Selection จะเห็นว่า quickselect ทำการเรียกใช้ฟังก์ชันแบบ recursive เพียงครั้งเดียวเท่านั้น (ในขณะที่ quicksort เรียก recursive 2 ครั้ง) สำหรับกรณี worst case ของ quickselect จะเท่ากันกับของ quicksort คือ O(N2) เนื่องจากกรณี worst case ของ quicksort นั้นเกิดขึ้นเมื่อ S1 หรือ S2 เป็นเซ็ตว่างนั่นเอง ส่วนกรณีเฉลี่ยของ quickselect เป็น O(N) โปรแกรม quickselect แสดงในรูป Figure 7.16 เมื่อทำงานเสร็จสิ้นข้อมูลที่มีค่าน้อยเป็นอันดับ k จะอยู่ที่ตำแหน่ง k – 1 (เนื่องจากดัชนีเริ่มที่ 0)
Figure 7.16 Main quickselect routine private static void quickSelect( Comparable [ ] a, int left, int right, int k ) { /* 1*/ if( left + CUTOFF <= right ) /* 2*/ Comparable pivot = median3( a, left, right ); // Begin partitioning /* 3*/ int i = left, j = right - 1; /* 4*/ for( ; ; ) /* 5*/ while( a[ ++i ].compareTo( pivot ) < 0 ) { } /* 6*/ while( a[ --j ].compareTo( pivot ) > 0 ) { } /* 7*/ if( i < j ) /* 8*/ swapReferences( a, i, j ); else /* 9*/ break; } Figure 7.16 Main quickselect routine
Figure 7.16 Main quickselect routine /*10*/ swapReferences( a, i, right - 1 ); // Restore pivot /*11*/ if( k <= i ) /*12*/ quickSelect( a, left, i - 1, k ); /*13*/ else if( k > i + 1 ) /*14*/ quickSelect( a, i + 1, right, k ); } else // Do an insertion sort on the subarray /*15*/ insertionSort( a, left, right ); Figure 7.16 Main quickselect routine
7.8 ขอบเขตล่างทั่วไปของการจัดเรียง ในหัวข้อนี้จะพิสูจน์ให้เห็นว่าอัลกอริทึมใด ๆ ของการจัดเรียงที่ใช้เฉพาะการเปรียบเทียบค่าจะต้องใช้จำนวนครั้งการเปรียบเทียบค่าเป็น (N log N) ครั้ง (ซึ่งก็คือเวลาที่ใช้นั่นเอง) ในกรณี worst-case สิ่งที่จะพิสูจน์ คือ อัลกอริทึมที่ใช้ในการจัดเรียงที่ใช้เฉพาะการเปรียบเทียบค่านั้น ต้องใช้จำนวนครั้งการในการเปรียบเทียบเท่ากับ log N ! ครั้งในกรณี worst case และใช้ log N! ครั้งในกรณีเฉลี่ย สมมุติให้ค่าทั้งหมด N ค่าไม่ซ้ำกัน
7.8.1 Decision Trees Decision tree ใช้ในการพิสูจน์ขอบเขตล่าง ในกรณีของเรา decision tree เป็น binary tree และ โนดแต่ละโนดเป็นทางที่เป็นไปได้ทั้งหมดของลำดับค่าตามที่มีการใช้เครื่องหมายการเปรียบเทียบค่าระหว่างสมาชิก และ ผลของการเปรียบเทียบค่า คือ tree edges รูป Figure 7.17 แสดง algorithm ที่ใช้ในการจัดเรียงค่า a, b, และ c โดยมีสถานะเริ่มต้นที่รากของ tree
7.8.1 Decision Trees ที่ root ยังไม่มีการเปรียบเทียบค่าเกิดขึ้น ดังนั้นลำดับค่าจะเป็นอย่างไรก็ได้ ในกรณีของเรา ให้มีการเทียบค่าครั้งแรกเป็นการเทียบค่าระหว่าง a และ b ซึ่งผลที่ได้อาจเป็นไปได้สองทาง (สองสถานะ) ถ้า a < b ก็จะเหลือทางที่เป็นไปได้อีก 3 ทาง คือโนดหมายเลข 2 และถ้าการทำงานมาถึงโนดที่ 2 เราก็จะทำการเปรียบเทียบค่า a และ c ถ้า a > c การทำงานจะมาถึงโนด 5 ซึ่งเป็นสถานะคำตอบและจะหยุดทำงาน แต่ถ้า a < c (ที่โนด 2) ก็จะต้องมีการเทียบค่าต่อไปในโนด 4
Figure 7.17 decision tree ของการจัดเรียง 3 ค่า a < b < c a < c < b b < a < c b < c < a c < a < b c < b < a 1 a < b b < a a < b < c a < c < b c < a < b 2 b < a < c b < c < a c < b < a 3 a < c c < a b < c c < b a < b < c a < c < b 4 c < a < b 5 b < a < c b < c < a 6 c < b < a 7 b < c c < b a < c c < a a < b < c 8 a < c < b 9 b < a < c 10 b < c < a 11 Figure 7.17 decision tree ของการจัดเรียง 3 ค่า
ในตัวอย่างใช้การเทียบค่า 3 ครั้งสำหรับกรณี worst case 7.8.1 Decision Trees การเขียนรูป decision tree ทำได้ในกรณีที่ข้อมูลในการจัดเรียงมีปริมาณน้อยมาก ๆ เท่านั้น จำนวนครั้งของการเปรียบเทียบค่าในการจัดเรียงเท่ากับ depth ของ leaf ที่อยู่ลึกที่สุด ในตัวอย่างใช้การเทียบค่า 3 ครั้งสำหรับกรณี worst case จำนวนครั้งเฉลี่ยของการเทียบค่าเท่ากับ depth เฉลี่ยของ leaves ทั้งหมด ในการพิสูจน์ขอบเขตล่างนั้นสิ่งที่ต้องใช้ คือคุณสมบัติของ tree เท่านั้น
7.8.1 Decision Trees LEMMA 7.1 ถ้า T เป็น binary tree ที่ depth = d แล้ว T จะมีจำนวน leaf มากที่สุดเท่ากับ 2d leaves PROOF: พิสูจน์โดย induction ถ้า d = 0 แสดงว่ามี leaf มากที่สุดหนึ่งตัว นี่เป็น basis ซึ่งแสดงว่าจริง มิฉะนั้น ก็จะมีราก(ซึ่งไม่ใช่ leaf) และมี left และ right subtree ซึ่งแต่ละ subtree มี depth ได้สูงสุดคือ d – 1 เท่านั้น ดังนั้น ด้วย induction hypothesis แต่ละ subtree นั้นจะมี leaf ได้สูงสุดเท่ากับ 2d-1 leaves เท่านั้น และเมื่อรวมกันจะได้ leaf สูงสุดเท่ากับ 2d leaves เป็นการพิสูจน์ Lemma
7.8.1 Decision Trees LEMMA 7.2 binary tree ที่มี L leaves ต้องมี depth อย่างน้อย log L PROOF: เป็นจริงด้วย lemma 7.1 THEOREM 7.6 อัลกอริทึมการจัดเรียงที่ใช้เฉพาะการเทียบค่าระหว่างข้อมูลสมาชิกต้องใช้จำนวนครั้งการเทียบค่าอย่างน้อย log n! ครั้งในกรณี worst case decision tree ที่ใช้ในการจัดเรียงข้อมูล N ตัว มี n! leaves ดังนั้นจึงเป็นจริงด้วย lemma 7.2
จากทฤษฎีที่แล้ว ต้องใช้การเทียบค่า log n! ครั้ง 7.8.1 Decision Trees THEOREM 7.7 อัลกอริทึมการจัดเรียงที่ใช้เฉพาะการเทียบค่าของข้อมูลสมาชิกต้องใช้การเทียบค่าเป็น (N log N) ครั้ง PROOF: จากทฤษฎีที่แล้ว ต้องใช้การเทียบค่า log n! ครั้ง log 𝑁! = log 𝑁∙ 𝑁−1 ∙ 𝑁−2 …2∙1 = log 𝑁 + log 𝑁−1 + log 𝑁−1 …+ log 𝑁 2 +…+𝑙𝑜𝑔2+𝑙𝑜𝑔1 ≥ log 𝑁 + log 𝑁−1 + log 𝑁−1 …+ log 𝑁 2 ≥ N 2 log 𝑁 2 = 𝑁 2 log 𝑁 − 𝑁 2 =Ω(𝑁 log 𝑁 )
7.9. Bucket Sort ในบางกรณีที่เป็นกรณีพิเศษเราอาจทำการจัดเรียงข้อมูลได้ด้วยการใช้เวลาเป็น linear time ตัวอย่างอย่างง่ายของ bucket sort จะต้องประกอบด้วยสารสนเทศเพิ่มเติมเพื่อให้มันทำงานได้ ถ้าอินพุต A1, A2, . . . , AN เป็นเลขจำนวนเต็มบวกที่มีค่าน้อยกว่า M ให้สร้าง array ชื่อ count ขนาด M และกำหนดค่าเริ่มต้นทั้งหมดเป็น 0 นั่นคือ count มีเซลล์ M เซลล์ (หรือ buckets) ที่เป็นเซลล์ว่าง เมื่ออ่านค่าเข้าเป็นai ก็ให้เพิ่มค่าสมาชิกตัวที่ count[ai] ขึ้น 1 หลังอ่านค่าทั้งหมดก็ให้อ่านค่าใน array count แล้วพิมพ์ออกก็จะได้รายการที่จัดเรียง
7.10. Bucket Sort อัลกอริทึมนี้ใช้ O(M+N) (ลองคิดดู) และถ้า M เป็น O(N) ก็จะได้เวลาทั้งหมดเป็น O(N) โดยทั่วไปแล้วถ้าเรามีสารสนเทศเพิ่มเติมเป็นพิเศษ เราก็อาจจะใช้อัลกอริทึมที่สามารถใช้ประโยชน์จากสารสนเทศพิเศษนั้น ๆ มาช่วยในการจัดเรียง ทำให้การจัดเรียงได้ง่ายและเร็วขึ้นกว่าการใช้อัลกอริทึมมาตรฐานดังเช่น quicksort
Radix Sort (บางครั้งเรียกว่า card sort) 3.2.6. Examples Radix Sort (บางครั้งเรียกว่า card sort) Radix sort เป็น generalization ของ bucket sort คือมันใช้ bucket sort หลาย ๆ รอบ กรณีอย่างง่าย เช่นมีตัวเลข 10 ตัว มีค่าอยู่ในช่วง 0 ถึง 999 ที่เราต้องการจัดเรียง กล่าวโดยทั่วไป นี่คือตัวเลขจำนวน n ตัว มีค่าในช่วง 0 ถึง np - 1 เมื่อ p เป็นค่าคงที่ใด ๆ จะเห็นว่าเราไม่สามารถใช้ bucket sort เนื่องจากจะต้องใช้ buckets จำนวนมากเกินไป อัลกอริทึมที่ใช้คือการทำ bucket-sort กับ the least significant "digit" ตามด้วย least significant ตัวต่อ ๆ ไป
Radix Sort จะเห็นว่าในตะกร้าหนึ่งอาจจะมีค่าได้มากกว่าหนึ่งค่าที่แตกต่างกัน ดังนั้นเราจะใช้ queue เพื่อเก็บค่าในตะกร้า จะเห็นได้ว่าตัวเลขทั้งหมดนั้นอาจจะมีดิจิตบางดิจิตที่ซ้ำกันได้ ดังนั้นหากเราใช้ array เพื่อสร้าง lists นั่นหมายความว่าเราต้องใช้ array ขนาด n กับแต่ละ list นั่นคือต้องใช้เนื้อที่ทั้งหมดเป็นจำนวน (n2)
INITIAL ITEMS: 064, 008, 216, 512, 027, 729, 000, 001, 343, 125 SORTED BY 1’s digit: 000, 001, 512, 343, 064, 125, 216, 027, 008, 729 SORTED BY 10’s digit: 000, 001, 008, 512, 216, 125, 027, 729, 343, 064 SORTED BY 100’s digit: 000, 001, 008, 027, 064, 125, 216, 343, 512, 729 running time คือ O(p(n + b)) เมื่อ p คือจำนวนรอบการทำงาน (passes), n จำนวนสมาชิกที่จัดเรียง, และ b เป็นจำนวนตะกร้า ในกรณีตัวอย่าง มี b = n; (โดยทั่วไปแล้ว b << n), และมี p เป็นค่าคงที่ ดังนั้นจึงได้ O(n)
7.10 External Sorting อัลกอริทึมในการจัดเรียงที่กล่าวมาแล้วทั้งหมดนั้นต้องให้ข้อมูลทั้งหมดสามารถบรรจุอยู่ในหน่วยความจำหลัก ซึ่งเรียกว่า Internal sorting อย่างไรก็ตาม ในการประยุกต์ใช้งานทั่วไปนั้นมักมีปัญหาที่ไม่สามารถนำข้อมูลทั้งหมดมาบรรจุลงในหน่วยความจำหลักได้ ในหัวข้อนี้จะกล่าวถึงอัลกอริทึมการจัดเรียงที่เรียกว่า external sorting ซึ่งใช้จัดการกับข้อมูลอินพุตขนาดใหญ่มาก ๆ
7.10.1 เหตุที่ต้องใช้ algorithm ใหม่ internal sorting ใช้ประโยชน์จากความจริงที่ว่าเราสามารถเข้าถึงหน่วยความจำได้โดยตรง กล่าวคือ Shellsort เทียบค่า a[i] และ a[i - hk] โดยใช้ 1 หน่วยเวลา Heapsort เทียบค่า a[i] และ a[i*2+1] โดยใช้ 1 หน่วยเวลา Quicksort ที่ใช้ median-of-three partitioning เทียบค่าของ a[left], a[center], และ a[right] ด้วยเวลาคงที่ ถ้า input อยู่ในเทป (หรือ disk) การทำดังกล่าวย่อมเสียประสิทธิภาพเนื่องจากข้อมูลที่อยู่ในเทปนั้นต้องเข้าถึงแบบ sequential
7.10.2 โมเดลสำหรับ External Sorting external sorting ขึ้นอยู่กับชนิดของ storage devices ว่าเป็นชนิดใด algorithm ที่จะกล่าวถึงนี้ใช้สำหรับเทปซึ่งต้องเข้าถึงด้วยการหมุนม้วนเทปไปยังตำแหน่งที่ต้องการข้อมูล (ในทิศทางไปและกลับ) สมมติเรามีเทปอย่างน้อยสามตัวเพื่อใช้ในการจัดเรียง เราต้องใช้เทปสองตัวสำหรับการจัดเรียงและเทปตัวที่สามใช้เพื่อให้ทำงานได้ง่ายขึ้น File system ทำงานเหมือน Tape คือ การอ่านเขียน file ทำเป็น sequential
7.10.3 algorithm อย่างง่าย อัลกอริทึมพื้นฐานของ external sorting ใช้ฟังก์ชันการ merge จาก mergesort สมมุติเรามีเทป 4 ตัว คือ Ta1, Ta2, Tb1, Tb2, ซึ่งใช้เป็น input สองตัวและ output สองตัว เทป a และ b จะเป็น input หรือ output นั้นขึ้นอยู่กับจุดที่ทำงานในอัลกอริทึม สมมุติข้อมูลเริ่มต้นอยู่ใน Ta1 และหน่วยความจำหลักสามารถบรรจุข้อมูล (และจัดเรียง) ได้จำนวน M ชุดในแต่ละเวลา
7.10.3. The Simple Algorithm ขั้นตอนแรกคือ อ่านค่าคราวละจำนวน M ตัวจากเทปอินพุตแล้วทำการจัดเรียงด้วย internal sort และเขียนกลับไปใน Tb1 และ Tb2 สลับกัน ชุดข้อมูลที่จัดเรียงแล้วและเขียนกลับไปนั้นเรียกว่า run เมื่อเสร็จขั้นตอนนี้ก็จะหมุนเทปกลับทั้งหมด
ถ้า M = 3 แล้วหลังจากสร้าง runs ก็จะได้ข้อมูลในเทปดังนี้ Ta1 81 94 11 96 12 35 17 99 28 58 41 75 15 Ta2 Tb1 ถ้า M = 3 แล้วหลังจากสร้าง runs ก็จะได้ข้อมูลในเทปดังนี้ Ta1 81 94 11 96 12 35 17 99 28 58 41 75 15 Ta2 Tb1
7.10.3. The Simple Algorithm ใน Tb1 และ Tb2 มีกลุ่มของ runs บรรจุอยู่ ให้นำ run แรกจากเทปแต่ละตัวมาควบรวมกันซึ่งจะได้ผลเป็น run ที่มีขนาดความยาวเป็นสองเท่าแล้วเขียนลงใน Ta1 จากนั้นนำ run ต่อไปจากเทปทั้งสองมาควบรวมแล้วเขียนผลลงใน Ta2 ทำเช่นนี้ไปเรื่อย ๆ โดยใช้เทปสลับกันระหว่าง Ta1 และ Ta2 จนกว่าข้อมูลใน Tb1 หรือ Tb2 หมด
7.10.3. The Simple Algorithm ในขณะนี้อาจจะอยู่ในสถานะที่เทปทั้งสองไม่มีข้อมูลเหลืออยู่หรืออยู่ในสถานะที่มีข้อมูลเหลืออยู่หนึ่ง run ในเทปตัวใดตัวหนึ่ง ถ้าเป็นสถานะหลังก็ให้ทำการคัดลอก run ที่เหลือนั้นไปลงในเทปตามทีควรจะเป็น จากนั้นให้กรอเทปทั้ง 4 กลับแล้วดำเนินการตามขั้นตอนข้างบนซ้ำอีก แต่ในเวลานี้จะใช้เทป a เป็นอินพุตและใช้เทป b เป็นเอาต์พุตซึ่งจะได้ runs ขนาดความยาวเป็น 4M เราจะทำซ้ำกระบวนการข้างบนจนกว่าจะได้ผลเหลือเพียงหนึ่ง run ที่มีความยาวเท่ากับ N
Ta1 11 12 35 81 94 96 15 Ta2 17 28 41 58 75 99 Tb1 Ta1 11 12 35 81 94 96 15 Ta2 17 28 41 58 75 99 Tb1 51 Ta1 11 12 15 17 28 35 51 58 75 81 94 96 99 Ta2 Tb1
7.10.3. The Simple Algorithm Algorithm นี้ต้องใช้จำนวนรอบ (pass) ในการทำงานเท่ากับ log(N/M) รอบ บวกกับรอบการสร้าง run เริ่มแรก เช่น ถ้าเรามีข้อมูลจำนวน 10 ล้านหน่วยข้อมูลโดยแต่ละหน่วยใช้พื้นที่ 128 ไบต์ และถ้ามีหน่วยความจำหลักขนาด 4 ล้านไบต์ นั่นคือ ในรอบแรกจะสร้าง run ได้ 320 runs และเราต้องใช้การทำงานอีก 9 รอบเพื่อจัดเรียง สำหรับกรณีตัวอย่างข้างบนเราต้องใช้จำนวนรอบการทำงานเพิ่มอีก log 13/3 = 3 รอบ
พิจารณา running time ดังนี้ 7.10.3. The Simple Algorithm พิจารณา running time ดังนี้ หลังจากการสร้าง run เริ่มต้น จะได้จำนวน k runs: k = N/M 1st pass => No. of runs = N / 2*M runs 2nd pass => No. of runs = N / 2*2*M runs ณ จุดสิ้นสุดการทำงานจะเหลือ 1 runs ในรอบที่ x นั่นคือ xth pass => No of runs = N / 2x * M runs ดังนั้น N / 2x * M = 1 2x = N / M x = lg (N / M)
7.10.4 Multiway Merge ถ้าเรามีจำนวนเทปเพิ่มขึ้น เราก็สามารถลดจำนวนรอบการทำงานการจัดเรียงลงได้ และทำได้โดยการขยายการทำงานการควบรวมที่ใช้เดิม (two-way) ให้เป็นแบบ k-way merge ในกรณีที่มีเทปสำหรับอินพุตสองตัว เราจะกรอเทปทั้งสองกลับไปจุดเริ่มต้นของแต่ละ run แล้วทำการควบรวมด้วยการหาค่าที่น้อยที่สุด (จากจำนวนสองตัวในเทปทั้งสอง) แต่ถ้าเรามีเทปสำหรับอินพุตจำนวน k ตัว การหาค่าที่น้อยที่สุดก็ซับซ้อนขึ้น วิธีการคือเราจะใช้ priority queue
7.10.4 Multiway Merge การหาค่าที่นำไปเขียนลงในเทปที่เก็บเอาต์พุตจะใช้การทำงาน deleteMin แล้วจะเลื่อนเทปตัวที่มีค่าน้อยที่สุดไปหนึ่งตำแหน่ง โดยที่ถ้าเทปที่เป็นอินพุตดังกล่าวนี้ยังมีชุดข้อมูล run อยู่ การนำข้อมูลไปเขียนในเทปที่เป็นเอาต์พุตก็ใช้การดำเนินการ insert ลงใน priority queue จากข้อมูลตัวอย่างเดิม จัดลงในเทป 3 ตัว แล้วทำการจัดเรียง ดังรูปข้างล่างนี้
Ta1 81 94 11 96 12 35 17 99 28 58 41 75 15 Ta2 Ta3 Tb1 Tb2 Tb3 Ta1 11 12 17 28 35 81 94 96 99 Ta2 15 41 58 75 Ta3 Tb1 Tb2 Tb3 Ta1 11 12 17 28 35 81 94 96 99 Ta2 15 41 58 75 Ta3 51 Tb1 Tb2 Tb3
7.10.4. Multiway Merge หลังจากการสร้าง run เริ่มต้นแล้ว k-way merging ต้องใช้การทำงานอีกจำนวน logk(N/M) รอบ เนื่องจาก run ที่ได้จะมีขนาดเป็น k เท่าของแต่ละรอบ จากตัวอย่างข้างบน เราจึงต้องการรอบการทำงานอีก log3 13/3 = 2 ถ้าเรามีเทป 10 ตัว นั่นคือ k = 5 และจากตัวอย่างขนาดใหญ่ที่กล่าวมาแล้ว เราต้องใช้รอบการทำงานเพิ่มอีก log5 320 = 4 รอบ
7.10.4. Multiway Merge สำหรับ k-way merge หลังการสร้าง run เริ่มต้น จะได้ N / M runs 1st pass => No. of runs = N / k*M runs 2nd pass => No. of runs = N / k*k*M runs At stop case, 1 runs remains, at xth passes xth pass => No of runs = N / kx * M runs Then N / kx * M = 1 kx = N / M x = lgk (N/M)
7.10.5. Polyphase Merge สำหรับการใช้การควบรวมแบบ k-way ที่กล่าวมาแล้ว ต้องใช้เทปจำนวน 2k ตัว ซึ่งบางครั้งอาจจะไม่สะดวกหรือทำไม่ได้ เป็นไปได้ที่จะใช้เทป k + 1 ตัว เช่นการทำ two-way merging จะใช้เทปเพียง 3 ตัว สมมุติเรามีเทปอยู่ 3 ตัวคือ T1, T2, และ T3, และข้อมูล input อยู่ในเทป T1 ซึ่งมี 34 runs
7.10.5. Polyphase Merge ทางเลือกที่ 1 คือ บรรจุข้อมูลลงในเทป T2 และ T3 ตัวละ 17 runs จากนั้นนำผลการควบรวมลงใน T1 ซึ่งก็จะมีข้อมูลจำนวน 17 runs ปัญหาก็คือ ข้อมูลทั้งหมดได้ถูกเก็บอยู่ในเทปเพียงตัวเดียว แต่เราต้องให้มี run บางชุดอยู่ใน T2 เพื่อจะทำการควบรวมได้ ซึ่งทำได้ด้วยการคัดลอก 8 run แรกจาก T1 ไปลงใน T2 แล้วจึงทำการควบรวม ข้อเสียคือมีการทำงานเพิ่มขึ้นในแต่ละรอบ
7.10.5. Polyphase Merge ทางเลือกที่สอง คือ แบ่งจำนวน 34 runs ออกเป็นสองส่วนที่ไม่ต้องเท่ากัน เช่นเก็บ 21 runs ใน T2 และ 13 runs ใน T3 จากนั้นทำการควบรวม 13 runs ไปไว้ใน T1 จากนั้นกรอ T1 และ T3 กลับแล้วทำการควบรวม T1 ที่มี 13 runs เข้ากับ T2 ที่มี 8 runs (เหลือจากการควบรวมรอบที่แล้ว) ลงใน T3 ซึ่งใช้ 8 runs ใน T2 จนหมด ส่วนใน T1 ยังคงเหลือ 5 runs และขณะนี้มี 8 runs ใน T3 ให้ทำการควบรวม T1 และ T3 และต่อไปเรื่อย ๆ
7.10.5. Polyphase Merge การจัดแบ่ง runs เริ่มต้นมีผลอย่างมากต่อการทำงานด้วยวิธีที่กล่าวมานี้ เช่นถ้าเราแบ่ง 22 runs ลงใน T2 และ 12 ลงใน T3 หลังการควบรวมครั้งแรกจะได้ 12 runs ใน T1 และ 10 runs ใน T2 หลังจากควบรวมอีกรอบจะได้ 10 run ใน T3 และ 2 run ใน T1 ถ้าจำนวน runs เป็นค่า Fibonacci number FN, แล้วพบว่าการแบ่ง run ออกเป็น Fibonacci numbers FN-1 และ FN-2 เป็นวิธีที่ดีที่สุด
7.10.5. Polyphase Merge Run After T2+T3 T1+T2 T1+T3 T1 13 5 3 1 T2 21 8 2 T3 Run After T2+T3 T1+T2 T1+T3 Copy T1 12 2 1 T2 22 10 T3 8 6 4
7.10.6 Replacement Selection การสร้าง runs ซึ่งนิยมใช้กัน เรียกว่า replacement selection อ่านข้อมูล M หน่วยลง priority queue ในหน่วยความจำ ทำ deleteMin แล้วเขียนค่าที่น้อยที่สุดลงใน output tape อ่านข้อมูลตัวต่อไปจาก input เทป ถ้ามันมีค่ามากกว่าค่าที่เพิ่งถูกเขียนออกก็ให้ใส่ค่านี้ลงใน priority queue ถ้ามันมีค่าน้อยกว่าก็หมายความว่ามันจะเข้าไปอยู่ใน run ขณะนั้น ๆ ไม่ได้ ซึ่งกรณีนี้เราจะเก็บค่าใหม่นี้ไว้ในพื้นที่ที่เรียกว่า dead space ของ priority queue เอาไว้จนกว่าจะจบ run และใช้ข้อมูลตัวนี้สำหรับ run ต่อไป เราจะทำเช่นนี้ไปจนกว่าจะได้ขนาดของ priority queue เป็นศูนย์ ซึ่งเป็นจุดจบ run เริ่ม run ใหม่ด้วยการสร้าง priority queue ใหม่ด้วยสมาชิกที่อยู่ใน dead space
Figure 7.18 แสดงการสร้าง run สำหรับตัวอย่างขนาดเล็กโดยมี M = 3 สมาชิกใน Dead space มีเครื่องหมายดอกจัน Elements in heap array h[1] h[2] h[3] Out put Next element read Run 1 11 94 81 96 12* 35* 17* End of run Rebuild heap Run 2 12 35 17 99 28 58 41 15* End of tape Run 3 15