1. 程式人生 > 實用技巧 >kmp字串匹配

kmp字串匹配

28. 實現 strStr()

難度簡單

實現strStr()函式。

給定一個haystack 字串和一個 needle 字串,在 haystack 字串中找出 needle 字串出現的第一個位置 (從0開始)。如果不存在,則返回-1。

示例 1:

輸入: haystack = "hello", needle = "ll"
輸出: 2

示例 2:

輸入: haystack = "aaaaa", needle = "bba"
輸出: -1

說明:

needle是空字串時,我們應當返回什麼值呢?這是一個在面試中很好的問題。

對於本題而言,當needle

是空字串時我們應當返回 0 。這與C語言的strstr()以及 Java的indexOf()定義相符。

通過次數220,386 提交次數555,007

實現strStr( )---小白做法

小天才釋出於10 天前578C++

解題思路

為大家推薦下面這個B站up主講的KMP演算法,超級好
kmp原理:https://www.bilibili.com/video/BV1Px411z7Yo?from=search&seid=13225444196686531503
kmp實現程式碼:https://www.bilibili.com/video/BV1hW411a7ys/?spm_id_from=333.788.videocard.0

程式碼

class Solution {
public:
    int strStr(string haystack, string needle) 
    {
        if(!needle.size()) return 0;
        if(!haystack.size()) return -1;
        //構造字首表
        int j = -1, i = 0;
        vector<int> b(needle.size() + 1);
        b[i] = j;
        while(i < needle.size())
        {
            while(j >= 0 && needle[i] != needle[j]) j = b[j];
            i++, j++;
            b[i] = j;
        }
        //KMP-search
        j = 0, i = 0; 
        while(j < haystack.size())
        {
            while(i >= 0 && needle[i] != haystack[j]) i = b[i];
            i++, j++;
            if(i == needle.size())
            {
                return j - needle.size();
            }
        }
        return -1;
    }
};

KMP:花48小時看懂了KMP,想讓你在48分鐘內看懂 EL1S

EL1S釋出於2020-03-137.7kC++

點開了無數個KMP的講解頁面,點開文章中的各種跳轉連結,居然被我發現了這樣一篇好文章,程式碼寫的好短,還有圖:https://www.inf.hs-flensburg.de/lang/algorithmen/pattern/kmpen.htm 結合自己的理解分享一下。

一、KMP的用途,作用物件

第一個問題:KMP是幹嘛用的?
KMP是用來找字串匹配的

第二個問題:為什麼要用KMP來找字串匹配?
KMP的時間複雜度是O(m + n),時間複雜度低

我們拿一道題來做例子Leetcode28.實現 strStr()

這道題能想到幾種方式?
第一種:遍歷haystack的每一個字元作為開頭,needle.size()為長度和needle做對比,一樣則返回這個開頭的idx
第二種: 列舉haystack的每一個字元作為開頭,neddle.size()作為長度,扔到map<string, int>裡面,去map裡面找有沒有needle
第三種:本文介紹的KMP

KMP的兩個物件是什麼:
第一個:pattern->也就是題目中的needle
第二個:text->也是就是題目中的haystack

二、KMP的流程要分為兩步:

第一步:構建一個數組b,大小是pattern.size() + 1,進行Preprocess預處理對陣列填充
第二步:把pattern和text進行匹配,利用b陣列加速匹配,找到那個出現的第一個位置的idx

2.1 構建陣列b

在構建b陣列之前,要讓你們認識到b陣列的必要性,也是就是它到底能怎麼幫助我們加速匹配?
text: ababbabaa
pattern: ababac

不用加速的時候是這樣的(綠的匹配上了,紅的匹配失敗,黑的還沒匹配):
每次遇到不匹配的時候呢,頭部就往右邊挪一個位置

用加速的時候是這樣的:
每次遇到不匹配挪動的位置可能就不止一步了! 注意注意:不止一步不止一步!!上面那個是慢走大路,這個就是抄近路。這裡上面的圖和下面的圖我都是畫了5個step,似乎並沒有加速?這個例子呢是因為字元長度比較短,對於字串長度長的效果會很明顯。
下面這張圖我是為了說明兩個道理:1. 它不是一步一步的挪動的,是跳著挪動的!所以會快,因為不用匹配那麼多次了呀;2. 而且仔細看,下面這些圖有一些紅色之前還是黑色!這些也是沒有去重複匹配的,但是上面的圖紅色之前都是綠色,說明都有遍歷匹配!

好了,現在我們知道是可以這樣加速的,我們就有加速的rules!這些rules就存在前文提及到的陣列b裡面,也是就是說啊,當我pattern和text發生不匹配的時候啊,我要怎麼往前跳一下?
現在我們的問題是怎麼構建這個陣列b?
讓我們先來觀察一下這個跳動是怎麼跳的,為什麼要這樣跳?

一開始的時候是text[0-3] = "abab" 匹配上了pattern[0-3] = "abab",但是在text[4]和pattern[4]匹配的時候發現了不匹配,可惜之前匹配了那麼一大串了,於是就有一個想法:能不能把之前匹配過的利用起來?
我們把pattern進行滑動看看效果:

這張圖應該讓你明白是想要怎麼保留利用之前匹配過了的,那麼現在該看看這個b怎麼建立了
b這個陣列的大小是pattern.size() + 1,數組裡面存的東西的含義是,假設text[k]和pattern[i]沒有匹配上,i要進行更新,更新成i = b[i], 然後再去text[k]和pattern[i]進行對比。

