42
Thuật toán Knuth–Morris–Pratt Thuật toán so khớp chuỗi Knuth–Morris–Pratt (hay thuật toán KMP) tìm kiếm sự xuất hiện của một "từ" W trong một "xâu văn bản" S bằng cách tiếp tục quá trình tìm kiếm khi không phù hợp, chính từ cho ta đầy đủ thông tin để xác định vị trí bắt đầu của kí tự so sánh tiếp theo, do đó bỏ qua quá trình kiểm tra lại các kí tự đã so sánh trước đó. Thuật toán được Donald Knuth , Vaughan Pratt J. H. Morris nghiên cứu độc lập năm 1977, nhưng họ công bố nó cùng nhau. Mục lục Thuật toán KMP Ví dụ cho thuật toán tìm kiếm Để minh họa chi tiết thuật toán, chúng ta sẽ tìm hiểu từng quá trình thực hiện của thuật toán. Ở mỗi thời điểm, thuật toán luôn được xác định bằng hai biến kiểu nguyên, m và i, được định nghĩa lần lượt là vị trí tương ứng trên S bắt đầu cho một phép so sánh với W, và chỉ số trên W xác định kí tự đang được so sánh. Khi bắt đầu, thuật toán được xác định như sau: m: 0 S: ABC ABCDAB ABCDABCDABDE W: ABCDABD i: 0

giai thuat doi sanh mau

  • Upload
    asataka

  • View
    620

  • Download
    4

Embed Size (px)

Citation preview

Page 1: giai thuat doi sanh mau

Thuật toán Knuth–Morris–Pratt

Thuật toán so khớp chuỗi Knuth–Morris–Pratt (hay thuật toán KMP) tìm kiếm sự xuất

hiện của một "từ" W trong một "xâu văn bản" S bằng cách tiếp tục quá trình tìm kiếm khi

không phù hợp, chính từ cho ta đầy đủ thông tin để xác định vị trí bắt đầu của kí tự so sánh

tiếp theo, do đó bỏ qua quá trình kiểm tra lại các kí tự đã so sánh trước đó.

Thuật toán được Donald Knuth, Vaughan Pratt và J. H. Morris nghiên cứu độc lập năm 1977, nhưng

họ công bố nó cùng nhau.

Mục lục

Thuật toán KMP

Ví dụ cho thuật toán tìm kiếm

Để minh họa chi tiết thuật toán, chúng ta sẽ tìm hiểu từng quá trình thực hiện của thuật toán.

Ở mỗi thời điểm, thuật toán luôn được xác định bằng hai biến kiểu nguyên, m và i, được định

nghĩa lần lượt là vị trí tương ứng trên S bắt đầu cho một phép so sánh với W, và chỉ số trên W

xác định kí tự đang được so sánh. Khi bắt đầu, thuật toán được xác định như sau:

m: 0

S: ABC ABCDAB ABCDABCDABDE

W: ABCDABD

i: 0

Chúng ta tiến hành so sánh các kí tự của W tương ứng với các kí tự của S, di chuyển lần lượt

sang các chữ cái tiếp theo nếu chúng giống nhau. S[0] và W[0] đều là ‘A’. Ta tăng i :

m: 0

S: ABC ABCDAB ABCDABCDABDE

W: ABCDABD

i: _1

S[1] và W[1] đều là ‘B’. Ta tiếp tục tăng i :

Page 2: giai thuat doi sanh mau

m: 0

S: ABC ABCDAB ABCDABCDABDE

W: ABCDABD

i: __2

S[2] và W[2] đều là ‘C’. Ta tăng i lên 3 :

m: 0

S: ABC ABCDAB ABCDABCDABDE

W: ABCDABD

i: ___3

Nhưng, trong bước thứ tư, ta thấy S[3] là một khoảng trống trong khi W[3] = 'D', không

phù hợp. Thay vì tiếp tục so sánh lại ở vị trí S[1], ta nhận thấy rằng không có kí tự 'A' xuất

hiện trong khoảng từ vị trí 0 đến vị trí 3 trên xâu S ngoài trừ vị trí 0; do đó, nhờ vào quá trình

so sánh các kí tự trước đó, chúng ta thấy rằng không có khả năng tìm thấy xâu dù có so sánh

lại. Vì vậy, chúng ta di chuyển đến kí tự tiếp theo, gán m = 4(m=m+1) và i = 0.

012345678910

m: ____4

S: ABC ABCDAB ABCDABCDABDE

W: ABCDABD

i: 0

Tiếp tục quá trình so sánh như trên, ta xác định được xâu chung "ABCDAB", với W[6]

(S[10]), ta lại thấy không phù hợp. Nhưng từ kết quả của quá trình so sánh trước, ta đã

duyệt qua "AB", có khả năng sẽ là khởi đầu cho một đoạn xâu khớp, vì vậy ta bắt đầu so

sanh từ vị trí này. Như chúng ta đã thấy các kí tự này đã trùng khớp với hai kí tự trong phép

so khớp trước, chúng ta không cần kiểm tra lại chúng một lần nữa; ta bắt đầu với m = 8, i

= 2 và tiếp tục quá trình so khớp.

m: ________8

Page 3: giai thuat doi sanh mau

S: ABC ABCDAB ABCDABCDABDE

W: ABCDABD

i: __2

Quá trình so khớp ngay lập tức thất bại, nhưng trong W không xuất hiện kí tự ‘ ‘,vì vậy, ta

tăng m lên 11, và gán i = 0.

m: ___________11

S: ABC ABCDAB ABCDABCDABDE

W: ABCDABD

i: 0

Một lần nữa, hai xâu trùng khớp đoạn kí tự "ABCDAB" nhưng ở kí tự tiếp theo, 'C', không

trùng với 'D' trong W. Giống như trước, ta gán m = 15, và gán i = 2, và tiếp tục so sánh.

m: _______________15

S: ABC ABCDAB ABCDABCDABDE

W: ABCDABD

i: __2

Lần này, chúng ta đã tìm được khớp tương ứng với vị trí bắt đầu là S[15].

Thuật toán và mã giả của thuật toán tìm kiếm

Bây giờ, chúng ta tìm hiểu về sự tồn tại của bảng "so khớp một phần"(partial match) T, được

mô tả bên dưới, giúp ta xác định được vị trí tiếp theo để so khớp khi phép so khớp trước đã

thất bại. Mảng T được tổ chức để nếu chúng ta có một phép so khớp bắt đầu từ S[m] thất bại

khi so sánh S[m + i] với W[i], thì vị trí của phép so khớp tiếp theo có chỉ số là m + i

- T[i] trong S ( T[i] là đại lượng xác định số ô cần lùi khi có một phép so khớp thất bại).

Mặc dù phép so khớp tiếp theo sẽ bắt đầu ở chỉ số m + i - T[i], giống như ví dụ ở trên,

chúng ta không cần so sánh các kí tự T[i] sau nó, vì vậy chúng ta chỉ cần tiếp tục so sánh từ

kí tự W[T[i]]. Ta có T[0] = -1, cho thấy rằng nếu W[0] không khớp, ta không phải lùi

lại mà tiếp tục phép so sánh mới ở kí tự tiếp theo. Sau đây là đoạn mã giả mẫu của thuật toán

tìm kiếm KMP.

Page 4: giai thuat doi sanh mau

algorithm kmp_search:

input:

mảng kí tự, S (đoạn văn bản)

mảng kí tự, W (xâu đang tìm)

output:

