Cấu trúc dữ liệu và giải thuật-Đinh Mạnh Tường(chuong 17) doc

21 246 0
Cấu trúc dữ liệu và giải thuật-Đinh Mạnh Tường(chuong 17) doc

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

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(n 2 ) để 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. 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: 187 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[1].key <= <= 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). 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ắp xếp lựa chọn Ý tưởng của phương pháp sắp xế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ắ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 ∑ − = 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ắp xếp lựa chọn là O(n 2 ). 17.1.2 Sắp xếp xen vào 189 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 <= … <= 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ắp xế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ắ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à ( ) )()1 21)(1(1 2 1 1 nOnOiO n i =−+++= ∑ − = 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: 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 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 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(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ẮP XẾP HOÀ NHẬP 192 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 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 193 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: 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]; 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 (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 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 195 T(n) ≤ 2T(n/2) + n d ≤ 2 2 T(n/2 2 ) + 2 (n/2)d + n d …… ≤ 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Ắ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(n 2 ). 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. 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ử 196 [...]... 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 left 14 7 1 7 13 15 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ó :... 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ữ. .. 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... 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 ổn định? 5 Cho mảng chứa n số nguyên và một số nguyên k Ta muốn biết trong mảng có chứa 2 số nguyên có tổng bằng k hay không Chẳng hạn, với mảng (8, 4, 1, 5, 6, 3) và k = 10 thì cầu trả lời là có, vì 4 + 6 = 10 a Hãy đưa ra... lần lặp là n-1 Thân vòng lặp (2), với i = n-1 là 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)... bình T tb(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 : Ttb(n)... 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... đỉ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 . 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,. 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. 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] <

Ngày đăng: 10/08/2014, 16:20

Từ khóa liên quan

Mục lục

  • SẮP XẾP

Tài liệu cùng người dùng

  • Đang cập nhật ...

Tài liệu liên quan