天天看點

KMP算法KMP算法前言1.KMP算法概述2.next數組3.KMP實作來自作者最真誠的吐槽

文章目錄

  • KMP算法
  • 前言
  • 1.KMP算法概述
  • 2.next數組
  • 3.KMP實作
    • KMP的簡單應用:
      • [J - 剪花布條](https://vjudge.net/problem/HDU-2087)
        • 題意:
        • 思路:
  • 來自作者最真誠的吐槽

KMP算法

字元串比對算法,是在實際工程中經常遇到的問題,也是各大公司筆試面試的常考題目。此算法通常輸入為原字元串(string)和子串(pattern),要求傳回子串在原字元串中首次出現的位置(做題的時候變化挺多的,基本上沒有隻傳回首次出現的位置,一般都是傳回所有的位置,或者出現的次數,還有讓你列印出next數組)。比如原字元串為“ABCDEFG”,子串為“DEF”,則算法傳回3。常見的算法包括:BF(Brute Force,暴力檢索)、RK(Robin-Karp,哈希檢索)、KMP(教科書上最常見算法)、BM(Boyer Moore)、Sunday等。今天我來介紹一下KMP算法,以及基本的代碼實作和每一步的原理分析……

前言

我覺得KMP算法比較容易了解,你要是認真看了阮大神的KMP文章,基本上就明白他的原理了(懂是懂,但是看不懂算法?! ……)其實就有兩部分比較重要,第一個是next數組的建構,另一個就是如何實作他的字元比對。(我手推了一下下,發現十行代碼之間,KMP灰飛煙滅,屬實有點神奇!!!)

1.KMP算法概述

首先還是簡單介紹一下KMP與傳統的暴力比對算法的不同點在哪裡,難點在哪裡。

我們先來看一下暴力的字元串比對算法

// 暴力比對(僞碼)
int search(String pat, String txt) {
    int M = pat.length;
    int N = txt.length;
    for (int i = 0; i <= N - M; i++) {
        int j;
        for (j = 0; j < M; j++) {
            if (pat[j] != txt[i+j])
                break;
        }
        // pat 全都比對了
        if (j == M) return i;
    }
    // txt 中不存在 pat 子串
    return -1;
}
           

對于暴力算法,如果出現不比對字元,會同時回退

txt

pat

指針,時間複雜度為

O(MN)

,空間複雜度為

O(1)

,最主要的問題是,如果字元串中重複的字元較多,該算法就很蠢。

比如

tex

= ‘’ aaacaaab’’,

pat

= ‘‘aaab’’:

很顯然

pat

中跟本沒有c,導緻在c這裡比較了4次,根本沒有必要退回指針

i

,暴力算法很明顯做了不必要的操作。

KMP優秀就優秀在他花費了一些空間來記錄一些資訊,來解決上述的情況

比如

txt

= “aaaaaaab”,

pat

= “aaab”,暴力算法會和上面那個例子一樣蠢蠢的傳回指針i,而kmp算法就會耍小聰明

因為KMP算法知道b字元之前的a都是比對的,是以每次隻需比較b是否被比對即可

KMP永遠不會回退

txt

的指針,就像 我高中曉明老師說的好:不走回頭路。(就是不會重複掃描

txt

),而是借助next數組的資訊把

pat

移動到正确的位置繼續比對.

這個算法的難點在于如果計算next函數的資訊?如何根據這些資訊正确的移動

pat

的指針?

是以我們接下來來解決一下next數組

2.next數組

咱們再舉個栗子

txt

= “BBC ABCDAB ABCDABCDABDE”

pat

= “ABCDABD”

我們要求的是

pat

的next數組

首先next數組其實是字元串的部分比對表,由部分搜尋詞和部分比對值組成

搜尋詞就是

pat

的每一個元素

部分比對值就是字首和字尾的最長共有元素長度

在這之前,我們要先知道什麼是字首和字尾。

字首是指除了最後一個字元以外的,一個字元串的全部頭部組合;

字尾是指除了第一個字元以外的,一個字元串的全部尾部組合

比如字元串“bread”

字首:b, br, bre, brea

字尾:read, ead, ad,d

對于

ABCDABD

的部分比對表如下

  • "A"的字首和字尾都為空集,共有元素的長度為0;
  • "AB"的字首為[A],字尾為[B],共有元素的長度為0;
  • "ABC"的字首為[A, AB],字尾為[BC, C],共有元素的長度0;
  • "ABCD"的字首為[A, AB, ABC],字尾為[BCD, CD, D],共有元素的長度為0;
  • “ABCDA"的字首為[A, AB, ABC, ABCD],字尾為[BCDA, CDA, DA, A],共有元素為"A”,長度為1;
  • “ABCDAB"的字首為[A, AB, ABC, ABCD, ABCDA],字尾為[BCDAB, CDAB, DAB, AB, B],共有元素為"AB”,長度為2;
  • "ABCDABD"的字首為[A, AB, ABC, ABCD, ABCDA, ABCDAB],字尾為[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的長度為0。
KMP算法KMP算法前言1.KMP算法概述2.next數組3.KMP實作來自作者最真誠的吐槽

看到這裡你應該明白什麼是next數組了,但是你心中一定有個疑惑,我怎麼用代碼實作呢???

别急,聽我慢慢道來

你先觀察一下上面那個ABCDABD,你會發現第二個A出現的時候,其比對值就是1,而他後面的B如果和第一個A後面的字元相等,那他的比對值就是2,一看,确實是一樣的B,是以就變成了2。

其實是這麼一個過程:你是從第一個開始求,但是第一個字元隻有一個元素,故沒有字首和字尾,就是0,然後你就開始比較第二個,一看,A != B,是以也是0,再看三四,都是0,直到你看到了第五個,第五個的A == 第一個的A ,一看比對成功,我們就記錄第五個數的比對值是1。再就比較下一個字元,也就是第六個。這個時候細心的同學就會問:哎呀,你怎麼就判斷了這一個元素就判定他是1,而不去周遊看看他的所有字首字尾有沒有其他的一樣的,而且長度還超過1的?其實第i個是在第i- 1 的基礎上進行的,如果第i個與前面那個比對了,就在第i- 1的基礎上加1.等你看第6和7個字元就明白了,因為第五個字元與第一個字元比對了,我們就把第六個字元與第二個字元比較一下,實作了“移動”。這個時候發現第六個和第二個字元是相等的,是以第6個的部分比對值就在第五個的基礎上加1,1 + 1 = 2,就是2.。你寫一下想一想,就能明白是什麼意思。然後我們在看第七個與第三個比較,一看C != D,比對失敗,就為0.這個時候如果還有第八個元素,那第八是要和第一個重新比較的,因為第七個已經比對失敗,是以他的字首字尾共有元素最多就是1(當且僅當第八個和第一個比對成功)。

next構造的代碼奉上

int next1[1000005];
void getnext()//打表,構造出next數組(這也太神奇了吧這個法)
{
    int j, k;
    j = 0;
    k = -1;
    next1[0] = -1;//當字元串隻有一個元素的時候,就沒有字首和字尾,是以直接指派為0;
    while(j < m)//表面上看其實可以換成從j從1到m的for循環的,但實際上你要是手推一波你就會發現這個循環不止進行了m-1次,這就是神奇所在
    {
        if(k == -1 || ss[j] == ss[k])//這個ss[j] 其實就是到最後一個元素,而ss[k] 其實就是第k個元素,看看這兩個想不想等
            next1[++j] = ++k;
        else
            k = next1[k];
        /*這個就是用來清值的,因為每次求第j個的時候,如果不比對,就得清值,有可能求一個next1的值時不止用了一次這個玩意,很神奇*/
    }
}
           
KMP算法KMP算法前言1.KMP算法概述2.next數組3.KMP實作來自作者最真誠的吐槽

這個是我手推的一遍循環過程(不推不知道,一推才知道其中的奧秘)

3.KMP實作

這個地方就要看如何靈活運用next數組的資料進行跳躍了……

(你可以當做闆子背下來)

void kmp()
{
    int ans = 0;//用來記錄出現幾次
    int i = 0;//i是用來記錄被搜字元串的位置
    int j = 0;//j是用來記錄要搜的字元串的位置
    getnext();
    for(int i = 0; i < n; i++)
    {
        while(j > 0 && s[i] != ss[j])//就在這一步,實作了跳躍!,因為j = next[j],進而在i的地方進行s[i]與ss[j]的新的比較
            j = next1[j];
        if(s[i] == ss[j])//主要是用來找第一次比對的位置
            j++;
        if(j == m)//當找到一次以後就輸出這個位置
        {
            ans ++;//找到一次就直接++,計算次數用
            cout<<i - m + 2 <<'\n';//這個地方是輸出位置的,不知道的話就直接背過就完事了了,咱也不知道為什麼要加2,一遍遍試出來的……
            j = next1[j];//再重新整理j的值,好找下一個,因為最後一個數的next值肯定為0,是以賦它
        }
    }
}
           

下面是一到洛谷的KMP模闆題

題意大概是這樣的:給你兩個字元串s1,s2.

讓你按從小到大的順序輸出s2在s1中的位置

在輸出一行整數,第i個整數代表s2的長度為i的字首的最長長度(其實就是讓你列印next數組的值)

下面是AC碼

#include<bits/stdc++.h>
using namespace std;

int next1[1000005];
string s, ss;
int n, m;

void getnext()
{
    int j, k;
    j = 0;
    k = -1;
    next1[0] = -1;
    while(j < m)
    {
        if(k == -1 || ss[j] == ss[k])
            next1[++j] = ++k;
        else
            k = next1[k];
    }
}
void kmp()
{
    int i = 0;
    int j = 0;
    getnext();
    for(int i = 0; i < n; i++)
    {
        while(j > 0 && s[i] != ss[j])
            j = next1[j];
        if(s[i] == ss[j])
            j++;
        if(j == m)
        {
            cout<<i - m + 2 <<'\n';
            j = next1[j];
        }
    }
}

int main()
{
    cin>>s>>ss;
    n = s.size();
    m = ss. size();
    kmp();
    for(int i = 1; i <= m; i++)
    {
        cout<<next1[i]<<' ';
    }
    return 0;
}

           

代碼的解釋都在上面寫了

KMP的簡單應用:

J - 剪花布條

題意:

給了兩個字元串a,b。讓你輸出a中有幾個完整的b

思路:

可以用KMP算法統計,也可以直接用find函數找,然後找到了就可以把這之前的所有字元删除

#include <bits/stdc++.h>
using namespace std;
int next1[1000005];
string s, ss;
int n, m, ans ;
void getnext()
{
    int j, k;
    j = 0;
    k = -1;
    next1[0] = -1;
    while(j < m)
    {
        if(k == -1 || ss[j] == ss[k])
            next1[++j] = ++k;
        else
            k = next1[k];
    }
}
int kmp()
{
    ans = 0;//用來記錄出現幾次
    int i = 0;//i是用來記錄被搜字元串的位置
    int j = 0;//j是用來記錄要搜的字元串的位置
    getnext();
    while(i <= n && j <= m)
    {
        if(j == m)//找到了一次s2,就讓ans++
        {
            ans ++;
            j = 0;
            continue;
        }
        if(j == -1 || s[i] == ss[j])//i與j處字元比對成功
        {
            i++;
            j++;
        }
        else//比對不成功就利用next數組來跳躍
            j = next1[j];
    }
    return ans;
}
int main()
{
    while(cin>>s)
    {
        if(s[0] == '#')
            break;
        else
        {
            cin>>ss;
            n = s.size();
            m = ss. size();
            cout<<kmp()<<endl;
        }
    }
    return 0;
}

           

來自作者最真誠的吐槽

之前也學了一遍KMP,但是沒靜下心來去了解,就當做闆子直接套(也不知道為什麼,我明明記得我在oj上面做了一個KMP的題,還wa了,清清楚楚的,但是現在就是找不到,難道我活在夢裡??!!),現在真正了解了,才知道他有多神奇多牛。還有那個洛谷的題,千萬記得寫函數用int時,得加傳回值,不然就會RE,淦!(你說這直接用void他不香嘛)我差點到死都不知道哪裡錯了,還好有ycgg告訴我了。

昨天下午學了個markdown,晚上又學KMP,從六點在集訓室,到回宿舍繼續幹,一直幹到了零點多,今天上午可算是在圖書館用markdown語言寫出來了這篇KMP算法的博文,屬實不容易……

不得不說,typora真滴好用,太喜歡這種簡約的風格了,吹爆typora……

(好奇怪啊,我導入到csdn的時候,外部的照片就會丢失,果然得去設定一下typora的圖檔的存儲……)

繼續閱讀