建立這個b的程式碼是:

int j = -1, i = 0;//j在後面,i在前面
vector<int> b(needle.size() + 1);
b[i] = j;
while(i < needle.size())
{
    while(j >= 0 && needle[i] != needle[j]) j = b[j];
    i++, j++;
    b[i] = j;
}

我給一個例子,搭配著圖解釋:

一開始,把j初始化為-1, i初始化為0,b[0]初始化為-1,為什麼要這樣初始化後面會講,先跳過看一下效果。
j >= 0不成立,所以沒有進入while迴圈,i++ = 1 j++ = 0 b[1] = 0;是不是看了這波操作有一點懵圈?讓我來理一下思路,這段程式碼在填充b的時候就是:背靠著山,未雨綢繆。意思是i = 0先保證了自己的匹配成功,然後去為i = 1考慮:若pattern在i = 1的時候和text匹配不上了,保留一段已經匹配過的,然後讓pattern的哪個idx的字元接著和text去匹配

看回我們之前的例子:

這裡不就是i = 4的時候pattern和text匹配不上了,然後pattern保留了一段(即"ab"),然後讓pattern idx = 2的字元接著和text進行匹配
再來看看我們現在這個例子:

就是i = 0的時候就去為i = 1做打算,如果說pattern[i = 1]和text不匹配了,i就不能是1了該變到幾?答案是0,去和pattern[i = 0]匹配吧
這兩個指標i, j,會努力維持pattern[i] = pattern[j],如果實在維持不了的話,j就是-1了; 因為程式碼裡面有那個i++, j++,所以下一個i = 1其實要是j = 0去匹配

繼續看,現在是i = 1,j = 0,發現居然匹配不上,j很生氣,就往左邊跑,j = b[j = 0] = -1,然後i++ = 2, j++ = 0, 這裡就是當i = 1的時候在為i = 2考慮啊了,i = 1先把自己匹配上,匹配不上j就到-1,然後i = 1告訴i = 2:b[2] = 0,意思是i = 2啊要是你pattern[i = 2]沒有和text匹配上的話你去pattern[b[2]] = pattern[0]看看能不能匹配上。

下一步是i = 2, j = 0;哈哈哈,有意思,我pattern[i]和pattern[j]一樣的,於是i = 2告訴i = 3:我i = 2是匹配了成了的,你要是沒和text匹配上,你就去和pattern[j + 1 = 1]匹配吧,因此b[i = 3] = j + 1 = 1

再下一步,i = 3, j = 1;pattern[i] = pattern[j],匹配上了哦,i = 3告訴i = 4: b[i= 4] = j + 1 = 2
接著,i = 4, j = 2, pattern[i] = pattern[j],嗯,很好,i = 4自己匹配上了,於是告訴i = 5: 你要是沒和text匹配上啊你就和pattern[3]匹配去吧,故b[i = 5] = 3

到了i = 5, j = 3; i = 5要讓自己先匹配,然後為i = 6做打算,呀pattern[i]和pattern[j]居然不匹配,說明啊,i = 5得找到前面的一個j和自己有相同的prefix這樣呢,才能為i = 6做打算,那麼怎麼找前面的j呢?i = 5心裡思考:i= 4和我說了要是我匹配不上的話我可以往前, b[j = 3] = 1,於是i = 5就去和pattern[j = 1]做這個對比了,發現還不一樣,只好再往前了,j = b[j = 1] = 0, pattern[0] = 'a' = pattern[5],好了終於相同了,可以為i = 6做謀劃了,i = 6就是要走i = 5的後塵下一步,b[6] = j + 1 = 0 + 1 = 1

2.2 利用陣列b

b陣列構建好了,到了KMP的第二大步驟了:把b陣列用起來!

j = 0, i = 0; //j這回是text的, i是pattern的
while(j < haystack.size())
{
    while(i >= 0 && needle[i] != haystack[j]) i = b[i];
    i++, j++;
    if(i == needle.size())
    {
        return j - needle.size();
    }
}
return -1;

i是指向pattern的指標,j是指向text的指標,如果text[j] == pattern[i],那就i++, j++繼續向後比對就是了,

如果text[j] != pattern[i]呢,這個時候就是pattern開始跳躍的時候了, text[j] != pattern[i]是吧,pattern說:那我看看我的rules記錄本b,b[i]告訴我我要跳到這個位置,好,於是i = b[i]; text[j]再和pattern[i]比過,如果相同那麼就繼續向後,不過不同,那i繼續向前跳,如果i都跳到-1了都沒有找到pattern[i] = text[j],說明啊這個text[j]我的pattern的字首裡面就是沒有合適的,那行吧,j你加一吧,我i從0開始重新開始匹配。

三、題目KMP解法程式碼

class Solution {
public:
    int strStr(string haystack, string needle) {
        if(!needle.size()) return 0;
        if(!haystack.size()) return -1;
        //先構造pattern
        int j = -1, i = 0;//j在後面,i在前面
        vector<int> b(needle.size() + 1);
        b[i] = j;
        while(i < needle.size())
        {
            while(j >= 0 && needle[i] != needle[j]) j = b[j];
            i++, j++;
            b[i] = j;
        }
        
        j = 0, i = 0; //j這回是text的, i是pattern的
        while(j < haystack.size())
        {
            while(i >= 0 && needle[i] != haystack[j]) i = b[i];
            i++, j++;
            if(i == needle.size())
            {
                return j - needle.size();
            }
        }
        return -1;
    }
};