Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 21 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
21
Dung lượng
211 KB
Nội dung
CHƯƠNG 17 SẮPXẾPSắpxế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ắpxế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ắpxế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ắpxếp được chia làm 2 loại: sắpxếp trong và sắpxếp ngoài. Sắpxếp trong được thực hiện khi mà các đối tượng cần sắpxếp được lưu ở bộ nhớ trong của máy tính dưới dạng mảng. Do đó sắpxếp trong còn được gọi là sắpxếp mảng. Khi các đối tượng cần sắpxế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ắpxếp ngoài, hay còn gọi là sắpxếp file. Trong chương này, chúng ta trình bày các thuật toán sắpxếp đơn giản, các thuật toán này dòi hỏi thời gian O(n 2 ) để sắpxế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ắpxế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ắpxếp chứa một số thành phần dữ liệu, và ta cần sắpxế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ắpxếp các sinh viên theo thứ tự chiều cao tăng, khi đó chiều cao là khoá sắp xếp. Từ đây về sau, ta giả thiết rằng, mảng cần được sắpxếp là mảng các đối tượng có kiểu Item, trong đó Item là cấu trúc sau: 187 struct Item { keyType key; // Khoá sắpxếp // Các trường dữ liệu khác }; Vấn đề sắpxế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ắpxếp lại các thành phần của mảng A sao cho: A[0].key <= A[1].key <= <= A[n-1].key 17.1 CÁC THUẬT TOÁN SẮPXẾP ĐƠN GIẢN Mục này trình bày các thuật toán sắpxếp đơn giản: sắpxếp lựa chọn (selection sort), sắpxếp xen vào (insertion sort), và sắpxếp nổi bọt (bubble sort). Thời gian chạy của các thuật toán này là O(n 2 ), trong đó n là cỡ của mảng. 17.1.1 Sắpxếp lựa chọn Ý tưởng của phương pháp sắpxếp lựa chọn là như sau: Ta tìm thành phần có khóa nhỏ nhất trên toàn mảng, giả sử đó là A[k]. Trao đổi A[0] với A[k]. Khi đó A[0] là thành phần có khoá nhỏ nhất trong mảng. Giả sử đến bước thứ i ta đã có A[0].key <= A[1].key <= … <= A[i-1]. Bây giờ ta tìm thành phần có khóa nhỏ nhất trong các thành phần từ A[i] tới A[n-1]. Giả thành phần tìm được là A[k], i <= k <= n-1. Lại trao đổi A[i] với A[k], ta có A[0].key <=…<= A[i].key. Lặp lại cho tới khi i = n-1, ta có mảng A được sắp xếp. Ví dụ. Xét mảng A[0…5] các số nguyên. Kết quả thực hiện các bước đã mô tả được cho trong bảng sau A[0] A[1] A[2] A[3] A[4] A[5] I k 5 9 1 8 3 7 0 2 188 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ắpxếp lựa chọn: void SelectionSort(Item A[] , int n) // Sắpxế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ắpxế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 ∑ − = 2 0 n i (n-1-i)O(1) = O(1)(1 + 2 + …+ n-1) = O(1)n(n-1)/2 = O(n 2 ) Vậy thời gian chạy của hàm sắpxếp lựa chọn là O(n 2 ). 17.1.2 Sắpxếp xen vào 189 Phương pháp sắpxế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 <= … <= A[i-1].key. Ta xen A[i] vào vị trí thích hợp trong đoạn đầu A[0 i-1] để nhận được đoạn A[0 i] được sắp xếp. Với i = 1, đoạn đầu chỉ có một thành phần, đương nhiên là đã được sắp. Lặp lại quá trình đã mô tả với i = 2,…,n-1 ta có mảng được sắp. Việc xen A[i] vào vị trí thích hợp trong đoạn đầu A[o i-1] được tiến hành như sau. Cho chỉ số k chạy từ i, nếu A[k].key < A[k-1].key thì ta trao đổi giá trị của A[k] và A[k-1], rồi giảm k đi 1. Ví dụ. Giả sử ta ta có mảng số nguyên A[0 5] và đoạn đầu A[0 2] đã được sắp 0 1 2 3 4 5 1 4 5 2 9 7 Lúc này i = 3 và k = 3 vì A[3] < A[2], trao đổi A[3] và A[2], ta có 0 1 2 3 4 5 1 4 2 5 9 7 Đến đây k=2, và A[2] < A[1], lại trao đổi A[2] và A[1], ta có 0 1 2 3 4 5 1 2 4 5 9 7 Lúc này k = 1 và A[1] >= A[0] nên ta dừng lại và có đoạn đầu A[0 3] đã được sắp Hàm sắpxếp xen vào được viết như sau: void InsertionSort (Item A[], int n) { (1) for ( int i = 1 ; i < n ; i++) (2) for ( int k = i ; k > 0 ; k ) 190 (3) if (A[k].key < A[k-1].key) swap(A[k],A[k-1]); else break; } Phân tích sắpxế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à ( ) )()1 21)(1(1 2 1 1 nOnOiO n i =−+++= ∑ − = 17.1.3 Sắpxếp nổi bọt Ý tưởng của sắpxế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: 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ắpxếp nổi bọt như sau: void BubbleSort( Item A[] , int n) { 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 (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ắpxếp xen vào ,ta có thể đánh giá thời gian chạy của hàm sắpxếp nổi bọt là O(n 2 ). 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; } } 17.2 SẮPXẾP HOÀ NHẬP Thuật toán sắpxế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ắpxế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ắpxếp bằng 192 cách gọi đệ quy thủ tục sắpxế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ắpxế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 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: 193 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 Đế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; 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++]; 194 10 12 20 31 35 3 5 15 21 26 i A j 6a = 5 7 8 c=9 10 12 13 14 11 3 5 10 12 15 20 21 26 1 0 2 3 4 5 7 8 9 6 (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ắpxếp hoà nhập. Giả sử mảng cần sắpxế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 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 ≤ 2 2 T(n/2 2 ) + 2 (n/2)d + n d …… 195 ≤ 2 k T(n/2 k ) + 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 / 2 k . Khi đó, ta có T(n) ≤ 2 k 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ẮPXẾP NHANH Trong mục này chúng ta trình bày thuật toán sắpxếp được đưa ra bởi Hoare, nổi tiếng với tên gọi là sắpxế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(n 2 ). Tuy nhiên thời gian chạy trung bình là O(n logn). Thuật toán sắpxếp nhanh được thiết kế bởi kỹ thuật chia-để-trị như thuật toán sắpxếp hòa nhập. Nhưng trong thuật toán sắpxế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ắpxế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. 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ắpxế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ắpxếp nhanh là thuật toán đệ quy được biểu diễn bởi hàm đệ quy như sau : 196 [...]... nguyên (8, 1, 4, 1, 5, 2, 6, 5) Hãy sắpxếp mảng này bằng cách sử dụng: a Sắpxếp lựa chọn b Sắpxếp xen vào c Sắpxếp nổi bọt d Sắpxếp nhanh e Sắpxếp hoà nhập f Sắpxế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ắpxếp lựa chọn b Sắpxếp xen vào c Sắpxếp nhanh d Sắpxếp hoà nhập Trong các trường hợp... (A[i].key > A[j].key) { swap(A[i],A[j]); i = j; j = 2 * i + 1; } else break; } } 204 Sử dụng hàm ShiftDown, ta đưa ra thuật toán sắpxế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ắpxế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... xấu nhất của MergeSort cũng là O(n 202 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ắpxếp khác 17.4 SẮPXẾ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ắpxế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 đó... 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] 4 Một thuật toán sắpxếp được xem là ổn định, nếu trật tự của các phần tử có khoá bằng nhau trong mảng đầu vào và trong mảng kết quả là như nhau Trong các thuật toán sắp xếp, thuật toán nào là ổn định, thuật toán nào là không... 12 1 7 13 15 right left Đế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 12 1 7 13 15 k 199 Phân tích sắpxếp nhanh Chúng ta cần đánh giá thời gian chạy T(n) của thuật toán sắpxế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... đó Bằng cách thế lặp ta có : T(n) = T(1) + 2C + 3C + … + nC 200 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ắpxếp nhanh là O(n2) Thời gian trung bình Bây giờ ta đánh giá thời gian trung bình T tb(n) mà QuickSort đòi hòi để sắpxế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)...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 . 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ộ. 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. A[n-1].key 17.1 CÁC THUẬT TOÁN SẮP XẾP ĐƠN GIẢN Mục này trình bày các thuật toán sắp xếp đơn giản: sắp xếp lựa chọn (selection sort), sắp xếp xen vào (insertion sort), và sắp xếp nổi bọt (bubble sort).