Thuật toán sắpxếp nhanh (Quick Sort)

Một phần của tài liệu tổ chức dữ liệu cho lớp thuật toán chia để trị và ứng dụng (Trang 32 - 79)

a) Ý tƣởng: Để sắp xếp dãy a1, a2 …an giải thuật Quick Sort dựa trên việc phân hoạch dãy ban đầu thành 2 dãy con khác nhau :

- Dãy con 1: Gồm các phần tử a1 …ai có giá trị không lớn hơn x. - Dãy con 2: Gồm các phần tử ai …an có giá trị không nhỏ hơn x.

Với x là giá trị của một phần tử tuỳ ý trong dãy ban đầu. Sau khi thực hiện phân hoạch, dãy ban đầu được chia làm 3 phần:

1. ak< x, với k=1..i 2. ak = x, với k=i..j 3. ak> x, với k=j..N

Trong đó dãy con thứ hai đã có thứ tự, nếu các dãy con 1 và 3 chỉ có 1 phần tử thì chúng cũng đã có thứ tự, khi đó dãy ban đầu đã được sắp. Ngược lại, nếu các dãy con 1 và 3 có nhiều hơn 1 phần tử thì dãy ban đầu chỉ được sắp khi các dãy con 1, 3 có thứ tự. Để sắp xếp dãy con 1 và 3, lần lượt tiến hành phân hoạch từng dãy con theo cùng phương pháp phân hoạch dãy ban đầu vừa trình bày…

Vấn đề còn lại bây giờ là xây dựng một thủ tục phân hoạch cho dãy ban đầu, điều này phụ thuộc nhiều vào việc xác định phần tử làm mốc ban đầu, mốc được chọn làm sao để tạo ra hai dãy con cân bằng nhau, điều này rất mất thời gian. Vì vậy người ta thường chọn phần tử đầu tiên của mảng làm mốc, giả sử dãy ban đầu là mảng gồm có các phần tử a[i]...a[j], tức là lấy p= a[i] làm mốc.Sau đó sử dụng các biến L chạy từ trái sang phải bắt đầu từ vị trí thứ i, biến k chạy từ phải sang trái bắt đầu từ j+1. Biến L được tăng cho tới khi

a[L] >p, còn biến k được giảm cho tới khi a[k]<=p, Nếu L<k thì ta đổi giá trị của a[L] và

a[k]. Quá trình đó lặp đi lặp lại cho đến khi L>k. Cuối cùng ta trao đổi vị trí a[i] và a[k] để đặt mốc vào đúng vị trí của nó.Nhận xét:

Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/

- Về nguyên tắc, có thể chọn giá trị mốc p là một phần tử tuỳ ý trong dãy, nhưng để đơn giản, dễ diễn đạt giải thuật, phần tử có vị trí giữa thường được chọn, khi đó p :=int((i+j)/2).

- Giá trị mốc p được chọn sẽ tác động đến hiệu quả thực hiện thuật toán vì nó quyết định số lần phân hoạch. Số lần phân hoạch sẽ ít nhất nếu ta chọn được p là phần tử trung bình (median) của dãy. Tuy nhiên, trên thực tế ta khó chọn được phần tử này nên thường chọn phần tử bất kỳ hoặc phần tử nằm chính giữa dãy làm mốc với hy vọng nó có thể gần với giá trị median (HS viết thủ tục với p nằm giữa dãy).

b) Thuật toán

Có thể phát biểu giải thuật sắp xếp QuickSort một cách đệ qui như sau Bước 1 :

- Phân hoạch dẫy ai…ajthành các dãy con : - Dãy con 1 : a[i]…a[L] < x

- Dãy con 2 : a[L]…a[j] >= x Bước 2 :

- Nếu (i< L) Phân hoạch dãy a[i]…a[L] - Nếu (L<j) Phân hoạch dãy a[L]…a[j] Cài đặt thuật toán trên C:

void QUICKSORT(int X[], int n) {

Partition(1,n); }

void Partition (int L, int R) {

if (L>=R) return;

pirot=(L+R)div 2; // Luôn lấy chốt ở vị trí gần chính giữa dãy key = X[pirot]; int i=L;int j= R;

while (i<=j) {

while (X[i] < key) i=i+1; while (X[j] > key) j=j-1; if (i<j) { swap(X[i],X[j]); i=i+1; j=j-1; } }

Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/

Partition(L,j); Partition(i,R); }

