Thuật toán tìm kiếm nhịphân

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 25 - 79)

a) Ý tƣởng: Thuật toán tìm kiếm nhị phân là thuật toán được thiết kế dựa trên chiến

lược chia-để-trị. Cho mảng A cỡ n được sắp xếp theo thứ tự tăng dần: A[0] ≤…≤ A[n-1]. Với x cho trước, ta cần tìm xem x có chứa trong mảng A hay không, tức là có hay không chỉ số 0 ≤ i ≤ n-1 sao cho A[i] = x[3].

Kỹ thuật chia-để-trị gợi ý ta: Chia mảng A[0…n-1] thành 2 mảng con cỡ n/2 là A[0…k-1] và A[k+1…n-1], trong đó k là chỉ số đứng giữa mảng. So sánh x với A[k]. Nếu x = A[k] thì mảng A chứa x và i = k. Nếu không, do tính được sắp của mảng A, nếu x A[k] ta tìm x trong mảng A[0…k-1], còn nếu x A[k] ta tìm x trong mảng A[k+1…n-1].

Áp dụng cho bài toán sau:

- Input: Dãy gồm N số nguyên k1, k2,..., kN đôi mộ nguyên x.

- Output: Chỉ số i ki = x hoặc thông báo không có số hạng nào của dãy có giá trị trùng với x.

- Ý tưởng:

Giua

Giua=[ (N+1)/2].

Khi đó, chỉ xảy ra một trong ba trường hợp sau:

- Nếu kGiua = x thì Giua là chỉ số cần tìm. Việc tìm kiếm kết thúc.

- Nếu kGiua> k thì do dãy khoa là dãy đã được sắp xếp nên việc tìm kiếm tiếp theo chỉ xét trên dãy k1, ka2,..., kGiua–1 (phạm vi tìm kiếm mới bằng khoảng một nửa phạm vi tìm kiếm cũ).

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

- Nếu aGiua< k thì thực hiện tìm kiếm trên dãy kGiua+1, kGiua+2,..., kN.

Quá trình trên sẽ được lặp lại một số lần cho đến khi hoặc đã tìm thấy x trong dãy khoa hoặc khẳng định dãy khoa không chứa giá trị bằng x.

b) Mô tả thuật toán

- 1.Nhập N, các giá trị k1, k2,..., kNvà giá trị khóa x. - 2. Dau 1, Cuoi N.

- 3. Giua Dau Cuoi

2 .

- 4. Nếu kGiua = x thì thông báo chỉ số Giua, rồi kết thúc

- 5. Nếu kGiua> x thì đặt Cuoi = Giua – 1 rồ ớc 7. - 6. Dau Giua + 1

- > hông báo dãy không có số hạng có giá trị trùng với x, rồi kết thúc.

- 8 Quay lại bước 3. Cài đặt thuật toán như sau:

intBINARYSEARCH(int a[max],int x, int l, int r) {

int mid;

if (r > l) return 0; mid= (l + r)/2;

if ( x == a[mid] ) return 1;

if ( x > a[mid] ) return BINARYSEARCH (a, x, mid + 1, r); returnBINARYSEARCH (a, x, l, mid - 1);

}

c) Ví dụ: Chúng ta xem các ví dụ mô phỏng các bước thực hiện thuật toán trên.

k = 21, N =10 i 1 2 3 4 5 6 7 8 9 10 A 2 4 5 6 9 21 22 30 31 33 Dau 1 6 6 Cuoi 10 10 7 Giua 5 8 6 aGiua 9 30 21 Lượt 0 1 2 (adsbygoogle = window.adsbygoogle || []).push({});

Sau hai lượt thì aGiua = k. Vậy chỉ số cần tìm là i = Giua = 6. k = 25, N =10

i 1 2 3 4 5 6 7 8 9 10

Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/ Dau 1 6 6 7 8 Cuoi 10 10 7 7 7 Giua 5 8 6 7 aGiua 9 30 21 22 Lượt 0 1 2 3 4

Tại lượt thứ tư Dau>Cuoi nên kết luận trong dãy A không có toán hạng nào có giá trị là 25 cả.

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

Trường hợp giải thuật tìm nhị phân ta có bảng phân tích sau:

Bảng 2.1Độ phức tạp của thuật toán tìm kiếm nhị phân

Trƣờng hợp Số lần so sánh Giải thích

