字符串模式匹配——最长公共子序列与子串 KMP 算法
最長公共子序列
最長公共子序列的問題很簡單,就是在兩個字符串中找到最長的子序列,這里明確兩個含義:
所以這里要查找的是不連續的最長子序列,
動態規劃
這里為什么要使用動態規劃可以說一下,簡單來說動態規劃是為了降低時間復雜度的一種算法,申請一個額外空間,來保存每一個步驟的結果,最后從這些結果中找到最優的解。
這里有個問題就是:一般來說,當前的最優解,只與當前時刻和上一時刻有關系,和其他時刻沒有關系,這樣才能讓動態規劃發生作用,降低復雜度。
分析LCS
其實LCS看起來很麻煩,找不到思路,如果暴力破解可能要O(n^4)了,而這個題目使用動態規劃的思想也非常簡單,為何相比之前的問題不好找思路呢?
是因為之前的動態規劃問題例如:背包問題,生產線問題,都是一維數組空間內的結果,規劃到一個線性時間內,而這個題目需要O(m*n)的時間復雜度和空間復雜度。
所以其實就是進行m*n次對比,每次保存當前的最優解,就可以了。
代碼實現分析
這里有個問題,就是我們需要的結果僅僅是長度? 還是包括這個序列串一起輸出。
看下面圖:
這里可以看到,我們構造的一個i*j的矩陣,這個矩陣里的內容不但包括數值(當前結果的最優解),還包括一個方向箭頭,這個代表了我們回溯的時候,需要行走的方向。
所以我們這里保存兩個值,可以使用兩個二維矩陣,也可以使用一個結構體矩陣。
解法分析
其實這個題目在動態規劃來理解,也非常簡單。一個狀態轉移函數。
這個非常好理解,其中一個字符串為0的時候,那么肯定是0了。
當兩個字符相等的時候,這個時候很好理解,舉例來說:
abcd?和?adcd,在遍歷c的時候,發現前面只有a相等了,也就是1.?
那么c相等,也就是abc和adc在匹配的時候,一定比ab和ad的長度大1,這個1就是c相等么。也就是相等的時候,是比c[i-1][j-1]大1的。
下一個更好理解了,如果不相等,肯定就是找到上一個時刻對比最大的么。
代碼
這個代碼只輸出了LCS的長度,而結果數組的方向我已經存儲好了,想要遍歷的,直接從后向前遍歷數組就可以輸出最長公共子序列了,即哪些 direct = 0 的節點。
// // main.cpp // LCS // // 最長公共子序列(LCS) //#include <iostream>using namespace std;/* * 這里可以不定義長度,輸入的字符串用string存儲,然后利用string.c_str()來對字符串進行數組轉化。 我這里為了方便沒有這樣做。 */ #ifndef MAX_LENGTH #define MAX_LENGTH 15 //定義字符串最大長度 #endifint MaxNum(int firstNum, int secondNum){return firstNum > secondNum ? firstNum : secondNum; }//定義數組結構體 struct matrix{int num;int direct; };typedef matrix Matrix;int LCS(char *strA, char *strB, int lengthA, int lengthB, Matrix *resultMatrix[]){if (lengthA == 0 || lengthB == 0) {return 0;}for (int i = 0; i < lengthA; i++) {for (int j = 0; j < lengthB; j++) {resultMatrix[i][j].num = 0; //設置所有默認的最長為0resultMatrix[i][j].direct = 1; //所有默認方向變成上 0斜上,1上,-1左}}for (int i = 0; i < lengthA; i++) {for (int j = 0; j < lengthB; j++) {if (strA[i] == strB[j]) {resultMatrix[i+1][j+1].num = resultMatrix[i][j].num + 1;resultMatrix[i+1][j+1].direct = 0;}else{resultMatrix[i+1][j+1].num = MaxNum(resultMatrix[i+1][j].num, resultMatrix[i][j+1].num);resultMatrix[i+1][j+1].direct = resultMatrix[i+1][j].num > resultMatrix[i][j+1].num ? 1 : -1;}}}return resultMatrix[lengthA][lengthB].num; }int main(int argc, const char * argv[]) {char *strA = (char*)malloc(sizeof(char) * MAX_LENGTH);char *strB = (char*)malloc(sizeof(char) * MAX_LENGTH);scanf("%s",strA);scanf("%s",strB);int lengthA = (int)strlen(strA);int lengthB = (int)strlen(strB);Matrix *resultMatrix[lengthA+1];for (int i = 0; i <= lengthA; i++) {resultMatrix[i] = (Matrix*)malloc(sizeof(struct matrix)* (lengthB+1));}int max = LCS(strA, strB, lengthA, lengthB, resultMatrix);printf("%d\n",max);std::cout << "Hello, World!\n";return 0; }?
上面我們介紹了最長公共子序列,下面我們來介紹一種子串(連續字符)的模式匹配問題,以及 BF 算法和 KMP 算法。
KMP 算法背景
給定一個主串(以 S 代替)和模式串(以 P 代替),要求找出 P 在 S 中出現的位置,此即串的模式匹配問題。
Knuth-Morris-Pratt 算法(簡稱 KMP)是解決這一問題的常用算法之一,這個算法是由高德納(Donald Ervin Knuth)和沃恩·普拉特在 1974 年構思,同年詹姆斯·H·莫里斯也獨立地設計出該算法,最終三人于 1977 年聯合發表。
在繼續下面的內容之前,有必要在這里介紹下兩個概念:真前綴?和?真后綴。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
由上圖所得, "真前綴" 指除了自身以外,一個字符串的全部頭部組合;"真后綴" 指除了自身以外,一個字符串的全部尾部組合。于是得到兩個集合,而我們后面的 next 數組的元素便是這兩個集合之交中最長字符串元素的長度。而 KMP 算法便是根據next數組中的值來減少回溯重復比較字符而提高算法的效率的。
?
暴力樸素字符串匹配?BF 算法
初遇串的模式匹配問題,我們腦海中的第一反應,就是樸素字符串匹配?BF(Brute Force)(即所謂的暴力匹配),代碼如下:
暴力匹配的時間復雜度為?O( (m-n+1) * n),其中?n?為 S 的長度,m?為 P 的長度。很明顯,這樣的時間復雜度很難滿足我們的需求。
接下來進入正題:時間復雜度為?Θ(n+m)?的 KMP 算法。
?
KMP字符串匹配算法
簡言之,以圖中的例子來說,在 i 處失配,那么主字符串和模式字符串的前邊6位就是相同的。又因為模式字符串的前6位,在前 6 位中我們發現其??"真前綴" 和 "真后綴" 的最長的相同真前后綴的長度是 4,它的前4位前綴和后4位后綴是相同的,所以我們推知主字符串i之前的4位和模式字符串開頭的4位是相同的。就是圖中的灰色部分。那這部分就不用再比較了。而當我們回溯時,此時 i = 6,j = 4,我們保持 i = 6, 不變。而將模式串向右滑動,即調整 j 的值,而湊巧我們知道其 next[ i=4 ] 此時的值,代表最長的相同真前后綴長度,就等于 4。于是我們果斷將 j 更新回溯為 j = 4, i 不變,i = 6。得到如圖(b)的狀態。
? ? ? ? ? ? ? ? ? ? ? ??
有了上面的分析,我們先假設已經有了獲取 next 數組的函數,在不分析其具體實現的抽象之上便可以構建起 KMP 算法的框架,如下:
/* 在 S 中找到 P 第一次出現的位置 */ int KMP(string S, string P, int *next) {GetNext(P, next);int i = 0; // S 的下標int j = 0; // P 的下標int s_len = S.size();int p_len = P.size();while (i < s_len && j < p_len) // 因為末尾 '\0' 的存在,所以不會越界{if (j == -1 || S[i] == P[j]) // P 的第一個字符不匹配或 S[i] == P[j]{++i; ++j;}elsej = next[j]; // 當前字符匹配失敗,進行跳轉}if (j == p_len) // 匹配成功return i - j;return -1; }注意,因為作為模式串如果只有一個字符,則它肯定沒有其相等的最長真前后綴。故我們會將 next [0] = -1 即第一位的值設為 -1。所以在上述的代碼中 if 條件中的表達式 j == -1 是有可能通過 else 語句中的 next[j] 賦值而致使其成立的。此時表示其第一個字符不匹配。注意,每一個 next[j] 的值都是依賴從模式串中的 [0 ... j-1] 中的相等的最長真前后綴的長度決定的。
于是我們便可以的出求取 next 數組的第一種方法:
KMP優化
其實在上面的求取 next 數組中,在 ++i, ++j 之后并沒有判斷 p[i] 是否與 p[j] 相同,故仍會出現有冗余的比較字符的情況,于是就有了下面的求取 next 數組的改進算法。使用這種方式求取的 next 數組,整合到 KMP 中就稱為 kMP 的優化情況。如下所示:
void GetNextval(string P, int *nextval) {int p_len = P.size();int i = 0; // P 的下標int j = -1; nextval[0] = -1;while (i < p_len){if (j == -1 || P[i] == P[j]){i++;j++;if (P[i] != P[j])nextval[i] = j;elsenextval[i] = nextval[j]; // 既然相同就繼續往前找真前綴}elsej = nextval[j];} }??至此,關于 KMP 算法,我們就介紹到這里了。
?
參考
- 嚴蔚敏. 數據結構(C 語言版)
- 阮一峰.?字符串匹配的KMP算法
- ?https://blog.csdn.net/alps1992/article/details/47923041
總結
以上是生活随笔為你收集整理的字符串模式匹配——最长公共子序列与子串 KMP 算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 高级Java面试题,你敢挑战吗?
- 下一篇: Linux基础命令---mpstat显示