Cong ty Cong Nghe Tin hoc Nha truong http://www.vnschool.net
Bài toán tìm kiếm và phương pháp tìm kiếm cơ bản
24/01/2009

a. Bài toán: Tìm kiếm trên cây nhị phân là một thuật toán đơn giản, một phương pháp tìm kiếm động hiệu quả. Phương pháp này là một trong các thuật toán nền móng của khoa học máy tính. Sở dĩ thuật tóan này được bàn ở đây và được coi là cơ bản bởi lẽ nó đơn giản; nhưng lại là phương pháp tìm kiếm được chọn lựa trong nhiều trường hợp ứng dụng.


Như chúng ta đã biết trong một cây: mỗi nút chỉ được trỏ tới bởi duy nhất một nút khác gọi là cha của nó. Một cây nhị phân thì mỗi nút có 2 liền kề trái và phải. Với việc tìm kiếm, mỗi nút của cây có một mẩu tin chứa giá trị khóa. Không mất tính tổng quát, ta giả thiết rằng: trong cây – tìm – kiếm – nhị - phân tất cả các mẩu tin với các khóa nhỏ hơn thì ở trong cây – con – trái vàtất cả các mẩu tin trong cây con phải có giá trị khóa lớn hơn hay bằng nhau. Chúng tathấy rằng sẽ hoàn toàn đơn giản để đảm bảo cho cây tìm kiếm nhị phân thỏa mãn định nghĩa của nó khi chèn thêmvào một cây nút mới.

Thủ tục tìm kiếm giống như thủ tục tìmkiếmnhiphân ta đã xét ở phần trước. Tất nhiên, chúng ta sẽ luôn bám sát với định nghĩa của cây nhị phân

b. Hướng giải quyết

Để tìm một mẩu tin với khóa k đã cho, trước tiên ta so sánh nó với nút gốc nếu nó nhỏ hơn thì đi đến cây con trái. Nếu bằng thì dừng, nếu nó lớn hơn thì đi đến cây con phải.

Áp dụng đệ quy quá trình trên cho các cây con. Trong mỗi bước, chúng ta chắc chắn rằng không có bộ phận nào của cây ngoài “cây con hiện hành” có thể chứa các mẩu tin với khóa k, và “cây con hiện hành” ngày càng nhỏ hơn. Thủ tục sẽ dừng khi có một mẩu tin với khóa k được tìm thấy hoặc “cây con hiện hành” trở nên trống. Nghĩa là không có một mẩu tin nào chứa khóa k

Trong tìm kiếm nhị phân đã xét ở phần trước. Chúng ta dùng một cây nhị phân để mô tả dãy của các phép so sánh được tạo bởi một hàm tìm kiếm trong mảng. Ở phần tìm kiếm cây nhị phân này, chúng ta xây dựng một cấu trúc dữ liệu gồm các mẩu tin được liên kết với nhau và dùng cấu trúc dữ liệu này cho việc tìm kiếm.

Xét hàm tìm kiếm cây nhị phân sau:

Type lienket = ↑ diem;

diem = record

khoa, thongtin: integer;

l, r : lienket;

end;

var t, z, dau: lienket;

function timkiemcaynhiphan(k : integer; x : lienket): lienket;

begin

z ↑.khoa:= k;

repeat

if (k< x ↑.khoa)then

x:= x ↑.l

else x: = x↑.r;

untilk = x↑.khoa;

timkiemcaynhiphan:= x;

end;

end;

Trong hàm trên ta quy ước: liên kết bên phải của nút dau trỏ tới nút gốc của cây và khóa của nút dau nhỏ hơn tất cả các nút của các khóa khác (thường thì ta cho luôn khóa của nút dau bằng 0 và giả sử tất cả các khóa khác đều nguyên). Như vậy liên kết trái của dau sẽ không được dùng. Bạn sẽ thấy được sự cần thiết của nút dau khi ta dùng nó để thao tác trong hàm chèn sau này.

