日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

字符串模式匹配——最长公共子序列与子串 KMP 算法

發(fā)布時間:2025/3/15 编程问答 16 豆豆
生活随笔 收集整理的這篇文章主要介紹了 字符串模式匹配——最长公共子序列与子串 KMP 算法 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

最長公共子序列

最長公共子序列的問題很簡單,就是在兩個字符串中找到最長的子序列,這里明確兩個含義:

  • 子串:表示連續(xù)的一串字符 。
  • 子序列:表示不連續(xù)的一串字符。
  • 所以這里要查找的是不連續(xù)的最長子序列,

    動態(tài)規(guī)劃

    這里為什么要使用動態(tài)規(guī)劃可以說一下,簡單來說動態(tài)規(guī)劃是為了降低時間復(fù)雜度的一種算法,申請一個額外空間,來保存每一個步驟的結(jié)果,最后從這些結(jié)果中找到最優(yōu)的解。

    這里有個問題就是:一般來說,當(dāng)前的最優(yōu)解,只與當(dāng)前時刻和上一時刻有關(guān)系,和其他時刻沒有關(guān)系,這樣才能讓動態(tài)規(guī)劃發(fā)生作用,降低復(fù)雜度。

    分析LCS

    其實LCS看起來很麻煩,找不到思路,如果暴力破解可能要O(n^4)了,而這個題目使用動態(tài)規(guī)劃的思想也非常簡單,為何相比之前的問題不好找思路呢?

    是因為之前的動態(tài)規(guī)劃問題例如:背包問題,生產(chǎn)線問題,都是一維數(shù)組空間內(nèi)的結(jié)果,規(guī)劃到一個線性時間內(nèi),而這個題目需要O(m*n)的時間復(fù)雜度和空間復(fù)雜度。

    所以其實就是進行m*n次對比,每次保存當(dāng)前的最優(yōu)解,就可以了。

    代碼實現(xiàn)分析

    這里有個問題,就是我們需要的結(jié)果僅僅是長度? 還是包括這個序列串一起輸出。

    看下面圖:

    這里可以看到,我們構(gòu)造的一個i*j的矩陣,這個矩陣?yán)锏膬?nèi)容不但包括數(shù)值(當(dāng)前結(jié)果的最優(yōu)解),還包括一個方向箭頭,這個代表了我們回溯的時候,需要行走的方向。

    所以我們這里保存兩個值,可以使用兩個二維矩陣,也可以使用一個結(jié)構(gòu)體矩陣。

    解法分析

    其實這個題目在動態(tài)規(guī)劃來理解,也非常簡單。一個狀態(tài)轉(zhuǎn)移函數(shù)。

    這個非常好理解,其中一個字符串為0的時候,那么肯定是0了。

    當(dāng)兩個字符相等的時候,這個時候很好理解,舉例來說:

    abcd?和?adcd,在遍歷c的時候,發(fā)現(xiàn)前面只有a相等了,也就是1.?
    那么c相等,也就是abc和adc在匹配的時候,一定比ab和ad的長度大1,這個1就是c相等么。也就是相等的時候,是比c[i-1][j-1]大1的。

    下一個更好理解了,如果不相等,肯定就是找到上一個時刻對比最大的么。

    代碼

    這個代碼只輸出了LCS的長度,而結(jié)果數(shù)組的方向我已經(jīng)存儲好了,想要遍歷的,直接從后向前遍歷數(shù)組就可以輸出最長公共子序列了,即哪些 direct = 0 的節(jié)點。

    // // main.cpp // LCS // // 最長公共子序列(LCS) //#include <iostream>using namespace std;/* * 這里可以不定義長度,輸入的字符串用string存儲,然后利用string.c_str()來對字符串進行數(shù)組轉(zhuǎn)化。 我這里為了方便沒有這樣做。 */ #ifndef MAX_LENGTH #define MAX_LENGTH 15 //定義字符串最大長度 #endifint MaxNum(int firstNum, int secondNum){return firstNum > secondNum ? firstNum : secondNum; }//定義數(shù)組結(jié)構(gòu)體 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; //設(shè)置所有默認(rèn)的最長為0resultMatrix[i][j].direct = 1; //所有默認(rèn)方向變成上 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; }

    ?

    上面我們介紹了最長公共子序列,下面我們來介紹一種子串(連續(xù)字符)的模式匹配問題,以及 BF 算法和 KMP 算法。


    KMP 算法背景

    給定一個主串(以 S 代替)和模式串(以 P 代替),要求找出 P 在 S 中出現(xiàn)的位置,此即串的模式匹配問題。

    Knuth-Morris-Pratt 算法(簡稱 KMP)是解決這一問題的常用算法之一,這個算法是由高德納(Donald Ervin Knuth)和沃恩·普拉特在 1974 年構(gòu)思,同年詹姆斯·H·莫里斯也獨立地設(shè)計出該算法,最終三人于 1977 年聯(lián)合發(fā)表。

    在繼續(xù)下面的內(nèi)容之前,有必要在這里介紹下兩個概念:真前綴?和?真后綴

    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

    由上圖所得, "真前綴" 指除了自身以外,一個字符串的全部頭部組合;"真后綴" 指除了自身以外,一個字符串的全部尾部組合。于是得到兩個集合,而我們后面的 next 數(shù)組的元素便是這兩個集合之交中最長字符串元素的長度。而 KMP 算法便是根據(jù)next數(shù)組中的值來減少回溯重復(fù)比較字符而提高算法的效率的。

    ?

    暴力樸素字符串匹配?BF 算法


    初遇串的模式匹配問題,我們腦海中的第一反應(yīng),就是樸素字符串匹配?BF(Brute Force)(即所謂的暴力匹配),代碼如下:

    /* 字符串下標(biāo)始于 0 */ int strFind(const char *str, const char *substr) {assert(str != NULL && substr != NULL);int m = strlen(str), n = strlen(substr);if(m < n) return -1for(int i=0; i <= m-n; ++i){for(int j=0; j < n; ++j){if(str[i+j] != substr[j])break;}if(j == n) // 匹配成功return i;}return -1; }

    暴力匹配的時間復(fù)雜度為?O( (m-n+1) * n),其中?n?為 S 的長度,m?為 P 的長度。很明顯,這樣的時間復(fù)雜度很難滿足我們的需求。

    接下來進入正題:時間復(fù)雜度為?Θ(n+m)?的 KMP 算法。

    ?

    KMP字符串匹配算法

    簡言之,以圖中的例子來說,在 i 處失配,那么主字符串和模式字符串的前邊6位就是相同的。又因為模式字符串的前6位,在前 6 位中我們發(fā)現(xiàn)其??"真前綴" 和 "真后綴" 的最長的相同真前后綴的長度是 4,它的前4位前綴和后4位后綴是相同的,所以我們推知主字符串i之前的4位和模式字符串開頭的4位是相同的。就是圖中的灰色部分。那這部分就不用再比較了。而當(dāng)我們回溯時,此時 i = 6,j = 4,我們保持 i = 6, 不變。而將模式串向右滑動,即調(diào)整 j 的值,而湊巧我們知道其 next[ i=4 ] 此時的值,代表最長的相同真前后綴長度,就等于 4。于是我們果斷將 j 更新回溯為 j = 4, i 不變,i = 6。得到如圖(b)的狀態(tài)。

    ? ? ? ? ? ? ? ? ? ? ? ??

    有了上面的分析,我們先假設(shè)已經(jīng)有了獲取 next 數(shù)組的函數(shù),在不分析其具體實現(xiàn)的抽象之上便可以構(gòu)建起 KMP 算法的框架,如下:

    /* 在 S 中找到 P 第一次出現(xiàn)的位置 */ int KMP(string S, string P, int *next) {GetNext(P, next);int i = 0; // S 的下標(biāo)int j = 0; // P 的下標(biāo)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]; // 當(dāng)前字符匹配失敗,進行跳轉(zhuǎn)}if (j == p_len) // 匹配成功return i - j;return -1; }

    注意,因為作為模式串如果只有一個字符,則它肯定沒有其相等的最長真前后綴。故我們會將 next [0] = -1 即第一位的值設(shè)為 -1。所以在上述的代碼中 if 條件中的表達(dá)式 j == -1 是有可能通過 else 語句中的 next[j] 賦值而致使其成立的。此時表示其第一個字符不匹配。注意,每一個 next[j] 的值都是依賴從模式串中的 [0 ... j-1] 中的相等的最長真前后綴的長度決定的。


    于是我們便可以的出求取 next 數(shù)組的第一種方法

    /* P 為模式串,下標(biāo)從 0 開始 */ void GetNext(string P, int *next) {int p_len = P.size();int i = 0; // P 的下標(biāo)int j = -1; next[0] = -1;while (i < p_len){if (j == -1 || P[i] == P[j]){++i;++j;next[i] = j;}elsej = next[j];} }

    KMP優(yōu)化

    其實在上面的求取 next 數(shù)組中,在 ++i, ++j 之后并沒有判斷 p[i] 是否與 p[j] 相同,故仍會出現(xiàn)有冗余的比較字符的情況,于是就有了下面的求取 next 數(shù)組的改進算法。使用這種方式求取的 next 數(shù)組,整合到 KMP 中就稱為 kMP 的優(yōu)化情況。如下所示:

    void GetNextval(string P, int *nextval) {int p_len = P.size();int i = 0; // P 的下標(biāo)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]; // 既然相同就繼續(xù)往前找真前綴}elsej = nextval[j];} }

    ??至此,關(guān)于 KMP 算法,我們就介紹到這里了。

    ?

    參考

    • 嚴(yán)蔚敏. 數(shù)據(jù)結(jié)構(gòu)(C 語言版)
    • 阮一峰.?字符串匹配的KMP算法
    • ?https://blog.csdn.net/alps1992/article/details/47923041

    總結(jié)

    以上是生活随笔為你收集整理的字符串模式匹配——最长公共子序列与子串 KMP 算法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。