một biến kiểu nguyên ( vị trí (bắt đầu từ 0) trên S mà

W được tìm thấy)

define variables:

biến nguyên, m ← 0

biến nguyên, i ← 0

mảng nguyên, T

while m + i nhỏ hơn độ dài của sâu S, do:

if W[i] = S[m + i],

let i ← i + 1

if i bằng độ dài W,

return m

Otherwise,

let m ← m + i - T[i],

if T[i] lớn hơn -1,

let i ← T[i]

else

let i ← 0

return độ dài của đoạn văn bản S

Độ phức tạp của thuật toán tìm kiếm

Với sự xuất hiện của mảng T, phần tìm kiếm của thuật toán Knuth–Morris–Pratt có độ phức

tạp O(k), trong đó k là độ dài của xâu S. Ngoại trừ các thủ tục nhập xuất hàm ban đầu, tất cả

các phép toán đều được thực hiên trong vòng lặp while, chúng ta sẽ tính số câu lệnh được

thực hiện trong vòng lặp; để làm được việc này ta cần phải tìm hiểu về bản chất của mảng T.

Page 5: giai thuat doi sanh mau

Theo định nghĩa, mảng được tạo để: nếu một phép so khớp bắt đầu ở vị trí S[m] thất bại khi

so sánh S[m + i] với W[i], thì phép so khớp có thể thành công tiếp theo sẽ bắt đầu ở vị

trí S[m + (i - T[i])]. Cụ thể hơn, phép so khớp tiếp theo sẽ bắt đầu tại vị trí có chỉ số

cao hơn m, vì vậy T[i] < i.

Từ điều này, chúng ta thấy rằng vòng lặp có thế thực hiện 2k lần. Với mỗi lần lặp, nó thực

hiện một trong hai nhánh của vòng lặp. Nhánh thứ nhất tăng i và không thay đổi m, vì vậy

chỉ số m + i của kí tự đang so sánh trên S tăng lên. Nhánh thứ hai cộng thêm i - T[i]

vào m, và như chúng ta đã biết, đây luôn là số dương. Vì vậy, vị trí m, vị trí bắt đầu của một

phép so khớp tiềm năng tăng lên. Vòng lặp dừng nếu m + i = k; vì vậy mỗi nhánh của

vòng lặp có thể được sử dụng trong tối đa k lần, do chúng lần lượt tăng giá trị của m + i

hoặc m, và m ≤ m + i: nếu m = k, thì m + i ≥ k, vì vậy: do các phép toán chủ yếu

tăng theo đơn vị, chúng ta đã có m + i = k vào một thời điểm nào đó trước, và vì vậy

thuật toán dừng.

Do đó vòng lặp chủ yếu thực hiện 2k lần, độ phức tạp tính toán của thuật toán tìm kiếm chỉ là

O(k).

Bảng so sánh một phần ("Partial match")

Mục đích của bảng là cho phép thuật toán so sánh mỗi kí tự của S không quá một lần. Sự

quan sát chìa khóa về bản chất của phương pháp tìm kiếm tuyến tính cho phép điều này xảy

ra là trong quá trình so sánh các đoạn của chuỗi chính với đoạn mở đầu của mẫu, chúng ta

biết chính xác được những vị trí mà đoạn mẫu có thế xuất hiện trước vị trí hiện tại. Nói cách

khác, chúng ta “tự tìm kiếm” đoạn mẫu trước và đưa ra một danh sách các vị trí trước đó mà

bỏ quá tối đa các kí tự vô vọng mà vẫn không mất đi các đoạn tiềm năng.

Chúng ra muốn tìm kiếm, với mỗi vị trí trên W, độ dài của đoạn dài nhất giống với “đoạn bắt

đầu” trên W tính đến (không bao gồm) vị trí đó, đây là khoảng cách chúng ra có thể lùi lại để

tiếp tục so khớp. Do vậy T[i] là giá trị của độ dài đoạn dài nhất kết thúc bởi phần tử W[i -

1]. Chúng ta sử dụng quy ước rằng một chuỗi rỗng có độ dài là 0. Với trường hợp không

trùng với mẫu ngay ở giá trị đầu tiên (không có khả năng lùi lại), ta gán T[0] = -1.

Ví dụ cho thuật toán xây dựng bảng

Page 6: giai thuat doi sanh mau

Ta xét xâu W = "ABCDABD". Ta sẽ thấy thuật toán xây dựng bảng có nhiều nét tương đồng

với thuật toán tím kiếm chính. Ta gán T[0] = -1. Để tính T[1], ta cần tìm ra một xâu con

"A" đồng thời cũng là xâu con bắt đầu của W. Vì vậy ta gán T[1] = 0. Tương tự , T[2] =

0 và T[3] = 0.

Ta xét đến kí tự W[4], 'A'. Dễ thấy kí tự này trùng với kí từ bắt đầu xâu W[0]. Nhưng do

T[i] là độ dài xâu dài nhất trùng với xâu con bắt đầu trong W tính đến W[i – 1] nên

T[4] = 0 và T[5] = 1. Tương tự, kí tự W[5] trùng với kí tự

W[1] nên T[6] = 2.

Vì vậy ta có bảng sau:

i 0 1 2 3 4 5 6

W[i] A B C D A B D

T[i] -1 0 0 0 0 1 2

Một ví dụ khác phức tạp hơn

i 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3

W[i] P A R T I C I P A T E I N P A R A C H U T E

T[i] -1 0 0 0 0 0 0 0 1 2 0 0 0 0 0 0 1 2 3 0 0 0 0 0

Mã giả của thuật toán tạo bảng

Ví dụ ở trên đã mô tả kỹ thuật tổng quát để tạo bảng.

Dưới đây là đoạn mã giả

algorithm kmp_table:

input:

mảng kí tự, W

Page 7: giai thuat doi sanh mau

mảng số nguyên, T

output:

mảng T

define variables:

biến kiểu nguyên, pos ← 2

biến kiểu nguyên, cnd ← 0

let T[0] ← -1, T[1] ← 0

while pos nhỏ hơn độ dài của W, ‘’’do’’’:

(trường hợp một: tiếp tục dãy con)

if W[pos - 1] = W[cnd],

let T[pos] ← cnd + 1, pos ← pos + 1, cnd ← cnd + 1

(trường hợp hai: không thỏa mãn, nhưng ta có thể quay

ngược trở lại)

otherwise, if cnd > 0, let cnd ← T[cnd]

(trường hợp ba: hết phần tử. Chú ý rằng cnd = 0)

otherwise, let T[pos] ← 0, pos ← pos + 1

Độ phức tạp của thuật toán tạo bảng

Độ phức tạp của thuật toán tạo bảng là O(n), với n là độ dài của W. Ngoại trừ một số sắp xếp

ban đầu, toàn bộ công việc được thực hiên trong vòng lặp while, độ phức tạp của toàn bộ

vòng lặp là O(n), với việc cùng lúc sử lý giá trị của pos và pos - cnd. Trong trường hợp

thứ nhất, pos - cnd không thay đổi, khi cả pos và cnd cùng tăng lên một đơn vị. Ở

trường hợp hai, cnd được thay thế bởi T[cnd], như chúng ta đã biết ở trên, luôn luôn nhỏ

hơn cnd, do đó tăng giá trị của pos - cnd. Trong trường hợp thứ ba, pos tăng và cnd thì

không, nên cả giá trị của pos và pos - cnd đều tăng. Mà pos ≥ pos - cnd, điều này

có nghĩa là ở mỗi bước hoặc pos hoặc chặn dưới pos đều tăng; mà thuật toán kết thúc khi