Như vậy theo thuật toán trên thì để tìm kiếm một mẩu tin có khóa k, chúng ta cho x:= timkiemcaynhiphan (x, dau). Nếu một nút không có cây con trái (hay phải) thì liên kết trái (hay phải ) của nó được trỏ tới nút đuôiz.

Ta đã xét trong trường hợp tìm kiếm tuần tự, chúng ta đã đặt giá trị dạng muốn tìm ở trong z để dừng một quá trình tìm kiếm không thành công. Do đó “cây con hiện hành” trỏ tới x không bao giờ trở thành rỗng và mọi quá trình tìm kiếm đều “thành công”. Trong chương trình gọi hàm tìm kiếm có thể kiểm tra liên kết được trả về có trỏ đến nút z hay không để xác định quá trình tìm kiếm có thành công hay không.

Không mất tính tổng quát, ta quy ước:

- Các liên kết trỏ tới z như trỏ tới các nút ngoài.

- Tất cả các quá trình tìm kiếm không thành công đều kết thúc ở nút ngoài

- Các nút thông thường có chứa các khóa gọi là các nút trong Chú ý: Khi ta đưa thêm khái niệm nút ngoài thì lẽ ra mỗi nút trong đều trỏ đến hai nút khác của cây. Song về mặt cài đặt chúng ta cho tất cả các nút biểu diễn chỉ bởi một nút z duy nhất.

Trường hợp cây rỗng sẽ được biểu diễn bởi một liên kết phải của nút dau trỏ tới z được tạo lập bởi đoạn chương trình sau:

procedure khoitaocay;

begin

new (z);

z↑.l := z↑.r;

z↑.r := z;

new(dau);

dau ↑. khoa := 0;

dau ↑.r := z;

End.

Khởi tạo này trỏ các liên kết của z đến chính nó. Chúng ta sẽ sử dụng thủ tục khởi tạo để đảm bảo an toàn trong các chương trình nâng cao sau này.

Xét ví dụ áp dụng hàm tìm kiếm khóa I trong cây nhị phân sau:

Trước tiên I được so sánh với A ở gốc. Vì I lớn hơn nên nó tiếp tục được so sánh với S. Cứ tiếp tục như thế nó sẽ được so sánh với E, R, và cuối cùng là H. Các liên kết trong nút chứa H trỏ tới z và quá trình tìm kiếm kết thúc: I được so sánh với chính nó ở trong z và kết quả tìm kiếm kết thúc không thành công.

Ta quy ước rằng các liên kết trỏ tới z như là trỏ tới các nút ngoài, tất cả các quá trình tìm kiếm không thành công đều kết thúc ở các nút ngoài. Các nút không thành công có chứa các khoá được gọi là nút trong; khi đưa thêm khái niệm nút ngoài thì lẽ ra mỗi nút trong đều phải trỏ đến hai nút khác của cây nhưng về mặt cài đặt chúng ta cho tất cả các nút ngoài được biểu diễn chỉ bởi nút z duy nhất.

Chúng ta sẽ cùng xét ví dụ sau để thấy các liên kết này và các nút giả một cách rõ nhất:

Chúng ta cùng xét một ví dụ cụ thể sau:

Tìm khoá I trên cây trong hìnhtrên bằng cách dùng thủ tục timkiemcaynhiphan. Trước tiên I được so sánh với khoá A ở gốc. Vì I lớn hơn nên nó tiếp tục được so sánh với S, cứ tiếp tục như thế I sẽ được so sánh với E, R, và cuối cùng là H. Các liên kết trong nút chứa H trỏ tới z và quá trình tìm kiếm kết thúc: I được so sánh với chính nó ở trong z và tìm kiếm kết thúc không thành công.

Chèn nút trong cây nhị phân

