窮舉法又叫暴力法。大多數(shù)程序員眼里,它是幼稚的,但大師們不這么認為。
Rob Pike, 最偉大的C 語言大師之一, 在《Notes on C Programming》中闡述了一個原則:花哨的算法比簡單算法更容易出bug、更難實現(xiàn),盡量使用簡單的算法配合簡單的數(shù)據(jù)結(jié)構(gòu)。而Ken Thompson——Unix 最初版本的設(shè)計者和實現(xiàn)者,禪宗偈語般地對Pike 的這一原則作了強調(diào): 拿不準就窮舉(When in doubt , use brute force)。 而對于裝13愛好者來說,更是自豪的稱其使用的是BF算法。
窮舉法用在字符串匹配上,簡單的描述就是,檢查文本從0到n-m的每一個位置,看看從這個位置開始是否與模式匹配。這種方法還是有一些優(yōu)點的,如:不需要預(yù)處理過程,需要的額外空間為常數(shù),每一趟比較時可以以任意順序進行。
盡管它的時間復(fù)雜度為O(mn),例如在文本"aaaaaaaaaaaaaaaaaaaaaaaaaaa"中尋找"aaaaab"時,就完全體現(xiàn)出來了。但是算法的期望值卻是2n,這表明該算法在實際應(yīng)用中效率不低。
C代碼如下:
- void BF(char *x, int m, char *y, int n) {
- int i, j;
-
-
- for (j = 0; j <= n - m; ++j) {
- for (i = 0; i < m && x[i] == y[i + j]; ++i);
- if (i >= m)
- OUTPUT(j);
- }
- }
-
如果我們注意到C庫函數(shù)是匯編優(yōu)化過的,并通常能提供比C代碼更高的性能的話,我們可以用memcmp來完成每一趟比較過程,從而達到更好的性能:
- #define EOS '\0'
-
- void BF(char *x, int m, char *y, int n) {
- char *yb;
-
- for (yb = y; *y != EOS; ++y)
- if (memcmp(x, y, m) == 0)
- OUTPUT(y - yb);
- }
-
自動機的方法其實和窮舉法有點相似,都是用最簡單直白的方式來做事情。區(qū)別在于窮舉法是在計算,而自動機則是查表。盡管自動機的構(gòu)造過程有一點點難解,要涉及到DFA的理論,但是自動機的比較過程那絕對是簡單到無語。
簡單說來,根據(jù)模式串,畫好了一張大的表格,表格m+1行σ列,這里σ表示字母表的大小。表格每一行表示一種狀態(tài),狀態(tài)數(shù)比模式長度多1。一開始的狀態(tài)是0,也就是處在表格的第0行,這一行的每個元素指示了當遇到某字符時就跳轉(zhuǎn)到另一個狀態(tài)。每當跳轉(zhuǎn)到最終狀態(tài)時,表示找到了一個匹配。
語言表述起來還是比較啰嗦,看代碼就知道了:
- #define ASIZE 256
-
- int preAut(const char *x, int m, int* aut) {
- int i, state, target, old;
-
- for (state = 0, i = 0; i < m; ++i) {
- target = i + 1;
- old = aut[state * ASIZE + x[i]];
- aut[state * ASIZE + x[i]] = target;
- memcpy(aut + target * ASIZE, aut + old * ASIZE, ASIZE*sizeof(int));
- state = target;
- }
- return state;
- }
-
-
- void AUT(const char *x, int m, const char *y, int n) {
- int j, state;
-
-
- int *aut = (int*)calloc((m+1)*ASIZE, sizeof(int));
- int Terminal = preAut(x, m, aut);
-
-
- for (state = 0, j = 0; j < n; ++j) {
- state = aut[state*ASIZE+y[j]];
- if (state == Terminal)
- OUTPUT(j - m + 1);
- }
- }
(注:原文的代碼使用一個有向圖的數(shù)據(jù)結(jié)構(gòu),我遵循大師的指引,改用了更簡單一點的數(shù)組)
從代碼上我們很容易看出,自動機的構(gòu)造需要時間是O(mσ),空間也是O(mσ)(嚴格來說這份代碼使用了O((m+1)σ)),但是一旦構(gòu)造完畢,接下來匹配的時間則是O(n)。
匹配的過程前面已經(jīng)說了,太簡單了沒什么好說的,這里就解釋一下構(gòu)造過程吧!
我們構(gòu)造的目標是對應(yīng)模式長度,構(gòu)造出同樣多的狀態(tài),用0表示初始狀態(tài),然后第一個字符用狀態(tài)1表示,第二個用狀態(tài)2表示,依次類推,直到最后一個字符,用m表示,也是最終狀態(tài)。
一開始,數(shù)組全都置0,,這個時候的自動機遇到任何字符都轉(zhuǎn)到初始狀態(tài)。然后給它看模式的第一個字符,假設(shè)這是'a'吧,告訴它,狀態(tài)0遇到'a'應(yīng)該到一個新的狀態(tài)——狀態(tài)1,所以把第0行的第'a'列修改為1。而這個時候狀態(tài)1還是空白的,怎么辦呢?
這時候狀態(tài)0就想呀,在我被告知遇到'a'要去狀態(tài)1之前,我原本遇到'a'都要去狀態(tài)0的,也就是修改之前第'a'列所指的那個狀態(tài),稱為old狀態(tài)吧;而現(xiàn)在我遇到'a'卻要去一個新的狀態(tài),既然以前old狀態(tài)能處理遇到'a'之后的事情,那么我讓新的狀態(tài)像old狀態(tài)一樣就好了。于是狀態(tài)0把old狀態(tài)拷貝到狀態(tài)1。
現(xiàn)在輪到狀態(tài)1了,給它看第二個字符,它也如法炮制,指向了狀態(tài)2,又把old狀態(tài)拷貝給了狀態(tài)2。
于是,狀態(tài)機就在這種代代傳承的過程中構(gòu)造完畢了。
雖然理論上自動機是最完美的匹配方式,但是由于預(yù)處理的消耗過大,實踐中,主要還是用于正則表達式。
結(jié)語:窮舉法與自動機各自走了兩個極端,因此都沒能達到綜合性能的最佳,本文之后介紹的算法,可以看成是在窮舉和自動機兩者之間取舍權(quán)衡的結(jié)果。