pos = n, nên nó phải kết thúc tối đa sau 2n vòng lặp, do pos - cnd bắt đầu với giá trị

1. Vì vậy độ phức tạp của thuật toán xây dựng bảng là O(n).

Page 8: giai thuat doi sanh mau

Độ phức tạp của thuật toán KMP

Do độ phức tạp của hai phần trong thuật toán lần lượt là O(k) và O(n), nên độ phức tạp của

cả thuật toán là O(n + k).

Như đã thấy trong ví dụ ở trên, thuật toán mạnh hơn các thuật toán so khớp chuỗi kém hơn vì

nó có thể bỏ qua các kí tự đã duyệt. Ít ô phải quay trở lại hơn, thuật toán sẽ nhanh hơn, và

được thể hiện trong bảng T bởi sự hiện diện của các số không. Một từ như "ABCDEFG" sẽ

làm tốt với thuật toán này vì nó không có sự lặp lại của những chữ bắt đầu, vì vây mảng đơn

giản chỉ toàn số không với -1 ở đầu. Ngược lại, với từ W = "AAAAAAA" nó hoạt động tồi tệ, bởi

vì bảng sẽ là

i 0 1 2 3 4 5 6

W[i] A A A A A A A

T[i] -1 0 1 2 3 4 5

Đây là mẫu xấu nhất cho mảng T, và nó có thể dùng để so sánh với đoạn như S =

"AAAAAABAAAAAABAAAAAAA", trong trường hợp này thuật toán sẽ cố gắng ghép tất cả

các chữ 'A' với 'B' trước khi dừng lại; kết quả là số lượng tối đa câu lệnh được sử dụng, tiến

tới trên hai lần số kí tự của xâu S khi số lần lặp của "AAAAAAB" tăng. Mặc dù quá trình xây

dựng bảng rất nhanh so với chữ này (nhưng vô tác dụng), quá trình này chạy có một lần với

chữ W, trong khi quá trình tìm kiếm chạy rất nhiều lần. Nếu với mỗi lần, từ W được dùng để

tìm trên xâu như xâu S, độ phức tạp tổng thể sẽ rất lớn. Bằng cách so sách, sự kết hợp này là

trường hợp tốt nhất với thuật toán so khớp chuỗi Boyer-Moore.

Lưu ý rằng trong thực tế, thuật toán KMP làm việc không tốt đối với tìm kiếm trong văn bản

ngôn ngữ tự nhiên, bởi vì nó chỉ có thể bỏ qua các ký tự khi phần đầu của từ giống với một

phần trong văn bản. Trong thực tế điều này chỉ đôi khi xảy ra trong các văn bản ngôn ngữ tự

nhiên. Ví dụ, hãy xem xét bao nhiêu lần một xâu "text" xuất hiện trong đoạn văn này.

Thuật toán Rabin-Karp là một trong những phương pháp tìm kiếm chuỗi. Ý tưởng là chúng ta sẽ khai thác một vùng

nhớ lớn bằng cách xem mỗi đoạn M-ký tự có thể có của văn bản như là một khoá (key) trong một bảng băm chuẩn.

Nhưng không cần thiết phải giữ một bảng băm tổng thể, vì bài toán được cài đặt sao cho chỉ một khoá là đang được

tìm kiếm; việc mà ta cần làm là đi tính hàm băm cho M ký tự từ văn bản vì nó chỉ đơn giản là kiểm tra xem chúng có

bằng với mẫu hay không. Với hàm băm: h(k) = k mod q, ở đây q (kích thước bảng) là một số nguyên tố lớn. Trong

Page 9: giai thuat doi sanh mau

trường hợp này, không có gì được chứa trong bảng băm, vì vậy q có thể được cho giá trị rất lớn.

Phương pháp này dựa trên việc tính hàm băm cho vị trí i trong văn bản, cho trước giá trị tại ví trí i-1 của nó, và suy ra

hoàn toàn trực tiếp từ công thức toán học. Giả sử rằng ta dịch M ký tự thành số bằng cách nén chúng lại với nhau

trong một từ (word) của máy, mà ta xem như một số nguyên. Điều này ứng với việc ghi các ký tự như các con số trong

một hệ thóng cơ số d, ở đây d là số ký tự có thể có. Vì vậy số ứng với a[i..i+M-1] là

x = a[i]dM-1 + a[i+1]dM-2 + …+ a[i+M-1]

Và có thể giả sử rằng ta biết giá trị của h(x) = x mod q. Nhưng dịch (shift) một vị trí sang phải trong văn bản tương ứng

với việc thay x bởi (x - a[i]dM-1)d + a[i+M].

Một tính chất cơ bản của phép toán mod là ta có thể thực hiện nó bất kỳ lúc nào trong các phép toán này và vẫn nhận

được cùng câu trả lời. Cách khác, nếu ta lấy phần dư khi chia cho q sau mỗi một phép toán số học (để giữ cho các số

mà ta đang gặp là nhỏ), thì ta sẽ nhận được cùng câu trả lời như thể ta đã thực hiện tất cả các phép toán học, sau đó

lầy phần dư khi chia cho q.

Điều này dẫn tới một thuật toán đối sánh mẫu rất đơn giản được cài đặt dưới đây:

Code:

fuction rksearch: integer;

const q=33253586; d = 32;

var h1, h2, dM, i: integer;

begin

dM:= 1;

for i:=1 to M-1 do dM:= (d*dM) mod q;

h1: = 0;

Page 10: giai thuat doi sanh mau

for i: = 1 to M do h1:= (h1*d+index(p[i])) mod q;

h2: = 0;

for i:= 1 to M do h2:= (h2*d + index (a[i])) mod q;

i: = 1;

while (h1 <> h2) and(i<=N-M) do

begin

h2:= (h2+d*q-index(a[i])*dM) mod q;

h2:= (h2*d+index(a[i+M])) mod q;

i:= i+1;

end;

rksearch:= i;

end;

Chương trình giả định dùng hàm index (function index(c: char): integer; hàm trả về 0 đối với

các khoảng trắng và i đối với ký tự thứ i của bảng chữ cái) nhưng d = 32 để cho hiệu quả (các

phép nhân có thể được càiđặt như các phép dịch bit).

Đầu tiên chương trình tính giá trị h1 cho mẫu, sau đó tới giá trị h2 cho M ký tự đầu tiêncảu

văn bản (nó cũng tính giá trị của dM-1mod q trong biến dM). Sau đó nó tiến hành công việc

qua chuỗi văn bản, dùng đến kỹ thuật ở trên để tính hàm băm cho M ký tự với h1. Số nguyên

tố q được chọn càng lớn càng tốt, nhưng đủ nhỏ sao cho (d+1)*q không gây ra tràn

(overflow): điều này cần ít phép mod hơn nếu ta dùng số nguyên tố lớn nhất biểu diễn được

Page 11: giai thuat doi sanh mau

(một giá trị d*q phụ trợ được cộng thêm vào trong khi tính h2 để bảo đảm rằng mọi đại lượng

vẫn còn là dương để cho phép toán mod có thể thực hiện được).

*** Phép đối sánh mẫu Rabin-Karp gần như là tuyến tính.

Thuật toán này hiển nhiên thực hiện theo thời gian tỉ lệ với M+N, nhưng chú ý là nó chỉ thực

sự đi tìm một vị trí trong văn bản có cùgn giá trị băm với mẫu. Để cho chắc chắn, ta nên thực