Để chèn thêm một nút mới vào cây (giả sử chúng ta đã tìm kiếm nó không thành công, kế đến gặp nó trong nơi chứa z), chúng ta cần một thủ tục duy trì cha p của x trong khi duyệt cây từ gốc đến ngọn. Khi tiếp xúc với ngọn của cây (xảy ra khi x=z) p trỏ tới nút mà liên kết của nó phải được thay đổi để trỏ tới nút mới được chèn vào.

function chentrongcay(k: integer; x: lienket): lienket;

var p: lienket;

begin

Repeat p:=x;

if (k = x↑.khoa) then

x:= x↑.lelsex:= x↑.r;

untilx = z;

new(x);x↑.k:= v;

x↑.l: = z;x↑.r := z;

if (v= p↑.khoa)thenp↑.l:= x;else p↑.r:= x;

chentrongcay:=x;

End;

Một khóa k có thể được thêm vào cây bằng cách gọi hàm chentrongcay(k, dau). Hàm này trả về một liên kết tới nút mới được tạo sao cho thủ tục gọi có thể đặt các giá trị thích hợp vào trường infor

Khi chèn một nút mới có khoá bằng với một khoá nào đó đã có sẵn trong cây, nút mới sẽ được chèn vào bên phải của nút đã có sẵn. Tất cả các mẩu tin có khoá bằng với k có thể được xử lý bằng cách đặt liên tục t vào thủ tục timkiem(k, t) như chúng ta đã làm trong tìm kiếm tuần tự.

Ví dụ:

Thấy rằng cây này có được khi chèn các khoá ASEARCHI vào một cây trống đã được khởi động.

Chú ý: Vị trí các khoá bằng nhau trong cây - dù một cây có tới 3 khoá trùng nhau trải dài trong cây nhưng sẽ không có khoá nào “xen giữa” chúng.

Khi dùng các cây tìm kiếm nhị phân thì hàm sắp xếp (sapxep) sẽ tự động có được nhờ vào cấu trúc của cây. Nếu quan sát kỹ, các bạn sẽ thấy các khoá được sắp xếp theo thứ tự từ trái qua phải (không kể chiều cao và các liên kết). Vậy chúng ta có thể suy ra một phương pháp sắp xếp từ các tính chất của cây tìm kiếm nhị phân nhờ quá trình duyệt cây theo một thứ tự xác định.

procedure hiencay(x : lienket);

begin

if ( x<>z)then

Begin hiencay(x↑.l);

hiendiem(x);hiencay(x↑.r)

end;

End

Việc gọihàm hiencay(dau ↑.r) sẽ in ra các khóa của cây theo thứ tự. Quá trình này đưa ra một phương pháp sắp xếp tương tự như phương pháp Quicksort, trong đó nút gốc của cây đóng vai trò là phần tử phân hoạch của Quicksort.

- Dễ dàng thấy được sự khác nhau giữa hai phương pháp sắp xếp này là: phương pháp sắp xếp dựa vào cây tìm kiếm nhị phân phải dùng thêm nhiều bộ nhớ trợ giúp, trong phương pháp Quicksort dùng ít bộ nhớ trợ giúp.

- Về mặt thời gian chạy các thuật toán trên cây tìm kiếm nhị phân hoàn toàn phụ thuộc vào hình dạng của cây với khoảng lgN nút nằm giữa nút gốc và nút ngoài. Tất nhiên, điều chúng ta hi vọng có thời gian tìm kiếm trung bình xấp xỉ logarit, bởi vì phần tử đầu tiên được chèn vào sẽ là gốc của cây; nếu N khoá được chèn vào là ngẫu nhiên thì phần tử đầu tiên sẽ chia tập các khoá ra làm đôi (xét về mặt trung bình).

-Lý luận tương tự cho các cây con chúng ta thấy thuật toán có thời gian tìm kiếm trung bình xấp xỉ logarit.

VậyMộtthao tác tìm kiếm hay chèn trên một cây tìm kiếm nhị phân đòi hỏi trung bình 2lnN phép so sánh, trong đó cây đang xét được xây dựng từ N khoá ngẫu nhiên.

