記得在窮舉法中,每一趟比較後,無論成與不成,都將模式向右滑動一個位置,然後繼續比較。有沒有辦法能利用之前的比較結果,使得模式滑動的更遠一點呢?
在介紹經典的KMP算法前,我先介紹幾個簡單的滑動類算法。
Not So Naive
同名字一樣,這個算法的確有點幼稚,它根據模式的前兩個字符是否相同來滑動比窮舉法稍長一點的距離:如果前兩個字符相同,那麼文本中與第二個字符不同則必然也與第一個不同;如果前兩個字符不同,則與第二個相同的文本字符必然與第一個不同。
那麼這兩種情況下不用比較都可以斷定,文本字符與模式的第一個字符肯定不相同,於是能比窮舉法多滑動1個位置。
代碼見下:
- void NSN(char *x, int m, char *y, int n) {
- int j, k, ell;
-
-
- if (x[0] == x[1]) {
- k = 2;
- ell = 1;
- }
- else {
- k = 1;
- ell = 2;
- }
-
-
- j = 0;
- while (j <= n - m)
- if (x[1] != y[j + 1])
- j += k;
- else {
- if (memcmp(x + 2, y + j + 2, m - 2) == 0 &
- x[0] == y[j])
- OUTPUT(j);
- j += ell;
- }
- }
這個算法僅需要常數時間和空間的預處理,比較過程中,先比較模式第二個字符,然後比較其餘位置,爲的就在某些情況下省掉第一個字符的比較,達到滑動的目的。不過複雜度依然是O(mn)的,比起窮舉法或者有輕微改善吧。
想法的確夠幼稚,僅僅只考慮了兩個模式字符,滑動的步子也太小,能否考慮的更多一點呢?下面請看Quick Search算法。
Quick Search
見到這個名字,不禁讓人想起快速排序了,快速排序在最壞情況下是n平方的複雜度,而通常情況下速度超級快,Quick Search莫非也是這樣的?沒錯,就是這樣,這個算法在模式長度短而字母表大時,有着優異的表現,儘管它的搜索時間複雜度是O(mn)。
算法的思想是這樣,如果文本中某個字符根本就沒在模式中出現過,那麼就不需要再去和模式中的任何一個比較;如果該字符出現過,那麼爲了不漏掉可能的匹配,只好與最晚出現過的位置對齊進行比較了。
代碼如下:
- void preQsBc(char *x, int m, int qsBc[]) {
- int i;
- for (i = 0; i < ASIZE; ++i)
- qsBc[i] = m + 1;
- for (i = 0; i < m; ++i)
- qsBc[x[i]] = m - i;
- }
- void QS(char *x, int m, char *y, int n) {
- int j, qsBc[ASIZE];
-
- preQsBc(x, m, qsBc);
-
-
- j = 0;
- while (j <= n - m) {
- if (memcmp(x, y + j, m) == 0)
- OUTPUT(j);
- j += qsBc[y[j + m]];
- }
- }
理解這個算法,請看22行,無論這一趟比較是否成功,都進行模式串的滑動,這個滑動就是根據窗口之外的第一個字符位於模式串的位置來決定的,你可以把窗口外第一個字符是否能匹配看成下一趟比較的前提。
現在你知道爲何這個算法最適合在短模式和大字母表下運行了,因爲字母表大,模式短,則文本字符不在模式中出現的機率就大,因此更大可能性得進行最長距離的滑動,而且模式短,花在比較上的時間就短,可以儘量多滑動。
美中不足的是這個算法最壞情況下複雜度還是O(mn),儘管預處理中已經利用上了每一個模式字符了。通過滑動能找到一個線性算法嗎?仔細審視一下比較過程,造成算法非線性的根本原因是什麼?沒錯,文本串回溯了。讓我們來看看一個真正的線性算法——MP,以及它的改進——KMP。
MP/KMP
本着文本串不回溯的目標,MP算法橫空出世,它的一個重要指導思想是,凡是比較過,被認定爲相同的文本字符,絕不再拿出來比。道理上也是能說得通的,因爲既然和模式串一部分相同,那麼它的信息就已經存在於模式串中了。預處理時,模式串自己和自己的一部分進行比較,存儲下自身的相似信息——Next數組。
以後在比較時,如果某處失配了,根據之前預處理的結果,可以直接滑動到自身相似的那一部分與文本串對齊,然後從失配處繼續比較,避免了文本串回溯。
偉大的計算機科學家Knuth,就是寫TAOUP的那位,對MP算法進行了些許修正,加上了自己的名字,成了KMP。Knuth注意到,如果滑動前的那個模式字符與滑動後的模式字符相同的話,那麼再比較必然再次失配,導致又一次滑動,與其多級滑動,不如一滑到底。
代碼:
- void preMp(char *x, int m, int Next[]) {
- int i, j;
- i = 0;
- j = Next[0] = -1;
- while (i < m) {
- while (j > -1 && x[i] != x[j])
- j = Next[j];
- i++;
- j++;
-
-
-
-
- Next[i] = j;
- }
- }
- void MP(char *x, int m, char *y, int n) {
- int i, j, Next[XSIZE];
-
- preMp(x, m, Next);
-
- i = j = 0;
- while (j < n) {
- while (i > -1 && x[i] != y[j])
- i = Next[i];
- i++;
- j++;
- if (i >= m) {
- OUTPUT(j - i);
- i = Next[i];
- }
- }
- }
MP和KMP算法都達到了O(m)的預處理時間和空間,O(n+m)的比較時間,算法實現是如此簡單優美,算法思想是如此無可挑剔,還能滑的更遠嗎?我們拭目以待。