sự tiến hành so sánh trực tiếp văn bản đó với mẫu. Tuy nhiên, việc sử dụng giá trị rất lớn của

q, được biến thành dương bởi các phép toán mod và bởi sự kiện làta không cần duy trì bảng

băm thực sự, đã khiến cho rất khó xảy ra một sự đụng độ. Về mặt lý thuyết, thuật toán này có

thể vẫn thực hiện theo O(NM) bước trong trường hợp xấu nhất ( không đáng tin cậy), nhưng

trong thực tế có thể dựa vào thuật toán để thực hiện khoảng N+M bước.

Thu t toán tìm ki m chu iậ ế ỗ

Trong bài viết này chúng ta sẽ quan tâm đến việc tìm kiếm tất cả các vị trí xuất hiện của mẫu

trên một văn bản. Cài đặt sẽ dùng một hàm ra : Output để thông báo vị trí tìm thấy mẫu. I.

Mở đầu Dữ liệu trong máy tính được lưu trữ dưới rất nhiều dạng khác...

Trong bài viết này chúng ta sẽ quan tâm đến việc tìm kiếm tất cả các vị trí xuất hiện của mẫu

trên một văn bản. Cài đặt sẽ dùng một hàm ra : Output để thông báo vị trí tìm thấy mẫu.

I. Mở đầu

Dữ liệu trong máy tính được lưu trữ dưới rất nhiều dạng khác nhau, nhưng sử dụng chuỗi

vẫn là một trong những cách rất phổ biến. Trên chuỗi các đơn vị dữ liệu không có ý nghĩa

quan trọng bằng cách sắp xếp của chúng. Ta có thể thấy các dạng khác nhau của chuỗi như ở

các file dữ liệu, trên biểu diễn của các gen, hay chính văn bản chúng ta đang đọc.

Một phép toán cơ bản trên chuỗi là đối sánh mẫu (pattern matching), bài toán yêu cầu ta

tìm ra một hoặc nhiều vị trí xuất hiện của mẫu trên một văn bản.. Trong đó mẫu và văn bản

là các chuỗi có độ dài N và M (M ≤ N), tập các ký tự được dùng gọi là bảng chữ cái , có số

lượng là .

Page 12: giai thuat doi sanh mau

Việc đối sánh mẫu diễn ra với nhiều lần thử trên các đoạn khác nhau của văn bản. Trong đó

cửa sổ là một chuỗi M ký tự liên tiếp trên văn bản. Mỗi lần thử chương trình sẽ kiểm tra sự

giống nhau giữa mẫu với cửa sổ hiện thời. Tùy theo kết quả kiểm tra cửa sổ sẽ được dịch đi

sang phải trên văn bản cho lần thử tiếp theo.

Trong trình bày này chúng ta sẽ quan tâm đến việc tìm kiếm tất cả các vị trí xuất hiện của

mẫu trên một văn bản. Cài đặt sẽ dùng một hàm ra : Output để thông báo vị trí tìm thấy

mẫu.

II. Thuật toán Brute Force

Có lẽ cái tên của thuật toán này đã nói lên tất cả (brute nghĩa là xúc vật, force nghĩa là sức

mạnh). Thuật toán brute force thử kiểm tra tất cả các vị trí trên văn bản từ 1 cho đến n-m+1.

Sau mỗi lần thử thuật toán brute force dịch mẫu sang phải một ký tự cho đến khi kiểm tra

hết văn bản.

Thuật toán brute force không cần công việc chuẩn bị cũng như các mảng phụ cho quá trình

tìm kiếm. Độ phức tạp tính toán của thuật toán này là O(n*m)

function IsMatch(const X: string; m: integer;

                 const Y: string; p: integer):

boolean;

var

  i: integer;

begin

  IsMatch := false;

  Dec(p);

  for i := 1 to m do

    if X <> Y[p + i] then Exit;

  IsMatch := true;

end;

Page 13: giai thuat doi sanh mau

procedure BF(const X: string; m: integer;

             const Y: string; n: integer);

var

  i: integer;

begin

  for i := 1 to n - m + 1 do

    if IsMatch(X, m, Y, i) then

      Output(i);  { Thông báo tìm thấy mẫu tại vị trí i của văn bản }

end;

III. Thuật toán Knuth-Morris-Pratt

Thuật toán Knuth-Morris-Pratt là thuật toán có độ phức tạp tuyến tính đầu tiên được phát

hiện ra, nó dựa trên thuật toán brute force với ý tưởng lợi dụng lại những thông tin của lần

thử trước cho lần sau. Trong thuật toán brute force vì chỉ dịch cửa sổ đi một ký tự nên có

đến m-1 ký tự của cửa sổ mới là những ký tự của cửa sổ vừa xét. Trong đó có thể có rất

nhiều ký tự đã được so sánh giống với mẫu và bây giờ lại nằm trên cửa sổ mới nhưng được

dịch đi về vị trí so sánh với mẫu. Việc xử lý những ký tự này có thể được tính toán trước rồi

lưu lại kết quả. Nhờ đó lần thử sau có thể dịch đi được nhiều hơn một ký tự, và giảm số ký

tự phải so sánh lại.

Xét lần thử tại vị trí j, khi đó cửa sổ đang xét bao gồm các ký tự y[j…j+m-1] giả sử sự

khác biệt đầu tiên xảy ra giữa hai ký tự x và y[j+i-1].

Khi đó x[1…i]=y[j…i+j-1]=u và a=x[i]y[i+j]=b. Với trường hợp này, dịch cửa

sổ phải thỏa mãn v là phần đầu của xâu x khớp với phần đuôi của xâu u trên văn bản. Hơn

nữa ký tự c ở ngay sau v trên mẫu phải khác với ký tự a. Trong những đoạn như v thoả

mãn các tính chất trên ta chỉ quan tâm đến đoạn có độ dài lớn nhất.

SHAPE \* MERGEFORMAT

u

Page 14: giai thuat doi sanh mau

u

v

b

c

a

x

y

x

j

i+j-1

Dịch cửa sổ sao cho v phải khớp với u và c a

Thuật toán Knuth-Morris-Pratt sử dụng mảng Next để lưu trữ độ dài lớn nhất của xâu v

trong trường hợp xâu u=x[1…i-1]. Mảng này có thể tính trước với chi phí về thời gian là

O(m) (việc tính mảng Next thực chất là một bài toán qui hoạch động một chiều).

Thuật toán Knuth-Morris-Pratt có chi phí về thời gian là O(m+n) với nhiều nhất là 2n-1 lần số lần so

sánh ký tự trong quá trình tìm kiếm.

procedure preKMP(const X: string; m: integer;

                 var Next: array of integer);

var

  i, j: integer;

begin

  i := 1;

  j := 0;

  Next[1] := 0;

  while (i <= m) do

  begin

    while (j > 0)and(X <> X[j]) do j := Next[j];

Page 15: giai thuat doi sanh mau

    Inc(i);

    Inc(j);

    if X[i] = X[j] then Next[i] := Next[j] {v khớp

với u và ca}

    else Next[i] := j;

  end;

end;

 

procedure KMP(const X: string; m: integer;

              const Y: string; n: integer);

var

  i, j: integer;

  Next: ^TIntArr; { TIntArr = array[0..maxM] of

integer }

begin

  GetMem(Next, (m + 1)*SizeOf(Integer));

  preKMP(X, m, Next^);

  i := 1;

  j := 1;

  while (j <= n) do

  begin

    {dịch đi nếu không khớp}

    while (i > 0)and(X[i] <> Y[j]) do i := Next^ ;

    Inc(i);

    Inc(j);

    if i > m then

    begin

      Output(j - i + 1);

      i := Next^ ;

    end;

  end;

  FreeMem(Next, (m + 1)*SizeOf(Integer));