- Với mỗi nút trong cây, số các phép so sánh dùng cho thao tác tìm kiếm thành công là khoảng cách từ nút đó tới gốc. Tổng của khoảng cách này xét trên tất cả các nút được gọi là độ dài đường đi trong của cây. Chia độ dài đường đi trong cho N chúng ta có số trung bình của số lần so sánh cho trường hợp tìm kiếm thành công.

- Giả sử nếu CN ký hiệu độ dài đường đi trong của cây tìm kiếm nhị phân có N nút thì ta có công thức truy hồi:


Trong đó C1=1;

- Đối với trường hợp không thành công, các bạn có thể làm tương tự như vậy mặc dù có phức tạp thêm một chút.

- Khi một khoá mới được chèn vào thì đòi hỏi cùng số lần so sánh trung bình nhưng linh động hơn đối với tìm kiếm nhị phân. Tuy nhiên, nếu các khoá không được sắp xếp ngẫu nhiên thì thuật toán sẽ có thể hiện xấu hơn.

* Trong trường hợp xấu nhất - một thao tác tìm kiếm trên cây tìm kiếm nhị phân có N khoá có thể đòi hỏi N lần so sánh.

- Khi các khoá được chèn vào theo thứ tự (hay thứ tự ngược), phương pháp tìm kiếm trên cây nhị phân sẽ không tốt hơn phương pháp tuần tự mà ta đã xét ở kỳ trước. Không chỉ có thế, sẽ có nhiều trường hợp thoái hoá khác có thể dẫn đến trường hợp xấu như trên. Chúng ta có thể thấy rõ điều này trong ví dụ:một cây được tạo khi các khoá A Z B Y C X… được chèn theo thứ tự đó vào một cây trống đã được khởi tạo.

Trường hợp xấu này có thể khử được, và còn có thể làm cho tất cả các cây gần giống như cây trong trường hợp xấu nhất.

Xoá nút trên cây nhị phân

Sẽ không có trở ngại gì lớn khi cài đặt các hàm cơ bản TIMKIEM, CHÈN và SAPXEP.

Nhưng cây nhị phân sẽ cung cấp cho chúng ta một ví dụ rất tốt đáng bàn trong các thuật toán tìm kiếm đó là hàm XOA, hàm này không dễ dàng cài đặt như các hàm đã xét ở trên.

Ta thấy ngay sẽ xảy ra 3 trường hợp thường gặp sau:

- Nút (mà ta muốn xoá) không có con, chẳng hạn như L hay P, ta tỉa nó bằng cách liên kết cha nó với Null.

- Nút chỉ có một con, chẳng hạn như A, H, R thì di chuyển liên kết trong con tới liên kết cha thích hợp

- Nút có hai con và một trong 2 con của nó không có con, chẳng hạn như N, ta dùng nút không có con để thay cha của nó.

Vậy với những trường hợp khác đối với các nút ở tầng cao hơn, chẳng hạn như E, ta phải làm như thế nào?

Từ ví dụ trên, có thể thấy ngay một phương pháp để xoá E như sau: E được thay thế bởi nút kế tiếp mà ở tầng cao nhất (trường hợp này là H). Nút này đảm bảo điều kiện có nhiều nhất một con(bởi vì không cónút nào giữa nó và nút bị xoá nên liên kết trái của nó phải là Null), đến đây ta có thể xoá nó một cách dễ dàng. Để xoá E từ cây bên trái của hình trên, ta làm như sau:

+ Trỏ liên kết trái của R tới liên kết của H (trường hợp này là N),

+ Sao chép các liên kết từ nút E vào nút chứa H và trỏdau.rđến H.

Kết quả là cây bên phải của hình vẽ như trên.

