7.5. Heapsort

ในบทที่ 6 ได้กล่าวแล้วว่าเราอาจใช้ priority queues ในการจัดเรียงด้วยเวลา O(N log N) อัลกอริทึมที่ใช้พื้นฐานแนวคิดนี้ เรียกว่า heapsort และมี Big-Oh running time ดีกว่าอัลกอริทึมอื่น ๆ ที่กล่าวมาแล้ว อย่างไรก็ตาม ในทางปฏิบัติแล้วมันทำงานช้ากว่า shellsort ที่ใช้ลำดับการเพิ่มของ Sedwick

วิธีการที่ใช้ตามที่กล่าวในบทที่ 6 คือ สร้าง binary heap (มีสมาชิก N ตัว) ซึ่งใช้เวลาเป็น O(N) จากนั้นทำ deleteMin N ครั้ง สมาชิกที่ถูกย้ายออกไปจาก heap ตัวแรก คือตัวที่มีค่าน้อยที่สุด และเรียงลำดับตามค่าไป แล้วนำไปเก็บในอะเรย์อีกตัวหนึ่งจากนั้นก็คัดลอกกลับไปยังอะเรย์เดิมซึ่งก็จะเป็นคำตอบในการจัดเรียง เนื่องจากการทำ deleteMin แต่ละตัวใช้ O(log N) ดังนั้น running time รวมทั้งหมด คือ O(N log N)

ปัญหาของอัลริทึมนี้ คือ ต้องใช้อะเรย์เพิ่มเข้ามาสำหรับเก็บค่าที่ deleteMin ออกไปจาก heap นั่นคือต้องใช้พื้นที่หน่วยความจำเพิ่มขึ้นเป็นสองเท่า และอาจก่อปัญหาได้ในบางกรณี ส่วนเวลาที่ใช้ในการคัดลอกอะเรย์กลับไปยังอะเรย์แรกนั้นใช้เวลาเป็น O(N) ซึ่งไม่มีผลอย่างมีนัยสำคัญต่อ running time เพื่อหลีกเลี่ยงการใช้อะเรย์ตัวที่สองดังกล่าว เราพบว่าหลังจากการ deleteMin แต่ละครั้งนั้นขนาดของ heap ก็จะลดลง 1 ตัวด้วย ดังนั้นตำแหน่งเซลล์ใน heap ที่ว่างซึ่งเป็นตำแหน่งสุดท้ายใน heap เดิมนั้นก็สามารถนำมาใช้ในการเก็บค่าของตัวที่เพิ่งจะถูกย้ายออกไปนั้นได้ เช่น ถ้าเรามี heap ที่มีสมาชิก 6 ตัว การทำ deleteMin ครั้งแรกจะได้ a1 และทำให้ heap เหลือสมาชิก 5 ตัว ดังนั้นเราก็สามารถบรรจุ a1 ลงในตำแหน่งที่ 6 ที่ว่างนั้นได้ การ deleteMin ถัดมาได้ a2 และ heap จะมีสมาชิกเหลือเพียง 4 ตัว และเราสามารถบรรจุ a2 ลงในตำแหน่ง 5 ได้

ด้วยกรรมวิธีที่กล่าวมาข้างบนนี้ หลังการทำงาน deleteMin ครั้งสุดท้ายแล้ว เราจะได้อะเรย์ที่มีสมาชิกจัดเรียงค่าจากมากไปน้อย ดังนั้นถ้าเราต้องการจัดเรียงค่าจากน้อยไปมาก เราก็จะใช้วิธีการเดียวกันนี้เพียงแต่ต้องใช้ heap ที่ parent มีค่ามากกว่า child ของมัน นั่นคือ (max)heap ในที่นี้เราจะใช้ (max)heap ในการ implement ของเราและยังคงใช้อะเรย์เช่นเดิม ขั้นแรกสร้าง heap ด้วย linear time จากนั้นทำ deleteMax เป็นจำนวน N - 1 ครั้ง ด้วยการสลับสมาชิกตัวสุดท้ายใน heap กับสมาชิกตัวแรก แล้วลดขนาดของ heap ลงแล้วทำการ percolating down หลังจบอัลกอริทึมจะได้อะเรย์ที่มีสมาชิกเรียงตามลำดับค่า เช่นมีลำดับของค่าเริ่มต้นคือ 31, 41, 59, 26, 53, 58, 97 ซึ่งสร้าง heap ได้ดังรูปที่ 7.6 ภายหลังจากทำการ deleteMax ครั้งแรกแล้วก็จะได้สถานะของ heap และอะเรย์ดังรูปที่ 7.7 ซึ่งมีค่า 31 เป็นค่าสุดท้ายใน heap และ 97 ถูกบรรจุลงในตำแหน่งสุดท้ายของ heap ซึ่งในทางเทคนิกแล้วในขณะนี้ตำแหน่งดังกล่าวนี้ไม่ถือว่าเป็นส่วนหนึ่งของ heap อีกต่อไปแล้ว รูปที่ 7.8 เป็นโปรแกรม heapsort ซึ่งแตกต่างจาก binary heap เล็กน้อย คือ ในที่นี้จะใช้อะเรย์ตั้งแต่ตำแหน่ง 0 แต่สำหรับ binary heap เราใช้ตำแหน่งที่ 1 ของอะเรย์เป็นตำแหน่งแรก

97 53 59 26 41 58 31
0 1 2 3 4 5 6 7 8 9 10

รูปที่ 7.6 รูป Heap หลัง buildHeap

59 53 58 26 41 31 97
0 1 2 3 4 5 6 7 8 9 10

รูปที่ 7.7 รูป Heap หลังการ deleteMax ครั้งแรก  

/* Internal method for heapsort.
    return the index of the left child. */
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;
        } 
/*** 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 );
            }
        }

รูปที่ 7.8 โปรแกรม heapsort