Page 16: giai thuat doi sanh mau

End;

 

IV. Thuật toán Deterministic Finite Automaton (máy automat hữu hạn)

Trong thuật toán này, quá trình tìm kiếm được đưa về một quá trình biến đổi trạng thái

automat. Hệ thống automat trong thuật toán DFA sẽ được xây dựng dựa trên xâu mẫu. Mỗi

trạng thái (nút) của automat lúc sẽ đại diện cho số ký tự đang khớp của mẫu với văn bản.

Các ký tự của văn bản sẽ làm thay đổi các trạng thái. Và khi đạt được trạng cuối cùng có

nghĩa là đã tìm được một vị trí xuất hiện ở mẫu.

Thuật toán này có phần giống thuật toán Knuth-Morris-Pratt trong việc nhảy về trạng thái

trước khi gặp một ký tự không khớp, nhưng thuật toán DFA có sự đánh giá chính xác hơn vì

việc xác định vị trí nhảy về dựa trên ký tự không khớp của văn bản (trong khi thuật toán

KMP lùi về chỉ dựa trên vị trí không khớp).

Với xâu mẫu là GCAGAGAG ta có hệ automat sau

SHAPE \* MERGEFORMAT

0

2

1

3

4

5

6

7

8

G

G

G

G

Page 17: giai thuat doi sanh mau

G

C

C

C

G

C

A

G

A

G

A

G

Với ví dụ ở hình trên ta có:

Nếu đang ở trạng thái 2 gặp ký tự A trên văn bản sẽ chuyển sang trạng thái 3

Nếu đang ở trạng thái 6 gặp ký tự C trên văn bản sẽ chuyển sang trạng thái 2

Trạng thái 8 là trạng thái cuối cùng, nếu đạt được trạng thái này có nghĩa là đã tìm

thất một xuất hiện của mẫu trên văn bản

Trạng thái 0 là trạng thái mặc định(các liên kết không được biểu thị đều chỉ về trạng

thái này), ví dụ ở nút 5 nếu gặp bất kỳ ký tự nào khác G thì đều chuyển về trạng thái 0

Việc xây dựng hệ automat khá đơn giản khi được cài đặt trên ma trận kề. Khi đó thuật toán

có thời gian xử lý là O(n) và thời gian và bộ nhớ để tạo ra hệ automat là O(m*) (tùy cách cài

đặt)

Nhưng ta nhận thấy rằng trong DFA chỉ có nhiều nhất m cung thuật và m cung nghịch, vì vậy

việc lưu trữ các cung không cần thiết phải lưu trên ma trận kề mà có thể dùng cấu trúc danh

sách kề Forward Star để lưu trữ. Như vậy thời gian chuẩn bị và lượng bộ nhớ chỉ là O(m).

Tuy nhiên thời gian tìm kiếm có thể tăng lên một chút so với cách lưu ma trận kề.

Page 18: giai thuat doi sanh mau

Cài đặt dưới đây xin được dùng cách đơn giản(ma trận kề)

type

  TAut = array[0..maxM, 0..maxd] of integer;

 

procedure preAUT(const X: string; m: integer; var G:

TAut);

var

  i, j, prefix, cur, c, newState: integer;

begin

  FillChar(G, SizeOf(G), 0);

  cur := 0;

  for i := 1 to m do

  begin

    prefix := G[cur, Ord(X )]; {x[1..prefix]=x[i-

prefix+1..i]}

    newState := i;

    G[cur, Ord(X )] := newState;

    for c := 0 to maxd do  {copy prefix ->

newState }

      G[newState, c] := G[prefix, c];

    cur := newState;

  end;

end;

 

procedure AUT(const X: string; m: integer;

              const Y: string; n: integer);

var

  G: ^TAut;

  state, i: integer;

begin

  New(G);

Page 19: giai thuat doi sanh mau

  preAUT(X, m, G^);

  state := 0;

  for i := 1 to n do

  begin

    state := G^[state, Ord(Y )]; {chuyển trạng thái}

    if state = m then Output(i - m + 1);

  end;

  Dispose(G);

end;

IV. Thuật toán Boyer-Moore

Thuật toán Boyer Moore là thuật toán có tìm kiếm chuỗi rất có hiệu quả trong thực tiễn, các

dạng khác nhau của thuật toán này thường được cài đặt trong các chương trình soạn thảo

văn bản.

Khác với thuật toán Knuth-Morris-Pratt (KMP), thuật toán Boyer-Moore kiểm tra các ký tự

của mẫu từ phải sang trái và khi phát hiện sự khác nhau đầu tiên thuật toán sẽ tiến hành

dịch cửa sổ đi Trong thuật toán này có hai cách dịch của sổ:

Cách thứ 1: gần giống như cách dịch trong thuật toán KMP, dịch sao cho những phần đã so

sánh trong lần trước khớp với những phần giống nó trong lần sau.

Trong lần thử tại vị trí j, khi so sánh đến ký tự i trên mẫu thì phát hiện ra sự khác nhau, lúc

đó x[i+1…m]=y[i+j...j+m-1]=u và a=xy[i+j-1]=b khi đó thuật toán sẽ dịch

cửa sổ sao cho đoạn u=y[i+j…j+m-1] giống với một đoạn mới trên mẫu (trong các phép

dịch ta chọn phép dịch nhỏ nhất)

SHAPE \* MERGEFORMAT

u

b

c

a

Page 20: giai thuat doi sanh mau

x

y

x

u

dịch

u

Dịch sao cho u xuất hiện lại và c a

Nếu không có một đoạn nguyên vẹn của u xuất hiện lại trong x, ta sẽ chọn sao cho phần đôi

dài nhất của u xuất hiện trở lại ở đầu mẫu.

SHAPE \* MERGEFORMAT

u

b

a

y

x

dịch

u

u

x

Dịch để một phần đôi của u xuất hiện lại trên x

Cách thứ 2: Coi ký tự đầu tiên không khớp trên văn bản là b=y[i+j-1] ta sẽ dịch sao cho

có một ký tự giống b trên xâu mẫu khớp vào vị trí đó (nếu có nhiều vị trí xuất hiện b trên

xâu mẫu ta chọn vị trí phải nhất)

Page 21: giai thuat doi sanh mau

SHAPE \* MERGEFORMAT

u

b

a

y

x

dịch

u

b

x

không chứa b

Dịch để ký tự b ăn khớp với văn bản.

Nếu không có ký tự b nào xuất hiện trên mẫu ta sẽ dịch cửa sổ sao cho ký tự trái nhất của

cửa sổ vào vị trí ngay sau ký tự y[i+j-1]=b để đảm bảo sự ăn khớp

SHAPE \* MERGEFORMAT

u

b

a

y

x

dịch

u

x

không chứa b

Dịch khi b không xuất hiện trong x

Page 22: giai thuat doi sanh mau

Trong hai cách dịch thuật toán sẽ chọn cách dịch có lợi nhất.

Trong cài đặt ta dùng mảng bmGs để lưu cách dịch 1, mảng bmBc để lưu phép dịch thứ 2(ký

tự không khớp). Việc tính toán mảng bmBc thực sự không có gì nhiều để bàn. Nhưng việc

tính trước mảng bmGs khá phức tạp, ta không tính trực tiếp mảng này mà tính gián tiếp