c) Ví dụ

Cho dãy số a: 4 9 3 7 5 3 8

Công việc sắp xếp dãy trên bằng thuật toán QuickSort được tiến hành như sau: Phân hoạch đoạn l = 0, r = 6, x = a[3] = 7

4 9 3 7 5 3 8

Phân hoạch đoạn l = 0, r = 2, x = a[1] = 3

4 3 3 5 7 9 8 (adsbygoogle = window.adsbygoogle || []).push({});

Phân hoạch đoạn l = 4, r = 6, x = a[5] = 9

3 3 4 5 7 9 8

Dừng

3 3 4 5 7 8 9

d) Phân tích, đánh giá

Hiệu quả của giải thuật QuickSort phụ thuộc vào việc chọn giá trị mốc. Trường hợp tốt nhất xảy ra nếu mỗi lần phân hoạch đều chọn được phần tử median làm mốc, khi đó dãy được phân chia thành 2 phần bằng nhau và chỉ cần log(n) lần phân hoạch thì sắp xếp xong. Nhưng nếu mỗi lần phân hoạch lại chọn phải phần tử có giá trị cực đại hay cực tiểu làm mốc, dãy sẽ bị phân thành 2 phần không đều: một phần chỉ có 1 phần tử, phần còn lại có (n-1) phần tử, do vậy cần phân hoạch n lần mới sắp xếp xong.

ẽ : 1,2,3,4,…N ( 1,2,..N-1, N, N, N-1,…..2.1) 03 ph ảyra.

Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/

Ta có bảng tổng kếtsau:

Bảng 2.2Độ phức tạp của thuật toán sắp xếp nhanh

Trƣờng hợp Độ phức tạp

Trung bình N*log(N)

Xấu nhất N2

2.4.3 Lớp bài toán tối ƣu

2.4.3.1 Bài toán dãy con dài nhất

Cho mảng A[1..n]. Mảng A[p..q] được gọi là mảng con của A. Trọng lượng mảng bằng tổng các phần tử. Tìm mảng con có trọng lượng lớn nhất (1≤ pqn).

Để đơn giản ta chỉ xét bài toán tìm trọng lượng của mảng con lớn nhất còn việc tìm vị trí thì chỉ là thêm vào bước lưu lại vị trí trong thuật toán. Ta có thể dễ dàng đưa ra thuật toán tìm kiếm trực tiếp bằng cách duyệt hết các dãy con có thể của mảng A như sau:

void BruteForceNaice; {

Max1 = -MaxInt;

for (i = 1; i<= n; i++) // i là điểm bắt đầu của dãy con for( j =i; j<= n; j++) // j là điểm kết thúc của dãy con {

s= 0;

for ( k = i; k<= j; k++) // Tính trọng lượng của dãy s = s + A[k]

if (s > Max1) Max1 = S }

}

Phân tích độ phức tạp của thuật toán:Lấy s = s + A[k] làm câu lệnh đặc trưng, ta có số lần thực hiện câu lệnh đặc trưng là 3

1

( )

j

n n

i j i k i (adsbygoogle = window.adsbygoogle || []).push({});

k O n . Do đó, thời gian T(n) = O(n3) . Nếu để ý, ta có thể giảm độ phức tạp của thuật toán bằng cách giảm bớt vòng lặp trong cùng (vòng lặp theo k): 1 1 1 [ ] [ ] [ ] j j k k

a k a j a k .Khi đó thuật toán có thể được viết một cách tóm tắt như sau:

for ( i = 1; i<= n; i++) for ( j = i; j<= n; j++) {

s = s + A[j]; //Câu lệnh đặc trưng if (s > max1) max1 = s;

Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/

Lấy s = s + A[j] làm câu lệnh đặc trưng thì ta có số lần thực hiện câu lệnh đặc trưng

là 2

1

( )

n n

i j i

j O n  Thời gian của thuật toán T(n) = O(n2). Cách tiếp cận chia để trị

 Chia: Chia mảng A ra thành hai mảng con với chênh lệch độ dài ít nhất, kí hiệu là AL, AR.

 Trị: Tính mảng con lớn nhất của mỗi nửa mảng A một cách đệ quy. Gọi WL, WR là trọng lượng của mảng con lớn nhất trong AL, AR.

 Tổng hợp: Max (WL, WR).

WM = WML + WMR Cài đặt thuật toán:

void MaxSubVector(A, i, j); { If (i == j) return a[i] Else { m = (i + j)/2; WL = MaxSubVector(a, i, m); WR = MaxSubVector(a, m+1, j); WM = MaxLeftVetor(a, i, m) + MaxRightVector(a, m+1, j); Return Max(WL, WR, WM ) } }

Các hàm MaxLeftVector, Max RightVector được cài đặt như sau: void MaxLeftVector(a, i, j);

{

MaxSum = -Maxint; Sum = 0; for( k = j;k>= i;k--) {

Sum = Sum + A[k];

MaxSum = Max(Sum,MaxSum); }

Return MaxSum; }

Tương tự với hàm MaxRightVector là. for (k = i;k<= j;k++)

{

Sum = Sum + A[k];

Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/

}

Phân tích độ phức tạp: Thời gian chạy thủ tục MaxLeftVector và MaxRightVector là O(m) (m = j - i + 1). Gọi T(n) là thời gian tính, giả thiết n = 2k . Ta có:

Nếu n = 1 thì T(n) = 1 (adsbygoogle = window.adsbygoogle || []).push({});

Nếu n > 1 thì việc tính WM đòi hỏi thời gian n/2 + n/2 = n  T(n) = 2T(n/2) + n

Theo định lý thợ ta có: ( )T n O(log )n

2.4.3.2 Bài toán tháp Hà Nội

Có 3 chiếc cọc và một bộ n chiếc đĩa. Các đĩa này có kích thước khác nhau và mỗi đĩa đều có 1 lỗ ở giữa để có thể xuyên chúng vào các cọc. Ban đầu, tất cả các đĩa đều nằm trên 1 cọc, trong đó, đĩa nhỏ hơn bao giờ cùng nằm trên đĩa lớn hơn. Yêu cầu của bài toán là chuyển bộ n đĩa từ cọc ban đầu A sang cọc đích C (có thể sử dụng cọc trung gian B), với các điều kiện: mỗi lần chuyển 1 đĩa, trong mọi trường hợp, đĩa có kích thước nhỏ hơn bao giờ cũng phải nằm trên đĩa có kích thước lớn hơn [3].

Với n = 1, có thể thực hiện yêu cầu bài toán bằng cách chuyển trực tiếp đĩa 1 từ cọc A sang cọc C. Với n = 2 có thể thực hiện như sau:

- Chuyển đĩa nhỏ từ cọc A sang cọc trung gian B. - Chuyển đĩa lớn từ cọc A sang cọc đích C.

- Cuối cùng, chuyển đĩa nhỏ từ cọc trung gian B sang cọc đích C.

Như vậy, cả 2 đĩa đã được chuyển sang cọc đích C và không có tình huống nào đĩa lớn nằm trên đĩa nhỏ.Với n > 2, giả sử ta đã có cách chuyển n – 1 đĩa, ta thực hiện như sau:

- Lấy cọc đích C làm cọc trung gian để chuyển n – 1 đĩa bên trên sang cọc trung gian B.

- Chuyển cọc dưới cùng (cọc thứ n) sang cọc đích C.

- Lấy cọc ban đầu A làm cọc trung gian để chuyển n – 1 đĩa từ cọc trung gian B sang cọc đích C.

Như vậy, cả hai đĩa đã được chuyển sang cọc đích C và không có tình huống nào đĩa lớn nằm trên đĩa nhỏ. Ta thấy toàn bộ n đĩa đã được chuyển từ cọc A sang cọc C và không vi phạm bất cứ điều kiện nào của bài toán.Ở đây, ta thấy rằng bài toán chuyển n cọc đã được chuyển về bài toán đơn giản hơn là chuyển n - 1 cọc. Điểm dừng của thuật toán đệ qui là khi n = 1 và ta chuyển thẳng cọc này từ cọc ban đầu sang cọc đích. Tính chất chia để trị của thuật toán này thể hiện ở chỗ: Bài toán chuyển n đĩa được chia làm 2 bài toán nhỏ

Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/

hơn là chuyển n - 1 đĩa. Lần thứ nhất chuyển n - 1 đĩa từ cọc A sang cọc trung gian B, và lần thứ 2 chuyển n - 1 đĩa từ cọc trung gian B sang cọc đích C.

Cài đặt đệ quy cho thuật toán như sau: hàm Chuyển (int n, char a char c) thực hiện việc chuyển đĩa thứ n từ cọc A sang cọc C. Hàm Thaphanoi (int n, char a, char c, char b)

là hàm đệ quy thực hiện việc chuyển n đĩa từ cọc A sang cọc C, sử dụng cọc trung gian là cọc B.

VoidChuyen (int n, char a, char c) {

Print (“Chuyendia thu %d tu coc %c sang coc %c\n”, n, a, c); Return;

Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/

Void ThapHaNoi (int n, char a, char c, char b) { If (n = = 1) chuyen (1, a, c); Else { Thaphanoi (n-1, a, b, c); Chuyen (n, a, c); Thaphanoi (n – 1, b, c, a); } Return; }

Hàm Chuyển thực hiện thao tác in ra 1 dòng cho biết chuyển đĩa thứ mấy từ cọc nào sang cọc nào. Hàm ThapHaNoi kiểm tra nếu số đĩa bằng 1 thì thực hiện chuyển trực tiếp đĩa từ cọc A sang cọc C. Nếu số đĩa lớn hơn 1, có ba lệnh được thực hiện:

- Lời gọi đệ quy ThapHaNoi(n – 1, a, b, c) để chuyển n – 1 đĩa từ cọc A sang cọc B, sử dụng cọc C làm trung gian.

- Thực hiện chuyển đĩa thứ n từ cọc A sang cọc C.

- Lời gọi đệ quy ThapHaNoi(n-1, b, c, a) để chuyển n – 1 đĩa từ cọc B sang cọc C, sử dụng cọc A làm cọc trung gian.

Độ phức tạp của thuật toán là 2n – 1. Nghĩa là để chuyển n cọc thì mất 2n – 1 thao tác chuyển. Ta sẽ chứng minh điều này bằng phương pháp quy nạp toán học. Với n = 1 thì số lần chuyển là 1 = 2*1 – 1. Giả sử giả thiết đúng với n - 1, tức là để chuyển n - 1 đĩa cần thực hiện 2n – 1 thao tác chuyển. Ta sẽ chứng minh rằng để chuyển n đĩa cần 2n - 1 thao tác chuyển. Thật vậy, theo phương pháp chuyển của giải thuật thì có 3 bước. Bước 1 chuyển n – 1 đĩa từ cọc A sang cọc B mất 2n - 1 thao tác. Bước 2 chuyển 1 đĩa từ cọc A sang cọc C mất 1 thao tác. Bước 3 chuyển n – 1 đĩa từ cọc B sang cọc C mất 2n - 1 - 1 thao tác. Tổng cộng ta mất (2n - 1 - 1) + (2n - 1 - 1) + 1 = 2 * 2n - 1 - 1 = 2n - 1 thao tác chuyển. Đó là điều cần chứng minh. Như vậy, thuật toán có cấp độ tăng rất lớn. Nói về cấp độ tăng này, có một truyền thuyết vui về bài toán tháp Hà Nội như sau: Ngày tận thế sẽ đến khi các nhà sư ở một ngôi chùa thực hiện xong việc chuyển 40 chiếc đĩa theo quy tắc như bài toán vừa trình bày. Với độ phức tạp của bài toán vừa tính được, nếu giả sử mỗi lần chuyển 1 đĩa từ cọc này sang cọc khác mất một giây thì với 240 – 1 lần chuyển, các nhà sư này phải mất ít nhất 34.800 năm thì mới có thể chuyển xong toàn bộ số đĩa này.

2.4.3.5 Bài toán xếp lịch thi đấu

Giả sử cần lập một lịch thi đấu Tennis cho n = 2 vận động viên (VĐV). Mỗi vận động viên phải thi đấu với lần lượt n-1 vận động viên khác, mỗi ngày thi đấu 1 trận. Như vậy n-1 là số ngày thi đấu tối thiểu phải có. Chúng ta cần lập lịch thi đấu bằng cách thiết

Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/

lập ma trận có n hàng, n-1 cột. Giá trị số tại vị trí (i,j) (hàng i, cột j) chỉ ra vận động viên cần thi đấu với vận động viên i trong ngày thứ j.

Sử dụng kỹ thuật Chia để trị, chúng ta hãy lập lịch thi đấu cho nửa (n/2) số vận động viên đầu tiên. Bằng việc sử dụng lời gọi đệ quy chúng ta đưa bài toán về trường hợp chỉ có 2 vận động viên. Trong trường hợp n=8, lịch thi đấu cho 4 người đầu tiên của danh sách chiếm nửa trái trên của ma trận (4 hàng, 3 cột). Phần nửa trái dưới (4 hàng, 3 cột) của ma trận là lịch thi đấu của 4 VĐV còn lại (từ 5 đến 8). Phần này thu được từ nửa trái trên bằng cách cộng 4 vào mỗi phần tử tương ứng của ma trận. Để điền nốt các phần còn lại của ma trận chúng ta chỉ cần xác định lịch thi đấu giữa các VĐV với số thấp (≤ n/2) với các VĐV với số cao (≥ n/2). Để làm việc này chúng ta xếp các VĐV từ 1 đến n/2 đấu lần lượt với các VĐV số cao vào ngày 4. Các ngày còn lại thu được từ ngày 4 bằng cách hoán vị vòng quanh các VĐV với số thứ tự cao. (adsbygoogle = window.adsbygoogle || []).push({});

Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/

CHƢƠNG 3. ỨNG DỤNG THUẬT TOÁN CHIA ĐỂ TRỊ GIẢI BÀI TOÁN NHÂN HAI SỐ NGUYÊN LỚN

3.1 Mô tả bài toán

Rất nhiều ứng dụng trong thực tế đòi hỏi phải xử lí các số rất lớn, nằm ngoài khoảng biểu diễn của các kiểu cơ sở của ngôn ngữ lập trình. Để giải quyết các yêu cầu đó, chúng ta phải xây dựng các kiểu số rất lớn và xây dựng các phép toán tương ứng. Trong phần này ta chỉ xét phép toán nhân đối với hai số rất lớn. Giả thiết cả hai đều có n chữ số và được biểu diễn bằng mảng. Bài toán nhân 2 số lớn phát biểu như sau:

- Input. A,B là 2 số nguyên có n chữ số: A=A1A2….An và B=B1B2….Bn. - Output. C=A.B

Trong các ngôn ngữ lập trình đều có kiểu dữ liệu số nhưng nhìn chung các kiểu này đều có miền giá trị hạn chế nên khi có một ứng dụng trên số nguyên lớn (hàng chục, hàng trăm chữ số) thì kiểu số nguyên định sẵn không đáp ứng được. Trong trường hợp đó, người lập trình phải tìm một cấu trúc dữ liệu thích hợp để biểu diễn cho một số nguyên, chẳng hạn ta có thể dùng một chuỗi kí tự để biểu diễn cho một số nguyên, trong đó mỗi kí tự lưu trữ một chữ số. Để thao tác được trên các số nguyên được biểu diễn bởi một cấu trúc mới, người lập trình phải xây dựng các phép toán cho số nguyên. Trong chương này, tôi sẽ đề cập đến bài toán nhân hai số nguyên lớn và các cách tiếp cận để giải quyết.

3.2 Thuật toán nhân tự nhiên

Thuật toán tự nhiên (Brute-force) của bài toán nhân 2 số lớn là giải thuật nhân tay ta vẫn thực hiện: lần lượt nhân từng chữ số của số thứ hai với số thứ nhất, dịch kết quả theo vị trí và cộng các kết quả trung gian lại [7].

Chẳng hạn để nhân A=981 và B=1234 ta tiến hành như sau: 981 1234 --- 3924 + 1943 1962 981 --- 1210554

Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/

Một phần của tài liệu tổ chức dữ liệu cho lớp thuật toán chia để trị và ứng dụng (Trang 32 - 79)