Tốt nhất 1 Phần tử giữa của mảng ban đầu có giá trị x.

Xấu nhất log2 N Phần tử cần tìm nằm ở cuối mảng

Trung bình log2 N/2 Giả sử xác xuất các phần tử trong mảng nhận giá trị x là như nhau.

Giải thuật tìm nhị phân phụ thuộc vào thứ tự của các phần tử trong mảng để định hướng trong quá trình tìm kiếm, do vậy chỉ áp dụng được cho những dãy đã có thứ tự. Thuật toán tìm kiếm nhị phân tiết kiếm thời gian hơn rất nhiều so với giải thuật tìm tuyến tính do Onhịphân(log2n) <Otuyếntính(n) [1]. Tuy nhiên khi muốn áp dụng giải thuật tìm nhị phân cần phải xét đến thời gian sắp xếp dãy số để thỏa điều kiện dãy số có thứ tự, thời gian này không nhỏ, và khi dãy số biến động cần phải tiến hành sắp xếp lại,…tất cả các nhu cầu đó tạo ra khuyết điểm chính cho giải thuật tìm nhị phân.

2.3.1.2 Bài toán tìm Max và min

a) Ý tƣởng: Cho mảng A cỡ n, chúng ta cần tìm giá trị lớn nhất (max) và nhỏ nhất

(min) của mảng này. Bài toán đơn giản này có thể giải quyết bằng các thuật toán khác nhau. Một thuật toán rất tự nhiên và đơn giản là như nhau. Đầu tiên ta lấy max, min là giá trị đầu tiên A[0] của mảng. Sau đó so sánh max, min với từng giá trị A[i], 1 ≤ i ≤ n-1, và cập nhật max, min một cách thích ứng [3].

Thuật toán này được mô tả bởi hàm sau:

SiMaxMin (A, max, min) {

max = min = A[0]; for ( i = 1 ; i n , i ++)

if (A[i] max) max = A[i]; else if (A[i] min)

min = A[i]; }

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

Thời gian thực hiện thuật toán này được quyết định bởi số phép so sánh x với các thành phần A[i]. Số lần lặp trong lệnh lặp for là n-1. Trong trường hợp xấu nhất (mảng A được sắp theo thứ tự giảm dần), mỗi lần lặp ta cần thực hiện 2 phép so sánh. Như vậy, trong trường hợp xấu nhất, ta cần thực hiện 2(n-1) phép so sánh, tức là thời gian chạy của thuật toán là O(n).

Bây giờ ta áp dụng kỹ thuật chia-để-trị để đưa ra một thuật toán khác. Ta chia mảng A[0..n-1] thành các mảng con A[0..k] và A[k+1..n-1] với k = [n/2]. Nếu tìm được max, min của các mảng con A[0..k] và A[k+1..n-1], ta dễ dàng xác định được max, min trên mảng A[0..n-1]. Để tìm max, min trên mảng con ta tiếp tục chia đôi chúng. Quá trình sẽ dừng lại khi ta nhận được mảng con chỉ có một hoặc hai phần tử. Trong các trường hợp này ta xác định được dễ dàng max, min.

b)Mô tả thuật toán

Do đó, ta có thể đưa ra thuật toán sau:

MaxMin (i, j, max, min)

// Biến max, min ghi lại giá trị lớn nhất, nhỏ nhất trong mảng A[i..j] {

if (i = = j)

max = min = A[i]; else if (i = = j-1)

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

max = A[j];min = A[i]; }

else {

max = A[i]; min = A[j]; }

else {

mid = (i+j) / 2;

MaxMin (i, mid, max1, min1); MaxMin (mid + 1, j, max2, min2); if (max 1 max2) max = max2; else max = max1;

if (min1 min2) min = min1; else min = min2;

} }

Cài đặt thuật toán trên C như sau:

void MinMax(int a[], int dau, int cuoi, int &min, int &max) {

int min1,min2,max1,max2; if (dau==cuoi)

Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/ min=a[dau]; max=a[dau]; } else { minmax(a,dau,(dau+cuoi)/2,min1,max1); minmax(a,(dau+cuoi)/2+1,cuoi,min2,max2);

If (min1 < min2) min=min1; else min=min2;

if (max1 > max2) max=max1; else max=max2;

} }

c) Đánh giá độ phức tạp

