串的匹配

1.調用庫函數
從0開始查找第一次出現目標字符或者串的位置,找到返回位置,未找到返回string::npos。

#include <iostream>
#include <string>
using namespace std;
int main(){
	string str1="Alpha Beta Gamma Delta";
    int loc = str1.find('t');//查找單個字符,從0開始 
    int loc2=str1.find("ph");//查找字符串 
    if( loc != string::npos )
      cout << "Found Be at " << loc << endl;//輸出字符位置 
    else
      cout << "Didn't find Be" << endl;
    if( loc2 != string::npos )//輸出字符串位置 
      cout << "Found ph at " << loc2 << endl;
    else
      cout << "Didn't find ph" << endl;

	return 0;
}

2.簡單匹配
從主串的第一位起和模式串的第一個字符開始比較,如果相等,則繼續逐一比較後續字符;否則從主串的第二個字符開始重新比較,以此類推,直到比較完模式串的所有字符,匹配成功返回模式串的位置,不成功返回“0”。

#include <iostream>
#include <string>
using namespace std;
//簡單匹配 
int easy_find(string str,string substr){
	int i=0,j=0,k=i;//k記錄主串匹配位置
	while(i<str.length()&&j<substr.length()){
		if(str[i]==substr[j]){
			++i;
			++j;
	}else{
		j=0;//子串回首位 
		i=++k;//主串右移 
	}
	} 
	if(j==substr.length()){
		return k;//返回位置 
	}else{
		return 0;//未找到 
	}
}
int main(){
	string s="abdefg";
	string subs1="ef";
	string subs2="deg";
	cout<<easy_find(s,subs1)<<endl;
	cout<<easy_find(s,subs2)<<endl;
	return 0;
}

3.KMP算法
(1)
在這裏插入圖片描述
首先,主串"BBC ABCDAB ABCDABCDABDE"的第一個字符與模式串"ABCDABD"的第一個字符,進行比較。因爲 B 與 A 不匹配,所以模式串後移一位。

(2)在這裏插入圖片描述因爲 B 與 A 又不匹配,模式串再往後移。

(3)在這裏插入圖片描述

就這樣,直到主串有一個字符,與模式串的第一個字符相同爲止。

(4)在這裏插入圖片描述

接着比較主串和模式串的下一個字符,還是相同。

(5)在這裏插入圖片描述

直到主串有一個字符,與模式串對應的字符不相同爲止。

(6)在這裏插入圖片描述

這時,最自然的反應是,將模式串整個後移一位,再從頭逐個比較。這樣做雖然可行,但是效率很差,因爲你要把"搜索位置"移到已經比較過的位置,重比一遍。

(7)在這裏插入圖片描述

一個基本事實是,當空格與 D 不匹配時,你其實是已經知道前面六個字符是"ABCDAB"。KMP 算法的想法是,設法利用這個已知信息,不要把"搜索位置"移回已經比較過的位置,而是繼續把它向後移,這樣就提高了效率。
(8)在這裏插入圖片描述
怎麼做到這一點呢?可以針對模式串,設置一個跳轉數組int next[],這個數組是怎麼計算出來的,後面再介紹,這裏只要會用就可以了。
(9)在這裏插入圖片描述
已知空格與 D 不匹配時,前面六個字符"ABCDAB"是匹配的。根據跳轉數組可知,不匹配處 D 的 next 值爲 2,因此接下來從模式串下標爲 2 的位置開始匹配。
(10)在這裏插入圖片描述
因爲空格與 C 不匹配,C 處的 next 值爲 0,因此接下來模式串從下標爲 0 處開始匹配。
(11)在這裏插入圖片描述
因爲空格與 A 不匹配,此處 next 值爲 -1,表示模式串的第一個字符就不匹配,那麼直接往後移一位。
(12)在這裏插入圖片描述
逐位比較,直到發現 C 與 D 不匹配。於是,下一步從下標爲 2 的地方開始匹配。
(13)在這裏插入圖片描述
逐位比較,直到模式串的最後一位,發現完全匹配,於是搜索完成。
next數組:

next 數組的求解基於“真前綴”和“真後綴”,即next[i]等於P[0]…P[i - 1]最長的相同真前後綴的長度(請暫時忽視 i 等於 0 時的情況,下面會有解釋)。
在這裏插入圖片描述

