CHƯƠNG 17: SẮP XẾP
lượt xem 127
download
Sắp xếp là một quá trình biến đổi một danh sách các đối tượng thành một danh sách thoả mãn một thứ tự xác định nào đó. Sắp xếp đóng vai trò quan trọng trong tìm kiếm dữ liệu. Chẳng hạn, nếu danh sách đã được sắp xếp theo thứ tự tăng dần (hoặc giảm dần), ta có thể sử dụng kỹ thuật tìm kiếm nhị phân hiệu quả hơn nhiều tìm kiếm tuần tự… Trong thiết kế thuật toán, ta cũng thường xuyên cần đến sắp xếp, nhiều thuật toán được thiết kế dựa trên ý tưởng xử lý các đối tượng theo một thứ...
Bình luận(0) Đăng nhập để gửi bình luận!
Nội dung Text: CHƯƠNG 17: SẮP XẾP
- CHƯƠNG 17 SẮP XẾP Sắp xếp là một quá trình biến đổi một danh sách các đối tượng thành một danh sách thoả mãn một thứ tự xác định nào đó. Sắp xếp đóng vai trò quan trọng trong tìm kiếm dữ liệu. Chẳng hạn, nếu danh sách đã được sắp xếp theo thứ tự tăng dần (hoặc giảm dần), ta có thể sử dụng kỹ thuật tìm kiếm nhị phân hiệu quả hơn nhiều tìm kiếm tuần tự… Trong thiết kế thuật toán, ta cũng thường xuyên cần đến sắp xếp, nhiều thuật toán được thiết kế dựa trên ý tưởng xử lý các đối tượng theo một thứ tự xác định. Các thuật toán sắp xếp được chia làm 2 loại: sắp xếp trong và sắp xếp ngoài. Sắp xếp trong được thực hiện khi mà các đối tượng cần sắp xếp được lưu ở bộ nhớ trong của máy tính dưới dạng mảng. Do đó sắp xếp trong còn được gọi là sắp xếp mảng. Khi các đối tượng cần sắp xếp quá lớn cần lưu ở bộ nhớ ngoài dưới dạng file, ta cần sử dụng các phương pháp sắp xếp ngoài, hay còn gọi là sắp xếp file. Trong chương này, chúng ta trình bày các thuật toán sắp xếp đơn giản, các thuật toán này dòi hỏi thời gian O(n2) để sắp xếp mảng n đối tượng. Sau đó chúng ta đưa ra các thuật toán phức tạp và tinh vi hơn, nhưng hiệu quả hơn, chỉ cần thời gian O(nlogn). Mảng cần được sắp xếp có thể là mảng số nguyên, mảng các số thực, hoặc mảng các xâu ký tự. Trong trường hợp tổng quát, các đối tượng cần được sắp xếp chứa một số thành phần dữ liệu, và ta cần sắp xếp mảng các đối tượng đó theo một thành phần dữ liệu nào đó. Thành phần dữ liệu đó được gọi là khoá sắp xếp. Chẳng hạn, ta có một mảng các đối tượng sinh viên, mỗi sinh viên gồm các thành phần dữ liệu: tên, tuổi, chiều cao,…, và ta muốn sắp xếp các sinh viên theo thứ tự chiều cao tăng, khi đó chiều cao là khoá sắp xếp. 187
- Từ đây về sau, ta giả thiết rằng, mảng cần được sắp xếp là mảng các đối tượng có kiểu Item, trong đó Item là cấu trúc sau: struct Item { keyType key; // Khoá sắp xếp // Các trường dữ liệu khác }; Vấn đề sắp xếp bây giờ được phát biểu chính xác như sau. Cho mảng A[0..n-1] chứa n Item, chúng ta cần sắp xếp lại các thành phần của mảng A sao cho: A[0].key
- A[0] A[1] A[2] A[3] A[4] A[5] I k 5 9 1 8 3 7 0 2 1 9 5 8 3 7 1 4 1 3 5 8 9 7 2 2 1 3 5 8 9 7 3 5 1 3 5 7 9 8 4 5 1 3 5 7 8 9 Sau đây là hàm sắp xếp lựa chọn: void SelectionSort(Item A[] , int n) // Sắp xếp mảng A[0..n-1] với n > 0 { (1) for (int i = 0 ; i < n-1 ; i++) { (2) int k = i; (3) for (int j = i + 1 ; j < n ; j++) (4) if (A[j].key < A[k].key) k = j; (5) swap(A[i],A[k]); } } Trong hàm trên, swap là hàm thực hiện trao đổi giá trị của hai biến. Phân tích sắp xếp lựa chọn. Thân của lệnh lặp (1) là các lệnh (2), (3) và (5). Các lệnh (2) và (5) có thời gian chạy là O(1). Ta đánh giá thời gian chạy của lệnh lặp (3). Số lần lặp là (n-1-i), thời gian thực hiện lệnh (4) là O(1), do đó thời gian chạy của lệnh (3) là (n-1-i)O(1). Như vậy, thân của lệnh lặp (1) có thời gian chạy ở lần lặp thứ i là (n-1-i)O(1). Do đó lệnh lặp (1) đòi hỏi thời gian n −2 ∑ i =0 (n-1-i)O(1) = O(1)(1 + 2 + …+ n-1) = O(1)n(n-1)/2 = O(n2) 189
- Vậy thời gian chạy của hàm sắp xếp lựa chọn là O(n2). 17.1.2 Sắp xếp xen vào Phương pháp sắp xếp xen vào là như sau. Giả sử đoạn đầu của mảng A[0..i-1] (với i >= 1) đã được sắp xếp, tức là ta đã có A[0].key
- { (1) for ( int i = 1 ; i < n ; i++) (2) for ( int k = i ; k > 0 ; k--) (3) if (A[k].key < A[k-1].key) swap(A[k],A[k-1]); else break; } Phân tích sắp xếp xen vào Số lần lặp tối đa của lệnh lặp (2) là i, thân của lệnh lặp (2) là lệnh (3) cần thời gian O(1). Do đó thời gian chạy của lệnh (2) là O(1)i. Thời gian thực hiện lệnh lặp (1) là n −1 ∑ O(1)i = O(1)(1 + 2 + ... + n − 1) = O(n i =1 2 ) 17.1.3 Sắp xếp nổi bọt Ý tưởng của sắp xếp nổi bọt là như sau. Cho chỉ số k chạy từ 0, 1 , …, n-1, nếu hai thành phần kề nhau không đúng trật tự, tức là A[k].key >A[k+1].key thì ta trao đổi hai thành phần A[k] và A[k+1]. Làm như vậy ta đẩy được dữ liệu có khoá lớn nhất lên vị trí sau cùng A[n-1]. Ví dụ. Giả sử ta có mảng số nguyên A[0..4]= (6,1,7,3,5).Kết quả thực hiện quá trình trên được cho trong bảng sau: A[0] A[1] A[2] A[3] A[4] 6 1 7 3 5 Trao đổi A[0] và A[1] 1 6 7 3 5 Trao đổi A[2] và A[3] 1 6 3 7 5 Trao đổi A[3] và A[4] 1 6 3 5 7 191
- Lặp lại quá trình trên đối với mảng A[0,…, n-2] để đẩy dữ liệu có khoá lớn nhất lên vị trí A[n-2]. Khi đó ta có A[n-2].key ≤ A[n-1].key. Tiếp tục lặp lại quá trình đã mô tả trên các đoạn đầu A[0..i], với i = n-3, …,1, ta sẽ thu được mảng được sắp . Ta có hàm sắp xếp nổi bọt như sau: void BubbleSort( Item A[] , int n) { (1) for (int i = n-1 ; i > 0 ; i--) (2) for (int k = 0 ; k < i ; k++) (3) if ( A[k].key > A[k+1].key) Swap(A[k],A[k+1]); } Tương tự như hàm sắp xếp xen vào ,ta có thể đánh giá thời gian chạy của hàm sắp xếp nổi bọt là O(n2 ). Trong hàm BubbleSort khi thực hiện lệnh lặp (1), nếu đến chỉ số i nào đó, n-1 ≥ i > 1, mà đoạn đầu A[0..i] đã được sắp, thì ta có thể dừng. Do đó ta có thể cải tiến hàm BubbleSort bằng cách đưa vào biến sorted, biến này nhận giá trị true nếu A[0..i] đã được sắp và nhận giá trị false nếu ngược lại. Khi sorted nhận giá trị true thì lệnh lặp (1) sẽ dừng lại. void BubbleSort (Item A[] , int n) { for (int i = n-1 ; i > 0 ; i -- ) { bool sorted = true; for( int k = 0 ; k < i ; k++) if (A[k].key > A[k+1].key) { swap (A[k], A[k+1]); sorted = false; } if (sorted) break; } } 192
- 17.2 SẮP XẾP HOÀ NHẬP Thuật toán sắp xếp hoà nhập (MergeSort) là một thuật toán được thết kế bằng kỹ thuật chia - để - trị. Giả sử ta cần sắp xếp mảng A[a..b], trong đó a, b là các số nguyên không âm, a b, a là chỉ số đầu và b là chỉ số cuối của mảng. Ta chia mảng thành hai mảng con bởi chỉ số c nằm giữa a và b ( c = ( a + b ) / 2). Các mảng con A[a..c] và A[c+1…b] được sắp xếp bằng cách gọi đệ quy thủ tục sắp xếp hoà nhập. Sau đó ta hoà nhập hai mảng con A[a…c] và A[c+1…b] đã được sắp thành mảng A[a…b] được sắp. Giả sử Merge(A,a,c,b) là hàm kết hợp hai mảng con đã được sắp A[a..c] và A[c+ 1..b] thành mảng A[a..b] được sắp. Thuật toán sắp xếp hoà nhập được biểu diễn bởi hàm đệ quy sau. void MergeSort( Item A[ ], int a, int b) { if (a < b) { int c = (a + b)/2; MergeSort ( A, a, c ); MergeSort ( A, c+1, b); Merge ( A, a, c, b); } } Công việc còn lại của ta là thiết kế hàm hoà nhập Merge ( A, a, c, b), nhiệm vụ của nó là kết hợp hai nửa mảng đã được sắp A[a…c] và A[ c+1…b] thành mảng được sắp. Ý tưởng của thuật toán hoà nhập là ta đọc lần lượt các thành phần của hai nửa mảng và chép vào mảng phụ B[0..b-a] theo đúng thứ tự tăng dần. Giả sử i là chỉ số chạy trên mảng con A[a…c], i được khởi tạo là a ; j là chỉ số chạy trên mảng con A[c+1..b], j được khởi tạo là c + 1. So sánh A[i] và A[j], nếu A[i].key < A[j].key thì ta chép A[i] vào mảng B và tăng i lên 1, còn nếu ngược lại thì ta chép A[j] vào mảng B va tăng j lên 1. Lặp lại hành động đó cho đến khi i vượt quá c hoặc j vượt quá b. Nếu chỉ số i chưa vượt quá c nhưng j đã vượt quá b 193
- thì ta cần phải chép phần còn lại A[i…c] vào mảng B. Tương tự, nếu i > c, nhưng j ≤ b thì ta cần chép phần còn lại A[j…b] vào mảng B. Chẳng hạn, xét mảng số nguyên A[ 5…14], trong đó A[5…9] và A[10…14] đã được sắp như sau: i j A 10 12 20 31 35 3 5 15 21 26 a=5 6 7 8 c=9 10 11 12 13 14 Bắt đầu i = 5 , j = 10. Vì A[5] > A[10] nên A[10] = 3 được chép vào mảng B và j = 11. Ta lại có A[5] > A[11], nên A[11] = 5 được chép vào mảng B và j = 12. Đến dây A[5] < A[12], ta chép A[5] = 10 vào mảng B và i = 6. Tiếp tục như thế ta nhận được mảng B như sau: B 3 5 10 12 15 20 21 26 0 1 2 3 4 5 6 7 8 9 Đến đây j = 15 > b = 14, còn i = 8 < c = 9, do đó ta chép nốt A[8] = 31 và A[9] = 35 sang B để nhận được mảng B được sắp. Bây giờ chỉ cần chép lại mảng B sang mảng A. Hàm Merge được viết như sau: void Merge( Item A[] , int a , int c , int b) // a, c, b là các số nguyên không âm, a ≤ c ≤ b. // Các mảng con A[a…c] và A[c+1…b] đã được sắp. { int i = a; int j = c + 1; 194
- int k = 0; int n = b – a + 1; Item * B = new Item[n]; (1) while (( i < c +1 ) && ( j < b +1 )) if ( A [i].key < A[j].key) B[k ++] = A[i ++]; else B[k ++] = A[j ++]; (2) while ( i < c + 1) B[k ++] = A[i++]; (3) while ( j < b +1) B[k ++] = A[ j ++]; i = a; (4) for ( k = 0 ; k < n ; k ++) A[i ++] = B [k]; delete [ ] B; } Phân tích sắp xếp hoà nhập. Giả sử mảng cần sắp xếp A[a…b] có độ dài n, n = b – a +1, và T(n) là thời gian chạy của hàm MergeSort (A, a, b). Khi đó thời gian thực hiện mỗi lời gọi đệ quy MergeSort (A, a, c) và MergeSort (A, c + 1, b) là T(n/2). Chúng ta cần đánh gía thời gian chạy của hàm Merge(A, a, c, b). Xem xét hàm Merge ta thấy rằng, các lệnh lặp (1), (2), (3) cần thực hiện tất cả là n lần lặp, mỗi lần lặp chỉ cần thực hiện một số cố định các phép toán. Do đó tổng thời gian của ba lệnh lặp (1), (2), (3) là O(n). Lệnh lặp (4) cần thời gian O(n). Khi thực hiện hàm MergeSort(A, a, b) với a = b, chỉ một phép so sánh phải thực hiện, do đó T(1) = O(1). Từ hàm đệ quy MergeSort và các đánh giá trên, ta có quan hệ đệ quy sau T(1) = O(1) T(n) = 2T(n/2) + O(n) với n>1 Giả sử thời gian thực hiện các phép toán trong mỗi lần lặp ở hàm Merge là hằng số d nào đó, ta có : T(1) ≤ d 195
- T(n) ≤ 2T(n/2) + nd Áp dụng phương pháp thế lặp vào bất đẳng thức trên ta nhận được T(n) ≤ 2T(n/2) + n d ≤ 22 T(n/22) + 2 (n/2)d + n d …… ≤ 2k T(n/2k) + n d + …+ n d (k lần nd) Giả sử k là số nguyên dương lớn nhất sao cho 1 ≤ n / 2k. Khi đó, ta có T(n) ≤ 2k T(1) + n d + … + n d ( k lần n d) T(n) ≤ (k + 1) n d T(n) ≤ (1 + log n) n d Vậy T(n) = O (n log n). 17.3 SẮP XẾP NHANH Trong mục này chúng ta trình bày thuật toán sắp xếp được đưa ra bởi Hoare, nổi tiếng với tên gọi là sắp xếp nhanh (QuickSort). Thời gian chạy của thuật toán này trong trường hợp xấu nhất là O(n2). Tuy nhiên thời gian chạy trung bình là O(n logn). Thuật toán sắp xếp nhanh được thiết kế bởi kỹ thuật chia-để-trị như thuật toán sắp xếp hòa nhập. Nhưng trong thuật toán sắp xếp hòa nhập, mảng A[a…b] cần sắp được chia đơn giản thành hai mảng con A[a..c] và A[c+1..b] bởi điểm chia ở giữa mảng, c = (a+b)/2. Còn trong thuật toán sắp xếp nhanh, việc “chia mảng thành hai mảng con” là một quá trình biến đổi phức tạp để từ mảng A[a..b] ta thu được hai mảng con A[a..k-1] và A[k+1..b] thỏa mãn các tính chất sau : A[i].key ≤ A[k].key với mọi i, a ≤ i ≤ k-1. A[j].key > A[k].key với mọi j, k+1 ≤ j ≤ b. 196
- Nếu thực hiện được sự phân hoạch mảng A[a..b] thành hai mảng con A[a..k-1] và A[k+1..b] thỏa mãn các tính chất trên, thì nếu sắp xếp được các mảng con đó ta sẽ có toàn bộ mảng A[a..b] được sắp xếp. Giả sử Partition(A, a, b, k) là hàm phân hoạch mảng A[a..b] thành hai mảng con A[a..k-1] và A[k+1..b]. Thuật toán sắp xếp nhanh là thuật toán đệ quy được biểu diễn bởi hàm đệ quy như sau : void QuickSort(Item A[] , int a , int b) //Sắp xếp mảng A[a..b] với a ≤ b. { if (a < b) { int k; Partition(A, a, b, k); if (a
- con bằng nhau. Điều này là có thể làm được, tuy nhiên nó đòi hỏi nhiều thời gian hơn sự cần thiết. Vì vậy, ta sẽ chọn ngay thành phần đầu tiên của mảng làm mốc, tức là pivot = A[a].key. Sau đó ta sử dụng hai chỉ số, chỉ số left chạy từ trái sang phải, ban đầu left = a + 1, chỉ số right chạy từ phải sang trái, ban đầu right = b. Biến left sẽ tăng và dừng tại vị trí mà A[left].key > pivot, còn biến right sẽ giảm và dừng lại tại vị trí mà A[right].key ≤ pivot. Khi đó nếu left < right thì ta trao đổi giá trị của A[left] với A[right]. Quá trình trên được lặp lại cho tới khi left > right. Lúc này ta dễ thấy rằng, mọi thành phần trong mảng A[a..right] có khóa nhỏ hơn hay bằng mốc, còn mọi thành phần trong mảng A[left..b] có khóa lớn hơn mốc. Cuối cùng ta trao đổi A[a] và A[right] để đặt mốc vào vị trí k = right. Hàm phân hoạch được viết như sau : void Partition( Item A[] , int a , int b , int & k) { keyType pivot = A[a].key; int left = a + 1; int right = b; do { while (( left
- Để thấy được hàm phân hoạch làm việc như thế nào, ta hãy xét ví dụ sau. Giả sử ta cần phân hoạch mảng số nguyên A[0..9] như sau : 8 3 17 12 6 14 7 5 13 15 left right Lấy mốc pivot = A[0] = 8, ban đầu left = 1, right = 9. Chỉ số left tăng và dừng lại tại vị trí left = 2, vì A[2] = 17 > 8, chỉ số right giảm và dừng lại tại vị trí right = 7, vì A[7] = 5 < 8. Trao đổi A[2] với A[7], đồng thời tăng left lên 1, giảm right đi 1, ta có : 8 3 5 12 6 14 7 1 13 15 7 left right Đến đây A[left] = 12 > 8 và A[right] = 7 < 8. Lại trao đổi A[left] với A[right], và tăng left lên 1, giảm right đi 1, ta có : 8 3 5 7 6 14 1 1 13 15 2 7 left right Tiếp tục, A[left] = 6 < 8 nên left được tăng lên và dừng lại tại left = 5 vì A[5] > 8. A[right] = 14 > 8 nên right được giảm đi và dừng lại tại right = 4, vì A[4] < 8. Ta có hoàn cảnh sau : 8 3 5 7 6 14 1 1 13 15 2 7 right left 199
- Đến đây right < left, ta dừng lại, trao đổi A[0] với A[4] ta thu được phân hoạch với k = right = 4. 6 3 5 7 8 14 1 1 13 15 2 7 k Phân tích sắp xếp nhanh Chúng ta cần đánh giá thời gian chạy T(n) của thuật toán sắp xếp nhanh trên mảng A[a..b] có n phần tử, n = b – a + 1. Trước hết ta cần đánh giá thời gian thực hiện hàm phân hoạch. Thời gian phân hoạch là thời gian đi qua mảng (hai biến left và right chạy từ hai đầu mảng cho tới khi chúng gặp nhau), tại mỗi vị trí mà left và right chạy qua ta cần so sánh thành phần ở vị trí đó với mốc và các trao đổi khi cần thiết. Do đó khi phân hoạch một mảng n phần tử ta chỉ cần thời gian O(n). Thời gian trong trường hợp tốt nhất. Trường hợp tốt nhất xảy ra khi mà sau mỗi lần phân hoạch ta nhận được hai mảng con bằng nhau. Trong trường hợp này, từ hàm đệ quy QuickSort, ta suy ra quan hệ đệ quy sau : T(1) = O(1) T(n) = 2 T(n/2) + O(n) với n > 1. Đây là quan hệ đệ quy mà ta đã gặp khi phân tích sắp xếp hòa nhập. Như vậy trong trường hợp tốt nhất thời gian chạy của QuickSort là O(n logn). Thời gian trong trường hợp xấu nhất. Trường hợp xấu nhất là trường hợp mà sau mỗi lần phân hoạch mảng n phần tử ta nhận được mảng con n – 1 phần tử ở một phía của mốc, còn phía kia không có phần 200
- tử nào. (Dễ thấy rằng trường hợp này xẩy ra khi ta phân hoạch một mảng đã được sắp). Khi đó ta có quan hệ đệ quy sau : T(1) = O(1) T(n) = T(n – 1) + O(n) với n > 1 Ta có : T(1) = C T(n) = T(n – 1) + nC với n > 1 Trong đó C là hằng số nào đó. Bằng cách thế lặp ta có : T(n) = T(1) + 2C + 3C + … + nC n = C ∑i i =1 = Cn(n+1)/2 Do đó trong trường hợp xấu nhất, thời gian chạy của sắp xếp nhanh là O(n2). Thời gian trung bình. Bây giờ ta đánh giá thời gian trung bình Ttb(n) mà QuickSort đòi hòi để sắp xếp một mảng có n phần tử. Giả sử mảng A[a..b] chứa n phần tử được đưa vào mảng một cách ngẫu nhiên. Khi đó hàm phân hoạch Partition(A, a, b, k) sẽ cho ra hai mảng con A[a..k – 1] và A[k + 1..b] với k là một trong các chỉ số từ a đến b với xác suất như nhau và bằng 1/n. Vì thời gian thực hiện hàm phân hoạch là O(n), từ hàm QuickSort ta suy ra quan hệ đệ quy sau : n Ttb(n) = 1 n ∑ k =1 [ Ttb(k - 1) + Ttb(n - k)] + O(n) Hay n Ttb(n) = 1 n ∑ k =1 [ Ttb(k - 1) + Ttb(n - k)] + nC (1) 201
- Trong đó C là hằng số nào đó. Chú ý rằng n n ∑k =1 Ttb(k - 1) = ∑ k =1 Ttb(n - k) Do đó có thể viết lại (1) như sau : n Ttb(n) = 2 n ∑k =1 Ttb(k - 1) + nC (2) Trong (2) thay n bới n – 1 ta có : n −1 Ttb(n - 1) = 2 n−1 ∑ k =1 Ttb(k - 1) + (n – 1)C (3) Nhân (2) với n, nhân (3) với n – 1 và trừ cho nhau ta nhận được n Ttb(n) = (n + 1) Ttb(n - 1) + (2n – 1)C Chia đẳng thức trên cho n(n + 1) ta nhận được quan hệ đệ quy sau : Ttb (n) Ttb (n - 1) 2n - 1 n+1 = n + n ( n+1) C (4) Sử dụng phép thế lặp, từ (4) ta có Ttb (n) Ttb (n - 1) 2n - 1 n+1 = n + n ( n+1) C Ttb (n - 2) 2n - 3 2n - 1 = n−1 + ( n −1) n + n ( n+1) C ... Ttb (n) Ttb (1) n 2i − 1 n+1 = 2 + c ∑ i(i + 1) i =1 (5) Ta có đánh giá 202
- n 2i − 1 n 2 n dx ∑ i(i + 1) ≤ k =1 ∑ i ≤ 2 ∫ x ≤ 2logn i =1 1 Do đó từ (5) ta suy ra Ttb (n) = O(logn) n+1 hay Ttb(n) = O(n logn). Trong trường hợp xấu nhất, QuickSort đòi hỏi thời gian O(n2), nhưng trường hợp này rất ít khi xảy ra. Thời gian trung bình của QuickSort là O(n logn), và thời gian trong trường hợp xấu nhất của MergeSort cũng là O(n logn). Tuy nhiên thực tiễn cho thấy rằng, trong phần lớn các trường hợp QuickSort chạy nhanh hơn các thuật toán sắp xếp khác. 17.4 SẮP XẾP SỬ DỤNG CÂY THỨ TỰ BỘ PHẬN Trong mục này chúng ta trình bày phương pháp sắp xếp sử dụng cây thứ tự bộ phận (heapsort). Trong mục 10.3, chúng ta biết rằng một cây thứ tự bộ phận n đỉnh có thể biểu diễn bởi mảng A[0..n-1], trong đó gốc cây được lưu trong A[0], và nếu một đỉnh được lưu trong A[i], thì đỉnh con trái (nếu có) của nó được lưu trong A[2*i + 1], còn đỉnh con phải nếu có của nó được lưu trong A[2*i + 2]. Mảng A thoả mãn tính chất sau (ta sẽ gọi là tính chất heap): A[i].key
- A[n-1]. Mảng A[0..n-2] bây giờ thoả mãn tính chất heap với mọi i >= 1, trừ i = 0. Biến đổi mảng A[0..n-2] để nó thoả mãn tính chất heap. Lại trao đổi A[0] và A[n-2]. Rồi lại biến đổi mảng A[0..n-3] trở thành mảng thoả mãn tính chất heap. Lặp lại quá trình trên, cuối cùng ta sẽ nhận được mảng A[0..n-1] được sắp theo thứ tự giảm dần: A[0].key >= A[1].key >= … >= A[n-1].key Trong quá trình trên, sau mỗ² ỗ lần trao đổi A[0] với A[m] (với m=n-1,…,1), ta sẽ nhận được mảng A[0…m-1] thoả mãn tính chất heap với mọi i >= 1, trừ i = 0. Điều này có nghĩa là cây nhị phân được biểu diễn bởi mảng A[0..m-1] đã thoả mãn tính chất thứ tự bộ phận, chỉ trừ gốc. Để nó trở thành cây thứ tự bộ phận, ta chỉ cần đẩy dữ liệu lưu ở gốc xuống vị trí thích hợp trong cây, bằng cách sử dụng hàm ShiftDown (Xem mục 10.3.3). Còn một vấn đề cần giải quyết, đó là biến đổi mảng cần sắp xếp A[0..n-1] thành mảng thoả mãn tính chất heap. Điều này có nghĩa là ta phải biến đổi cây nhị phân được biểu diễn bởi mảng A[0..n-1] thành cây thứ tự bộ phận. Muốn vậy, với i chạy từ n/2-1 giảm xuống 0, ta chỉ cần sử dụng hàm SiftDown để đẩy dữ liệu lưu ở đỉnh i xuống vị trí thíc hợp trong cây. Đây là cách xây dựng cây thứ tự bộ phận mà chúng ta đã trình bày trong 10.3.2. Bây giờ ta viết lại hàm ShiftDown cho thích hợp với sự sử dụng nó trong thuật toán. Giả sử mảng A[a..b] (a < b) đã thoả mãn tính chất heap với mọi i >= a+1. Hàm ShiftDown(a,b) sau đây thực hiện việc đẩy A[a] xuống vị trí thích hợp trong mảng A[a..b] để mảng thoả mãn tính chất heap với mọi i >= a. void ShiftDown(int a, int b) { int i = a; int j = 2 * i + 1; while (j
- { int k = j + 1; if (k A[j].key) { swap(A[i],A[j]); i = j; j = 2 * i + 1; } else break; } } Sử dụng hàm ShiftDown, ta đưa ra thuật toán sắp xếp HeapSort sau đây. Cần lưu ý rằng, kết quả của thuật toán là mảng A[0..n-1] được sắp xếp theo thứ tự giảm dần. void HeapSort(Item A[] , int n) //Sắp xếp mảng A[0..n-1] với n > 1 { for (int i = n / 2 – 1 ; i >= 0 ; i--) ShiftDown(i,n-1); //Biến đổi mảng A[0..n-1] // thành mảng thoả mãn tính chất heap for (int i = n – 1 ; i >= 1 ; i--) { swap(A[0],A[i]); ShiftDown(0,i - 1); } } Phân tích HeapSort. Thời gian thực hiện lệnh lặp (1) là thời gian xây dựng cây thứ tự bộ phận mà chúng ta đã xét trong mục 10.3.2. Theo chứng minh đã đưa ra trong 10.3.2, lệnh lặp (1) chỉ đòi hỏi thời gian O(n). Trong lệnh lặp (2), số lần lặp là n-1. Thân vòng lặp (2), với i = n-1 là 205
- swap(A[0],A[n - 1]); ShiftDown(0,n - 2); Đây là các lệnh thực hiện DeleteMin trên cây thứ tự bộ phận được biểu diễn bởi mảng A[0..n-1], và dữ liêụ có khoá nhỏ nhất được lưu vào A[n- 1]. Trong mục 10.3.1, ta đã chứng tỏ rằng DeleteMin chỉ cần thời gian O(logn). Như vậy thân của lệnh lặp (2) cần thời gian nhiều nhất là O(logn). Do đó lệnh (2) cần thời gian O(nlogn). Vì vậy, thời gian thực hiện HeapSort là O(nlogn). BÀI TẬP. 1. Cho mảng các số nguyên (8, 1, 4, 1, 5, 2, 6, 5). Hãy sắp xếp mảng này bằng cách sử dụng: a. Sắp xếp lựa chọn. b. Sắp xếp xen vào. c. Sắp xếp nổi bọt. d. Sắp xếp nhanh. e. Sắp xếp hoà nhập. f. Sắp xếp sử dụng cây thứ tự bộ phận. Cần đưa ra kết quả thực hiện mỗi bước của thuật toán. 2. Hãy đánh giá thời gian chạy của các thuật toán sắp xếp: a. Sắp xếp lựa chọn. b. Sắp xếp xen vào. c. Sắp xếp nhanh. d. Sắp xếp hoà nhập. Trong các trường hợp sau: a. Mảng đầu vào có tất cả các phần tử có khoá bằng nhau. b. Mảng đầu vào đã được sắp. 3. Viết hàm phân hoạch mảng A[a …b] với phần tử được chọn làm mốc là phần tử đứng giữa mảng, tức là phần tử A[(a + b) / 2]. 206
CÓ THỂ BẠN MUỐN DOWNLOAD
Chịu trách nhiệm nội dung:
Nguyễn Công Hà - Giám đốc Công ty TNHH TÀI LIỆU TRỰC TUYẾN VI NA
LIÊN HỆ
Địa chỉ: P402, 54A Nơ Trang Long, Phường 14, Q.Bình Thạnh, TP.HCM
Hotline: 093 303 0098
Email: support@tailieu.vn