Bây giờ ta đánh giá thời gian chạy của thuật toán này. Gọi T(n) là số phép so sánh cần thực hiện. Không khó khăn thấy rằng, T(n) được xác định bởi quan hệ đệ quy sau.

T(1) = 0, T(2) = 1

T(n) = 2T(n/2) + 2 với n 2

Áp dụng phương pháp thế lặp, ta tính được T(n) như sau:

T(n) = 2 T(n/2) + 2 = 22T(n/22) + 22 + 2 = 23T(n/23) + 23 + 22 + 2 ………

= 2kT(n/2k) + 2k + 2k-1 +… + 2

Với k là số nguyên dương sao cho 2k ≤ n 2k+1, ta có

T(n) = 2kT(1) + 2k+1 – 2 = 2k+1 – 2 ≤ 2(n-1)

Như vậy, T(n) = O(n).

2.4.2 Lớp bài toán sắp xếp

2.4.2.1 Thuật toán sắp xếp trộn (Merge Sort)

Một ví dụ lâu đời khác là thuật toán sắp xếp trộn, phát hiện bởi John Von Neumann năm 1945 [3]. Ý tưởngthực tế là thông thường dãy dữ liệu đang được lưu trữ là dãy đã sắp xếp, các dữ liệu mới bổ sung thêm vào cuối dãy đã có. Xuất hiện nhu cầu cần sắp xếp lại để dãy dữ liệu sau khi nhập bổ sung thêm các phần tử mới phải được sắp xếp lại.

a) Ý tƣởng: ần sắp xế ộn” hai dãy đã được sắp thành

một dãy được sắ ủa thuật toán MergeSort. (adsbygoogle = window.adsbygoogle || []).push({});

b)Mô tả thuật toán

Trộn (Merge): Cho hai dãy đã sắp xếp B={b1,b2,…bm} và C={ c1,c2,…,cn} cần trộn thành dãy D={d1,d2,…,dm+n ợc sắp.

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

i) Lần lượt xác định di ( 1<=i<=n+m) bằng cách chọn phần tử nhỏ hơn trong hai phần tử bj và ck (1<=j<=m; 1<=k<=n) tại mỗi bước.

ii) Trong cài đặt thường thêm một phần tử có giá trị lớn hơn giá trị các phần tử trong dãy vào cuối mỗi dãy B và C ( chẳng hạn, bm+1= Maxint và cn+1= Maxint, thường gọi là khóa cầm canh) để khi tất cả các phần tử của một dãy đã được lựa chọn cho dãy D thì các phần tử còn lại của dãy kia sẽ chuyển thành các phần tử còn lại của dãy D.

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

// Thủ tục trộn gọi đệ quy

voidMERGESORT(int X[], int L, int R)

{ int M; if (L<R){ M=int((L+R)/2); MERGESORT (X,L,M); MERGESORT (X,M+1,R); Merge(X,L,M+1,R); } }

// Thủ tục trộn hai đoạn đã sắp xếp trong một dãy

voidMERGE(int a[], int k1, int k2, int k3) {

inti,j,k,t; inttemp[]; i=k1; j=k2; k=k3; while (i<k2) and (j<=k3)

{ if (a[i]<=a[j]){ temmp[k]=a[i]; i=i+1; } else{ temp[k]=a[j]; j=j+1; } k=k+1; } if (i>=k2) for(t=j;t<=k3,t++) temp[k+t-j]=a[t]; else for(t=i;t<=k2,t++) temp[k+t-i]=a[t]; for(k=k1;t<=k3,k++)a[k]=temp[k]; } c) Ví dụ

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

Hình 2.3 Ví dụ thuật toán sắp xếp trộn

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

Sắp xếp trộn là một thuật toán sắp xếp cổ điển nhất nhưng cho tới nay đó là thuật toán được coi là thuật toán sắp xếp ngoài mẫu mực.Phép toán tích cực trong phép trộn là phép đưa một phần tử khóa vào dãy kết quả nên độ phức tạp của trộn là O(N).

Trong sắp xếp trộn sử dụng không quá [logn] lần trộn nên độ phức tạp của thuật toán sắp xếp trộn là O(NlgN).Nhược điểm là phải dùng thêm không gian để lưu trữ dãy khóa d

(trong việc trộn).

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

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). (adsbygoogle = window.adsbygoogle || []).push({});

- 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

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: (adsbygoogle = window.adsbygoogle || []).push({});

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

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

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

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

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 25 - 79)