thông qua mảng suff. Có suff =max{k | x[i-k+1…i]=x[m-k+1…m]}

Các mảng bmGs và bmBc có thể được tính toán trước trong thời gian tỉ lệ với O(m+). Thời

gian tìm kiếm (độ phức tạp tính toán) của thuật toán Boyer-Moore là O(m*n). Tuy nhiên với

những bản chữ cái lớn thuật toán thực hiện rất nhanh. Trong trường hợp tốt chi phí thuật

toán có thể xuống đến O(n/m) là chi phí thấp nhất của các thuật toán tìm kiếm hiện đại có thể đạt được.

procedure preBmBc(const X: string; m: integer;

                  var bmBc: array of integer);

var

  i: integer;

begin

  for i := 0 to maxd - 1 do bmBc := m;

  for i := 1 to m - 1 do bmBc[Ord(X )] := m - i;

end;

 

procedure suffixes(const X: string; m: integer;

                   var suff: array of integer);

var

  right, left, i: integer;

begin

  suff[m] := m;

  left := m;

  for i := m - 1 downto 1 do

    if (i > left)and(suff[i + m - right] < i - left)

then

      suff := suff[i + m - right]

Page 23: giai thuat doi sanh mau

    else

    begin

      if (i < left) then left := i;

      right := i;

      while (left >= 1)and(X[left] = X[left + m -

right]) do

        Dec(left);

      suff := right - left; {X[left…right] = X[m+left-right…

m]}     

    end;

end;

 procedure preBmGs(const X: string; m: integer;

                  var bmGs: array of integer);

var

  i, j: integer;

  suff: ^TIntArr;

begin

  GetMem(suff, (m + 1)*SizeOf(Integer));

  suffixes(X, m, suff^);  {Tính mảng suff}

  for i := 1 to m do bmGs := m;

  j := 0;

  for i := m downto 0 do

    if (i = 0)or(suff^ = i) then

      while (j < m - i) do

      begin

        {Nếu bmGs[j] chưa có giá trị thì điền vào}

        if bmGs[j] = m then bmGs[j] := m - i;

        Inc(j);

      end;

  for i := 1 to m - 1 do bmGs[m - suff^ ] := m - i;

{đảo lại}

  FreeMem(suff, (m + 1)*SizeOf(Integer));

Page 24: giai thuat doi sanh mau

end;

 

procedure BM(const X: string; m: integer;

             const Y: string; n: integer);

var

  i, j: integer;

  bmBc, bmGs: ^TIntArr;

begin

  GetMem(bmBc, (m + 1)*SizeOf(Integer));

  GetMem(bmGs, (m + 1)*SizeOf(Integer));

 

  preBmBc(X, m, bmBc^);

  preBmGs(X, m, bmGs^);

  j := 1;

  while (j <= n - m + 1) do

  begin

    i := m;

    while (i >= 1)and(X = Y[i + j - 1]) do Dec(i);

    if (i < 1) then

    begin

      Output(j);

      j := j + bmGs^[1];

    end

    else  {chọn cách dịch được lợi nhất }

      j := j + Max(bmGs^ , bmBc^[Ord(Y[i + j - 1])]

- m + i);

  end;

  FreeMem(bmBc, (m + 1)*SizeOf(Integer));

  FreeMem(bmGs, (m + 1)*SizeOf(Integer));

end;

Page 25: giai thuat doi sanh mau

Thuật toán Boyer-Moore có thể đạt tới chi phí O(n/m) là nhờ có cách dịch thứ 2 “ký tự

không khớp”. Cách chuyển cửa sổ khi gặp “ký tự không khớp” cài đặt vừa đơn giản lại rất

hiệu quả trong các bảng chữ cái lớn nên có nhiều thuật toán khác cũng đã lợi dụng các quét

mẫu từ phải sang trái để sử dụng cách dịch này.

Tuy nhiên chi phí thuật toán của Boyer-Moore là O(m*n) vì cách dịch thứ nhất của thuật

toán này không phân tích triệt để các thông tin của những lần thử trước, những đoạn đã so

sánh rồi vẫn có thể bị so sánh lại. Có một vài thuật toán đã cải tiến cách dịch này để đưa đến

chi phí tính toán của thuật toán Boyer-Moore là tuyến tính. (xin tham khảo thêm trong

chương trinh demo đi kèm)

VII. Thuật toán Karp-Rabin

Karp-Rabin bài toán tìm kiếm chuỗi không khác nhiều so với bài toán tìm kiếm chuẩn. Tại

đây một hàm băm được dùng để tránh đi sự so sánh không cần thiết. Thay vì phải so sánh

tất các vị trí của văn bản, ta chỉ cần so sánh những cửa sổ bao gồm những ký tự “có vẻ

giống” mẫu.

Trong thuật toán này hàm băm phải thỏa mãn một số tính chất như phải dễ dàng tính được

trên chuỗi, và đặc biệt công việc tính lại phải đơn giản để ít ảnh hưởng đến thời gian thực

hiện của thuật toán. Và hàm băm được chọn ở đây là:

hash(w[i…i+m-1]) = h = (w *dm-1 + w[i+1]*dm-2 + … w[i+m-1]*d0) mod q

Việc tính lại hàm băm sau khi dịch cửa sổ đi một ký tự chỉ đơn gian như sau:

