算法笔记(C++描述)——KMP算法

概述

   以发现者名字字母命名的KMP算法,是一种比BF算法(最朴素的串匹配算法)更高效的模式串匹配蛮力算法。

预备概念

 

1.主串:即将在其中进行匹配子串查找的字符串。

2.模式串:用来在主串中作为匹配时的参照模板的字符串。

3.模式串匹配算法:用来在主串中找到一个与模式串完全相同的子串的算法,找到即匹配成功,反之匹配失败。

4.BF算法:一种普通的模式串算法,时间效率为O(n^m) (其中m为主串长度,n为模式串长度),算法思想是:每当发生一轮匹配失败时,主串的指针直接回溯到本轮匹配的起始位置的下一个,模式串指针回溯到头部,开始下一轮匹配,直到匹配成功,或者以下一个位置为首字符的最长子串都比模式串长度要短时,匹配结束。

 

KMP算法主要思想

     沿用BF算法的匹配流程,仍是主串指针与模式串指针不停移动做匹配。但当发生匹配失败时,主串指针不回溯,模式串指针部分回溯。从而使算法的时间效率变高。

 

KMP算法可行性的推导(含思路过程)

   设主串为S,主串上的当前指针位置为i;模式串为T,模式串上当前指针位置为j。假设在当前各指针位置下,本轮匹配失败(即S[i] != T[j]),设模式串指针要回退到的位置为k,则下一轮匹配从S[j+1]和T[k]开始。

   这里先假设算法成立,如果能求出k,则说明每次回退到的位置k是可计算的,即这个算法的思想是可行的,否则这个算法就是错误的。

   首先,因为模式串回退到位置为k开始进行下一轮匹配,则说明下一轮的时候,默认从T[0]~T[K-1]都是已经成功匹配了的,则有

           ① T[0] ~ T[k-1] = S[i-k] ~ S[i-1]

  这里再观察S[i-k] ~ S[i-1],共k个字符。(没错,这里要“拍脑门”想到)这主串S[i]之前的k个字符在本轮匹配中也与模式串j位置之前的前k个字符匹配。即得到:

          ② S[i-k] ~ S[i-1] = T[j-k] ~ T[j-1]

   然后联立等式就得到了最重要的式子

          ③T[0]~T[k-1] = T[j-k] ~ T[j-1]

   因为算法要通过尽可能少地回退来减少比较次数,提高时间效率。所以回退后的位置k的值越大,说明回退的距离越小。由③得到的结论是:

      每轮要回退到的最大位置k就是模式串T的从0到(本轮匹配失败位置i之前)i-1子串中最长相等前后缀的长度

     没错,记就要记核心,KMP算法的核心我认为就是这个 最长相等前后缀的长度。而且注意这只与模式串有关,与主串没有关系。而一个子串的相等前后缀是可以很简单求出的。因此该算法成立。

 

KMP时间复杂度分析

      上面的可行性推导,说明KMP算法是可以实现的,但是是否真的能达到高效的初衷,则要另外分析。

       根据算法思想我们知道:主串在整个过程中只扫描一遍,不回退。模式串第 i 轮进行的比较次数为  ki。

      时间复杂度为:O(Σki)  (i从1到主串的长度 m)

      而 ki 可能的取值只有n个,因为模式串T只有n种匹配失败的情况。

      那么,时间复杂度的关键,就是求ki的算法的时间复杂度。即求  一个字符串   所有前缀子串中     最长相等前后缀子串长度   的算法时间复杂度。(说得比较绕口,注:“前缀子串”就是指从T[0]到T[i]构成的子串  0

 

求字符串相等前后缀长度k的算法介绍

     这里只给出一个不用太动脑筋,设要求的字符串长度为n,则就是分别取长度1、2、3 ......n的子串来比较,判断是否有最长的相等前后缀。

     这里直接给出C++代码

int* getLengthsOfLongestPreAndSuf(string s)
{
    int* result = new int[s.size()];
    result[0] = -1;//当第一个元素就匹配失败时,不仅模式串指针要回溯到0,主串指针还应当后移一位,因此这种情况要特殊处理,赋值为-1
    for (int i = 1; i < s.size(); i++)//当第i个位置匹配失败时,求应回溯到的位置k
    {
        string sub = s.substr(0, i);//此时的子串
        for (int j = sub.size() - 1; j >= 0; j--)
        {
            int font = 0, back = sub.size() - j;
            while (s[font] == s[back] && back < sub.size())
            {
                font++;
                back++;
            }
            if (back == sub.size())
            {
                result[i] = j;
                break;//如果该长度下的前后缀子串相等,则提前跳出循环
            }
        }
    }
    return result;
}

    根据代码可以看出,这种方法的时间复杂度为O(m^3)

   

完整代码

#include
#include
using namespace std;

int main(void)
{
    int KMP(string S, string T);
    
    string S, T;
    cout << "请输入主串:";
    cin >> S;
    cout << "请输入模式串 : ";
    cin >> T;
    
    int result = KMP(S, T);
    if (result == -1)
        cout << "匹配失败" << endl;
    else
        cout << "匹配成功,模式串在主串中出现的位置为:" << result << endl;

    cout << endl;
    system("pause");
    return 0;
}

int KMP(string S, string T)
{
    int* getLengthsOfLongestPreAndSuf(string s);
    int* next = getLengthsOfLongestPreAndSuf(T);
    int i = 0, j = 0;
    while (S.size() - i >= T.size() - j && j < T.size())
    {
        if (S[i] != T[j])
        {
            if (next[j] == -1)
            {
                i++;
                j = 0;
            }
            else {
                j = next[j];
            }
        }
        else {
            i++;
            j++;
        }
    }
    if (j == T.size())
        return i - T.size();
    return -1;
}

int* getLengthsOfLongestPreAndSuf(string s)
{
    int* result = new int[s.size()];
    result[0] = -1;//当第一个元素就匹配失败时,不仅模式串指针要回溯到0,主串指针还应当后移一位,因此这种情况要特殊处理,赋值为-1
    for (int i = 1; i < s.size(); i++)//当第i个位置匹配失败时,求应回溯到的位置k
    {
        string sub = s.substr(0, i);//此时的子串
        for (int j = sub.size() - 1; j >= 0; j--)
        {
            int font = 0, back = sub.size() - j;
            while (s[font] == s[back] && back < sub.size())
            {
                font++;
                back++;
            }
            if (back == sub.size())
            {
                result[i] = j;
                break;//如果该长度下的前后缀子串相等,则提前跳出循环
            }
        }
    }
    return result;
}

 

   测试示例

    算法笔记(C++描述)——KMP算法_第1张图片

     算法笔记(C++描述)——KMP算法_第2张图片

你可能感兴趣的:(算法笔记)