Nếu như chương trình xét đầy đủ các trường hợp thì phức tạp hơn nhiều so với các chương trình đơn giản cho các thao tác tìm kiếm và chèn, nhưng nó rất có ích cho các thao tác trên cây phức tạp hơn. Thủ tục sau xoá nút trỏ tới bởi t từ cây có gốc là a. Do đó một nút với khoá k có thể được xoá bằng cách gọi:

XOANP(TIMKIEM(k, dau), dau).

Biến p được dùng để theo dõi cha của a trong cây và biến c được dùng để tìm thấy nút nối tiếp của nút bị xoá. Sau khi xoá xong, thì a là con của p. Việc giải phóng nút được trỏ tới bởi t sẽ giành cho chương trình gọi (hay có thể không giải phóng mà đưa nó vào cấu trúc dữ liệu khác).

procedure XOANP(t, a: lienket);

var p, c : lienket;

begin

Repeat p:= a;

if(t↑.keythen

a:= a↑.l;

Else a:= a↑.r;

until a= t;

if(t↑.r = z) then a:= a↑.l ;

Else if(t↑.r ↑.l = z ) then

Begin a:= a↑.r ;a↑.l:= t↑.l;

end;

Else Begin c:= a ↑.r;

while (c↑.l↑.l <> z)do

c:= c↑.l ;a:= c↑.l;

c↑l := a↑.r ; a↑.l:= t↑.l ;

a↑.r:= t↑.r ;

end;

if (t↑.key < p↑.key)then

p↑.l:= a;

else p↑.r :=a;

End;

Từ chương trình ta thấy rằng, trước tiên chương trình tìm cây bằng phương pháp thông thường để lấy vị trí của t trên cây. Kế đến chương trình kiểm tra ba trường hợp:

- Nếu t không có con thì con của p sau khi xoá sẽ là con của t.

- Nếu t có một con phải và con phải này không có con trái thì con phải này sẽ là con của p sau khi xoá và liên kết trái của nó sau khi xoá là liên kết trái của nó được sao chép từ t.

Trường hợp ngược lại a nhận giá trị của nút có khoá nhỏ nhất trong cây con bên phải của t, liên kết phải của nút đó được sao chép vào liên kết trái của cha nó, và cả hai liên kết của nó được nhận giá trị từ t.

Thuật toán trên dường như không đối xứng: chẳng hạn như tại sao không sử dụng khoá đi ngay trước khoá bị xoá thay vì sử dụng một khoá đi ngay sau khoá bị xoá? Nhiều sự hiệu chỉnh đa dạng của nó được đề nghị, nhưng sự khác nhau không đáng kể trong các ứng dụng thực tế. Thuật toán trên có thể dẫn đến một cây khá không cân bằng (chiều cao trung bình tương đương với sqrt(N) nếu gặp phải một số lớn các thao tác xoá, chèn ngẫu nhiên. )

Trong nhiều trường hợp thì việc xoá đòi hỏi sự cài đặt phức tạp hơn, do đó người ta cũng dùng một phương pháp xoá khác gọi là “xoá lười biếng” tức là: Khi một nút bị xoá thì bản thân nó vẫn còn nằm trong cấu trúc dữ liệu và bị đánh dấu xoá. Đối với phương pháp này lại phải chuẩn bị phương án xây dựng lại toàn bộ cấu trúc dữ liệu, bỏ đi (bỏ thực sự) những nút bị đánh dấu xoá khi cần thiết.

4. Cây tìm kiếm nhị phân gián tiếp

Như chúng ta đã thấy, đối với những ứng dụng chúng ta muốn có một cấu trúc tìm kiếm để có thể tìm được các mẩu tin một cách đơn giản mà không di chuyển chúng. Ví dụ: chúng ta có một mảng A[1..N] các mẩu tin với các khoá và muốn có thủ tục tìm kiếm để lấy được mẩu tin với một khoá đã cho trước nào đó. Đôi khi chúng ta muốn xoá mẩu tin với chỉ số cho trước từ cấu trúc tìm kiếm nhưng lại muốn giữ nó lại trong mảng để dùng trong mục đích khác

Để cài đặt các cây tìm kiếm nhị phân trong trường hợp như thế, chúng ta chỉ cần cho trường infor của các nút là chỉ số mảng. Kế đến chúng ta có thể loại bỏ trường khoá nhờ vào các chương trình tìm kiếm truy xuất các khoá trong các mẩu tin một cách gián tiếp, nghĩa là bởi một chỉ thị giống như:

if (vthen…

Tuy nhiên, tốt nhất là tạo một bản sao của khoá và làm tương tự như trên.

+ Ta dùng hàm: gtchen(k, infor: integer; x: lienket); tương tự như hàm chentrongcay, nhưng hàm gtchenđặt giá trị đã cho của đối số vào trong trường infor;

+ Hàm gtxoa(k, infor: integer ; x: lienket) dùng để xoá nút với khoá k chỉ số mảng infor từ các tìm kiếm nhị phân có gốc ở x cũng bắt chiếc cách cài đặt của hàm XOANP. Các hàm này dùng một bảng sao chép phụ của các khoá(một ở trong mảng, một ở trong cây) nhưng điều này cho phép cùng một hàm được dùng cho nhiều mảng hoặc nhiều trường hợp khoá trong cùng một mảng. Có nhiều phương pháp khác để làm được điều này: chẳng hạn như một thủ tục có thể được kết hợp mỗi cây được trích các khoá từ các mẩu tin.

+ Một phương pháp khác để có “sự gián tiếp” cho các cây tìm kiếm nhị phân là bỏ toàn bộ các cài đặt liên kết. Nghĩa là tất cả các liên kết trở thành các chỉ số của một mảng A[1..N] các mẩu tin, mỗi mẩu tin chứa trường khoa và các trường chỉ số mảng 1 và r. Các tham số liên kết chẳng hạn như:

if (k< x↑. khoa) then x:= x↑.lelse… sẽ trở thành tham chiếu mảng như:

if (v < A[x].khoa )thenx:= A[x].lelse

-Sẽ không có một lệnh gọi new trong trường hợp này vì cây tồn tại trong mảng các mẩu tin,

* new(dau) trở thành dau:= 0;

* new(z)trở thành z:= N+ 1;

- Để chèn nút M chúng ta sẽ

* Gửi M thay vì k tới hàm chencay ,

*Kế đó chỉ cần tham chiếu tới A[M].khoa thay vì tham chiếu tới k,

* Sau cùng thay thế dòng new(x) trong chencay bởi x:=M

Phương pháp cài đặt các cây tìm kiếm nhị phân này dùng để tìm kiếm trong các mảng lớn các mẩu tin rất ưa dùng trong nhiều ứng dụng bởi vì nó tránh sao chép các khoá như phương pháp vừa trình bày trước nó và cũng tránh dùng hàm cấp phát new nhiều lần. Khuyết điểm của phương pháp này là sự hao phí các khoảng bộ nhớ không dùng trong mảng các mẩu tin.

Một phương pháp thứ 3 là dùng các mảng song song như đã làm cho các xâu liên kết. Sự cài đặt của phương pháp này rất nặng, ngoài ra ba mảng được dùng: 1 mảng dùng cho các khoá, một mảng dùng cho liên kết trái, mảng còn lại dùng cho liên kết phải. Thuận lợi của phương pháp này là tính linh động của nó. Các mảng trợ giúp (thông tin trợ giúp kết hợp với mỗi nút) có thể được thêm vào mà không cần thay đổi các thao tác trên cây, hơn nữa khi các thủ tục tìm kiếm trả về chỉ số của một nút thì ta có thể truy suất trực tiếp đến tất cả các mảng.
URL của bài viết này::http://www.vnschool.net/modules.php?name=News&file=article&sid=2925
© Cong ty Cong Nghe Tin hoc Nha truong contact: sales@schoolnet.vn