h = ((h – w[i]*dm-1)*d + w[i+m]

Trong bài toán này ta có thể chọn d = 2 để tiện cho việc tính toán a*2 tương đương a shl 1.

Và không chỉ thế ta chọn q = MaxLongint khi đó phép mod q không cần thiết phải thực hiện

vì sự tràn số trong tính toán chính là một phép mod có tốc độ rất nhanh.

Việc chuẩn bị trong thuật toán Karp-Rabin có độ phức tạp O(m). Tuy vậy thời gian tìm kiếm

lại tỉ lệ với O(m*n) vì có thể có nhiều trường hợp hàm băm của chúng ta bị lừa và không

Page 26: giai thuat doi sanh mau

phát huy tác dụng. Nhưng đó chỉ là những trường hợp đặc biệt, thời gian tính toán của thuật

toán KR trong thực tế thường tỉ lệ với O(n+m). Hơn nữa thuật toán KR có thể dễ dàng mở rộng cho các

mẫu, văn bản dạng 2 chiều, do đó khiến cho nó trở nên hữu ích hơn so với các thuật toán còn lại trong việc xử lý ảnh.

procedure KR(const X: string; m: integer;

             const Y: string; n: integer);

var

  dM, hx, hy: longint;

  i, j: integer;

begin

  {$Q-}   { Disable arithmetic overflow checking }

  dM := 1;

  for i := 1 to m - 1 do dM := dM shl 1;

  hx := 0;

  hy := 0;

  for i := 1 to m do

  begin

    hx := (hx shl 1) + Ord(X );

    hy := (hy shl 1) + Ord(Y );

  end;

  j := 1;

  while j <= n - m do

  begin

    if hx = hy then

      if IsMatch(X, m, Y, j) then Output(j);

      {hàm IsMatch trong phần BruteForce}

    hy := ((hy - Ord(Y[j])*dM) shl 1) + Ord(Y[j +

m]); {Rehash}

    Inc(j);

  end;

  if hx = hy then

    if IsMatch(X, m, Y, j) then Output(j);

end;

Page 27: giai thuat doi sanh mau

VIII. Các thuật toán khác

Một số thuật toán nêu trên chưa phải là tất cả các thuật toán tìm kiếm chuỗi hiện có. Nhưng

chúng đã đại diện cho đa số các tư tưởng dùng để giải bài toán tìm kiếm chuỗi.

Các thuật toán so sánh mẫu lần lượt từ trái sang phải thường là các dạng cải tiến (và cải lùi)

của thuật toán Knuth-Morris-Pratt và thuật toán sử dụng Automat như: Forward Dawg

Matching, Apostolico-Crochemore, Not So Naive, …

Các thuật toán so sánh mẫu từ phải sang trái đều là các dạng của thuật toán Boyer-Moore.

Phải nói lại rằng thuật toán BM là thuật toán tìm kiếm rất hiệu quả trên thực tế nhưng độ

phức tạp tính toán lý thuyết lại là O(m*n). Chính vì vậy những cải tiến của thuật toán này

cho độ phức tạp tính toán lý thuyết tốt như: thuật toán Apostolico-Giancarlo đánh dấu lại

những ký tự đã so sánh rồi để khỏi bị so sánh lặp lại, thuật toán Turbo-BM đánh giá chặt chẽ

hơn các thông tin trước để có thể dịch được xa hơn và ít bị lặp, … Còn có một số cải tiến

khác của thuật toán BM không làm giảm độ phức tạp lý thuyết mà dựa trên kinh nghiệm để

có tốc độ tìm kiếm nhanh hơn trong thực tế. Ngoài ra, một số thuật toán kết hợp quá trình

tìm kiếm của BM vào hệ thống Automat mong đạt kết quả tốt hơn.

Các thuật toán so sánh mẫu theo thứ tự đặc biệt

Thuật toán Galil-Seiferas và Crochemore-Perrin chúng chia mẫu thành hai đoạn, đầu

tiên kiểm tra đoạn ở bên phải rồi mới kiểm tra đoạn bên trái với chiều từ trái sang

phải.

Thuật toán Colussi và Galil-Giancarlo lại chia mẫu thành hai tập và tiến hành tìm kiếm

trên mỗi tập với một chiều khác nhau.

Thuật toán Optimal Mismatch và Maximal Shift sắp xếp thứ tự mẫu dựa vào mật độ

của ký tự và khoảng dịch được.

Thuật toán Skip Search, KMP Skip Search và Alpha Skip Search dựa sự phân bố các ký

tự để quyết đinh vị trí bắt đầu của mẫu trên văn bản.

Page 28: giai thuat doi sanh mau

Các thuật toán so sánh mẫu theo thứ tự bất kỳ những thuật toán này có thể tiến hành so

sánh mẫu với cửa sổ theo một thứ thự ngẫu nhiên. Những thuật toán này đều có cài đặt rất

đơn giản và thường sử dụng chiêu ký tự không khớp của thuật toán Boyer-Moore. Có lẽ loại

thuật toán này dựa trên ý tưởng càng so sánh loạn càng khó kiếm test chết

Thuật toán Phân tích từ-dưới-lên & Trình biên dịch trong phân tích câu14/07/2009

Mặc dù có nhiều lệnh gọi đệ quy trong các chương trình ở phần trước, có một bài tập hướng dẫn để loại đi sự đệ quy một cách có hệ thống. Mỗi lệnh gọi thủ tục có thể được thay bằng một thủ tục cất vào ngăn xếp và thay mỗi thủ tục trả về bởi một lệnh lấy khỏi ngăn xếp, bắt chước những gì hệ Pascal thực hiện để cài đặt đệ quy. Cũng vậy, nhớ lại rằng một lý do để làm điều này là nhiều lệnh gọi có vẻ như đệ quy nhưng lại không thực sự đệ quy. Khi một lệnh gọi thủ tục là hành động cuối cùng của một thủ tục, thì một lệnh goto đơn giản có thể được sử dụng. Nó chuyển expression và term vào những vòng lặp đơn giản có thể được trộn lại với nhau và được tổ hợp với factor để sinh ra một thủ tục duy nhất với một lệnh đẹ quy thực sự (lệnh gọi tới expression trong factor).

Cách nhìn này trực tiếp dẫn tới một phương pháp thật đơn giản để kiểm tra khi nào các biểu thức chính quy là hợp lệ. Một khi tất cả các lệnh gọi thủ tục đã được loại bỏ, ta thấy rằng mỗi ký hiệu kết thúc sẽ chỉ được quét khi nó bị bắt gặp. Việc xử lý duy nhất được thực hiện thực sự là kiểm tra xem có một dấu ngoặc phải sánh được với mỗi một dấu ngoặc trái hay không, khi nào thì mỗidấu "+" là được theo sau bởi một chữ cái hoặc một dấu "(", và khi nào thì mỗi dấu "*" là được theo sau bởi một chữ cái hoặc một dấu ")". Nghĩa là, việc kiểm tra khi nào một biểu thức chính quy là hợp lệ sẽ chủ yếu tương đương với việc kiểm tra xem có sự cân bằng số dấu ngoặc hay không. Điều này có thể được cài đặt một cách đơn giản bằng cách duy trì một biến đếm (counter), được khởi động là 0, được tăng lên khi bắt gặp một dấu ngoặc trái và được giảm đi khi bắt gặp một dấu ngoặc phải. Nếu biến đếm là 0 ở cuối biểu thức, và các ký hiệu "+" và "*" trong biểu thức thoả mãn các yêu cầu vừa được nêu, thì biểu thức là hợp lệ.

Dĩ nhiên, có những phân tích khác hơn là chỉ kiểm tra khi nào dãy nhập vào là hợp lệ: mục đích là để xây dựng cây phân tích (ngay cả theo cách ngầm định, như trong bộ phân tích từ-trên-xuống) cho công việc xử lý khác nữa. Có khuynh hướng là có thể làm điều này với các chương trình với cùng một cấu trúc chủ yếu như bộ kiểm tra dấu ngoặc đã được mô tả trong đoạn trước. Một kiểu bộ phân tích mà nó thực hiện theo cách này thường được gọi là bộ phân tích giảm-phép-dịch-chuyển. Ý tưởng là duy trì một ngăn xếp đẩy xuống (pushdown stack) chứa các ký hiệu kết và chưa kết. Mỗi bước trong việc phân tích sẽ hoặc là một bước dịch-chuyển, trong đó đưon giản là ký tự nhập kế sẽ được cất trên ngăn xếp, hoặc là một bước làm giảm, trong đó các ký tự tại đỉnh ngăn xếp được đối sánh với vế phải của một luật sinh nào đó trong văn phạm và được thay bằng ký tự chưa kết trên vế trái của luật sinh đó. (Khó khăn chính trong việc xây dựng một bộ phân tích giảm-phép-dịch-chuyểnlầ quyết định xem khi nào thì dịch chuyển và khi nào thì làm giảm. Đây có thể là một quyết định phức tạp, phụ thuộc vào văn phạm). Sau này,tất cả các ký tự nhập vào được dịch chuyểnvào trong ngăn xếp, và cuối cùng, ngăn xếp được làm giảm thành một ký hiệu chưa kết đơn lẻ.

Phân tích từ-dưới-lên thường được xem là phương pháp lựa chọn cho các ngôn ngữ lập trình thực sự, và có một tài liệu mở rộng việc phát triển các bộ phân tích cho các bộ văn phạm lớn có kiểu được yêu cầu để mô tả một ngôn ngữ lập trình. Mô tả ngắn gọn của chúng ta chỉ lướt qua bề mặt của các kết quả đã được tạo ra.

Page 29: giai thuat doi sanh mau

 

Trình biên dịch

Một trình biên dịch có thể được xem như một chương trình dịch một ngôn ngữ thành một ngôn ngữ khác. Ví dụ, một trình biên dịch Pascal dịch các chương trình từ ngôn ngữ Pascal thành ngôn ngữ máy của một máy cụ thể nào đó. Ta sẽ minh hoạ một cách làm điều này bằng cách tiếp tục với ví dụ đối-sánh-mẫu-biểu-thức-chính-quy; tuy nhiên, bây giờ ta mong muốn dịch từ ngôn ngữ của các biểu thức chính quy thành một "ngôn ngữ" cho các máy đối-sánh-mẫu, các mảng ch, next1, next2 của chương trình match (trong chương trình Thuật toán trong Đối sánh mẫu trong kỳ trước).

Tiến trình dịch chủ yếu là "một-một" : Với mỗi ký tự trong mẫu (ngoại trừ các dấu ngoặc đơn), ta muốn sinh ra một trạng thái cho máy đối-sánh-mẫu. Mẹo là lưu giữ dấu vết của thông tin cần thiết để đổ vào trong các mảng next1 và next2. Để làm điều đó ta sẽ chuyển mỗi một thủ tục trong bộ phân tích đệ-quy-xuống thành các hàm mà nó khởi tạo các máy đối-sánh-mẫu. Mỗi hàm sẽ thêm vào các trạng thái mới khi cần vào cuối của các mảng ch, next1 và next2 và trả về chỉ mục của trạng thái khởi đầu của máy đã được khởi tạo (trạng thái cuối sẽ luôn luôn là đầu vào cuối cùng trong các mảng). Ví dụ, hàm được cho dưới đây đối với luật sinh của sẽ tạo ra các trạng thái "hoặc" (or) cho máy đối-sánh-mẫu:

++

fuction expression: integer;

var ti, t2: integer;

begin

t1:=term; expression:=t1;

if p[j]= '+' then

begin

j:= j+1; state:= state+1; t2:=state; expression:=t2; state:=state+1;

setstate(t2, '', expression, t1);

setstate(t2-1,' ', state,state );

end;

end;

++

Hàm này sử dụng một thủ tục setstate mà nó chỉ đơn giản là đặt các đầu vào cho các mảng ch, next1 và next2 được chỉ mục bởi tham số đầu tiên chứa các giá trị được cho trong các tham số thứ 2, thứ 3 và thứ 4 tương ứng. Chỉ mục state lưu giữ tình trạng "hiện hành" của máy đang được xây dựng: Mỗi khi một trạng thái mới được tạo ra, state được tăng lên. Vì vậy các chỉ mục trạng thái cho máy sẽ tương ứng với một dãy lệnh gọi thủ tục cụ thể giữa giá trị của state ở đầu vào và giá trị state ở đầu ra. Chỉ mục trạng thái cuối là giá trị của state lúc thoát ra (chúng ta không thực sự " tạo" các trạng thái cuối cùng bằng cách tăng state trước khi thoát ra, vì điều này khiến cho nó dễ "trộn lẫn" trạng thái cuối cùng với trạng thái khởi đầu sau đó).

Page 30: giai thuat doi sanh mau

Với quy ước này, ta sẽ kiểm chứng (chú ý lệnh gọi đệ quy !) là chương trình ở trên cài đặt quy tắc cho việc tổ hợp hai máy với phép toán or. Trước hết máy dành cho phần đầu của biểu thức được xây dựng (một cách đệ quy), sau đó 2 trạng thái null mới được thêm vào và phần trhứ hai của biểu thức được xây dựng. Trạng thái null đầu tiên (với chỉ mục của biểu thức t2-1) là trạng thái cuối của máy cho phần đầu của biểu thức mà nó được chế tạo thành một trạng thái "no-op" để nhảy tới trạng thái kết thúc của máy cho phần thứ hai của biểu thức như được yêu cầu. Trạng thái null thứ hai (với chỉ mục t2) là trạng thái khởi đầu, như thế chỉ mục của nó là giá trị trả về cho expression và các đầu vào next1 và next2 được tạo ra để chỉ tới những trạng thái khởi đầucủa hai biểu thức. Chú ý là những biểu thức này được cấu tạo theo thứ tự đảo ngược với những gì ta mong muốn, do giá trị của state cho trạng thái no-op là không biết được cho đến khi lệnh gọi đệ quy tới expression được tạo ra.

Hàm trước tiên xây dựng một cái máy cho một và sau đó, nếu cần, trộn trạng thái kết thúc của máy đó với trạng thái khởi đầu của máy cho một khác. Điều này dễ dàng được thực hiện vì state là chỉ mục trạng thái kết thúc của lệnh gọi tới factor. Một lệnh gọi tới term mà không tăng state được thực hiện mẹo sau:

++

function term;

var t: integer;

begin

term:=factor;

if (p[j] = '(' ) or letter(p[j]) then

t:=term

end;

++

(chúng ta không cần dùng đến chỉ mục trạng thái khởi đầu được trả về bởi lệnh gọi thứ hai tới term, nhưng Pascal yêu cầu chúng ta đặt nó vào đâu đó, do đó ta sẽ bỏ nó vào một biến tạm i)

Hàm cho sử dụng các kỹ thuật tương tự để kiểm soát ba trường hợp của nó: một dấu ngoặc đại diện cho một lệnh gọi đệ quy trên expression; một ký hiệu v đại diện cho phép nối một trạng thái mới cuối chuỗi; và một dấu * đại diện cho các phép toán tương tự như các phép toán trong expression.

++

funtion factor;

var t1, t2: integer;

begin t1:=state;

if p[j] = '(' then

begin

j:= j+1; t2:= expression;

Page 31: giai thuat doi sanh mau

if p[j]=')' then j:=j+1 else error

end

else if letter(p[j]) then

begin

setstate(state, p[j], state+1, state+1);

t2:= state; j:=j+1; state:=state+1

end;

else error;

if p[j] <> '*' then factor:= t2 else

begin

setstate(state,' ', state+1, t2);

factor:=state; next1[t1-1]:= state;

j:= j+1; state:= state+1;

end;

end;

++

Với ví dụ của chúng ta, làm thế nào các trạng thái được kiến tạo cho mẫu (A*B+AC)D. Đầu tiên, trạng thái 1 được xây dựng cho A. Sau đó, trạng thái 2 được xây dựng cho toán hạng kết (closure) và trạng thái 3 được gắn vào cho B. Kế đó, dấu "+" được bắt gặp và các trạng thái 4, 5 được xây dựng bởi expression, nhưng các vùng của chúng không thể được đổ đầy cho đến khi thực hiện xong một lệnh gọi đệ quy với expression, và điều này cuối cùng gây ra việc tạo dựng các trạng thái 6 và 7. Cuối cùng phéo nối của D được điều khiển bởi trạng thái 8, để lại trạng thái 9 như là trạng thái kết thúc.

Bước cuối cùng trong việc phát triển một thuật toán đối-sánh-mẫu biểu-thức-chính-quy là gắn các thủ tục này lại với nhau cùng thủ tục match:

++

procedure matchall;

begin

j:=1; state:=1; next[0]:= expression;

setstate(state, ' ', 0,0);

Page 32: giai thuat doi sanh mau

for i:=1 to N-1 do

if match(i) >= i then

writeln(i);

end;

++

Chương trình này in ra tất cả các vị trí ký tự trong một chuỗi văn bản a[1..N] ở đó có một mẫu p[1..M] dẫn tới sự trùng khớp.