i = 0,對於模式串的首字符,我們統一爲next[0] = -1;
i = 1,前面的字符串爲A,其最長相同真前後綴長度爲 0,即next[1] = 0;
i = 2,前面的字符串爲AB,其最長相同真前後綴長度爲 0,即next[2] = 0;
i = 3,前面的字符串爲ABC,其最長相同真前後綴長度爲 0,即next[3] = 0;
i = 4,前面的字符串爲ABCD,其最長相同真前後綴長度爲 0,即next[4] = 0;
i = 5,前面的字符串爲ABCDA,其最長相同真前後綴爲A,即next[5] = 1;
i = 6,前面的字符串爲ABCDAB,其最長相同真前後綴爲AB,即next[6] = 2;
i = 7,前面的字符串爲ABCDABD,其最長相同真前後綴長度爲 0,即next[7] = 0。

那麼,爲什麼根據最長相同真前後綴的長度就可以實現在不匹配情況下的跳轉呢?舉個代表性的例子:假如i = 6時不匹配,此時我們是知道其位置前的字符串爲ABCDAB,仔細觀察這個字符串,首尾都有一個AB,既然在i = 6處的 D 不匹配,我們爲何不直接把i = 2處的 C 拿過來繼續比較呢,因爲都有一個AB啊,而這個AB就是ABCDAB的最長相同真前後綴,其長度 2 正好是跳轉的下標位置。

有的讀者可能存在疑問,若在i = 5時匹配失敗,按照我講解的思路,此時應該把i = 1處的字符拿過來繼續比較,但是這兩個位置的字符是一樣的啊,都是B,既然一樣,拿過來比較不就是無用功了麼?其實不是我講解的有問題,也不是這個算法有問題,而是這個算法還未優化,關於這個問題在下面會詳細說明,不過建議讀者不要在這裏糾結,跳過這個,下面你自然會恍然大悟。

思路如此簡單,接下來就是代碼實現了,如下:

/* P 爲模式串,下標從 0 開始 */
void GetNext(string P, int next[])
{
    int p_len = P.size();
    int i = 0;   // P 的下標
    int j = -1;  
    next[0] = -1;

    while (i < p_len)
    {
        if (j == -1 || P[i] == P[j])
        {
            i++;
            j++;
            next[i] = j;
        }
        else
            j = next[j];
    }
}

一臉懵逼,是不是。。。上述代碼就是用來求解模式串中每個位置的next[]值。

下面具體分析,我把代碼分爲兩部分來講:

(1)i 和 j 的作用是什麼?

i 和 j 就像是兩個”指針“,一前一後,通過移動它們來找到最長的相同真前後綴。

(2)if…else語句裏做了什麼?
在這裏插入圖片描述

假設 i 和 j 的位置如上圖,由next[i] = j得,也就是對於位置 i 來說,區段 [0, i - 1] 的最長相同真前後綴分別是 [0, j - 1] 和 [i - j, i - 1],即這兩區段內容相同。

按照算法流程,if (P[i] == P[j]),則i++; j++; next[i] = j;;若不等,則j = next[j],見下圖:
在這裏插入圖片描述
next[j]代表 [0, j - 1] 區段中最長相同真前後綴的長度。如圖,用左側兩個橢圓來表示這個最長相同真前後綴,即這兩個橢圓代表的區段內容相同;同理,右側也有相同的兩個橢圓。所以 else 語句就是利用第一個橢圓和第四個橢圓內容相同來加快得到 [0, i - 1] 區段的相同真前後綴的長度。

細心的朋友會問 if 語句中j == -1存在的意義是何?第一,程序剛運行時,j 是被初始爲 -1,直接進行P[i] == P[j]判斷無疑會邊界溢出;第二,else 語句中j = next[j],j 是不斷後退的,若 j 在後退中被賦值爲 -1(也就是j = next[0]),在P[i] == P[j]判斷也會邊界溢出。綜上兩點,其意義就是爲了特殊邊界判斷。

完整代碼:

#include <iostream>
#include <string>

using namespace std;

/* P 爲模式串,下標從 0 開始 */
void GetNext(string P, int next[])
{
    int p_len = P.size();
    int i = 0;   // P 的下標
    int j = -1;  
    next[0] = -1;

    while (i < p_len)
    {
        if (j == -1 || P[i] == P[j])
        {
            i++;
            j++;
            next[i] = j;
        }
        else
            j = next[j];
    }
}

/* 在 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++;
        }
        else
            j = next[j];  // 當前字符匹配失敗,進行跳轉
    }

    if (j == p_len)  // 匹配成功
        return i - j;
    
    return -1;
}

int main()
{
    int next[100] = { 0 };

    cout << KMP("bbc abcdab abcdabcdabde", "abcdabd", next) << endl; // 15
    
    return 0;
}

優化:

/* P 爲模式串,下標從 0 開始 */
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;
            else
                nextval[i] = nextval[j];  // 既然相同就繼續往前找真前綴
        }
        else
            j = nextval[j];
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章