【数据结构学习记录11】——KMP算法,AC自动机青春版

一.前言

上一节中,我们通过一种暴力的串搜索(匹配)方法,学名叫做BF算法。而这种方法的时间复杂度能够达到O(mn)的数量级,而有些程序员就不乐意了,于是乎有三个分别以KMP字母开头的大佬发明出了这种理论上在O(mn),实际上经常在O(m+n)的一种搜索算法,我们称为KMP算法

二.原理介绍

1.前后缀

我们要先懂得前后缀是啥玩意儿。假设我们有字符串Kanna
那么它的前缀有:KannKanKaK
而它的后缀有:annannanaa
很好理解对吧,就不多说了。不过一个字符串的本身是不构成其前后缀的。

2.最大公共(相同)前后缀

对于一个串,如果它有相同的前后缀,那么找出最大的那个,就叫做最大公共前后缀,也叫最大相同前后缀。
比如:

  1. aabb,它就没有公共前后缀。
  2. aabba,它的最大公共前后缀是a
  3. aabbaa,它的最大公共前后缀是aa
  4. ababa,它的最大公共前后缀是aba
    很简单,如果实在不知道怎么来的,就把他们所有的前缀后缀列出来,取交集,然后在交集里找最长的那个。

3.失配

假设我们字符串主串S=ababb,匹配串D=abaa,那么我们在遍历指针的时候S[0~2]和D[0~2]都是一样的aba,而在S[3] D[3]处发生了b!=a,那么我们就可以说,匹配串在S[3]处,发生了失配。

4.串的名字

被查找的串,我们叫主串;用于查找的串叫模式串
比如在abaa里查找baa,那么前者是主串,后者则是模式串。

三.匹配过程

1.查找模式

::对于主串和模式串,遍历主串的指针是不会回溯的::先记住这句话。
假设我们要在主串S=ababcaabcab查找匹配串P=abcab
在这里插入图片描述

这样我们就匹配到了我们的字符串abcab
我们注意第二次失配后,到第三次变化,我们的主串的指针并没有变化,而是我们模式串的指针发生了变化。而第二次到第三次,指针从[4]到了[1],也没有到[0],这就是该算法的魅力所在。

2.细解查找模式

为什么我们在第一次到第二次的时候,直接就从主串的指针依旧停留在S[2],而没有会退到S[1]呢?假设我们退回到S[1],那么我们匹配就是S[1]与P[0]比较,很显然:S[1] = b,P[0]=a,它俩并不相等。
我们来看一看对于串ab,它们没有公共串,所以,ab后面(后缀)的必不可能等于前面(前缀)的部分。那么我们在下一次匹配的时候,会用匹配串的前面的去与上一次匹配的末尾(也就是匹配串的末尾)进行匹配,而我们在上一次匹配的时候已经知道了,后面不可能等于前面的,所以就没必要进行匹配。当然,就直接跳过这一次比较。
再看第二次到第三次的时候,我们在S[6]处发生失配,但是在下一次比较的时候,并不是从P[0]开始,而是从P[1]开始,为什么?
我们看看串abca,它好像有一个最大公共串a,那么也就是说,上一次匹配的末尾,有我们这次匹配的开头,那么我们就也不用匹配了,直接跳过。
(好家伙,我直接好家伙。有公共串的也跳过,没公共串的也跳过,真的懒)

3.next数组介绍

好家伙,那么我怎么知道p指针回溯多少呢?那么我们需要用一个数组来存一下我们的串,这个串保存着我们该跳多少。比如我们的abcab,我们的next数组对应的是00012,怎么求?我们看看:

  1. a 0个公共串
  2. ab 0个公共串
  3. abc 0个公共串
  4. abca 最大公共串a,长度1
  5. abcab最大公共串ab,长度2
    这样,我们next数组不只代表着当前下标前所有字符组成的串的最大公共串长度,而且还代表了我们模式串指针回溯后,下一次遍历开始的地方。

4.求next数组的过程

那么,我们如何来求我们的next数组呢?求next数组的方法和我们匹配的方法也差不多,只不过它是自己匹配自己。
这一自然段是该小节的精髓,我觉得我自己总结得很精辟,大家细细揣摩一下:
用一个p指针指向前缀的末尾的下一个位置,也是描述当前连续的前后缀长度

next数组的内容,是回溯后的起点,换句话说,就是当前下标回溯后的p指针指向的下标然后s指针指向当前的字符。回溯的边界就是在P[0]处也失配,无法进行回溯了,就直接记0
还是以模式串Pabcab来看,我们的next[0]肯定是0。
然后从s=1 p=0开始匹配。
P[1]和P[0]不相等,且在P[0]处失配,所以next[1]=0, ++s
P[2]和P[0]不相等,且在P[0]处失配,所以next[2]=0, ++s
P[3]和P[0]相等,所以++p, next[3]=1, ++s, 注意,此时p指针加了1,说明我们连续最大的公共串的长度就是1。
P[4]和P[1]相等,所以 ++p, next[4]=2, ++s
这样我们的next数组就构造完成了。
当然,我们再举一个有回溯的最大公共串例子:
ababc
首先,next[0]一定为0,s=1,p=0
P[1]!=P[0],在P[0]失配,next[1]=0, ++s
P[2]=P[0],,++p, next[2]=1,++s
P[3]=P[1],++p, next[3]=2,++s
P[4] != p[2], 所以p=next[p-1] = 0
P[4] != P[0], 在P[0]失配,next[4] = 0, ++s
所以得到ababc的next数组是00120
在这里插入图片描述
在这里插入图片描述

如果搞不清过程,可以拿纸画一画

5.next数组的变种

很多时候,有些人对next数组进行了变种,假设有一种next数组是00120
变种数组有:
错位版-10012,这种就是把S[0]初始化为-1,且并不参与前缀计算,真的前缀计算从S[1]开始。
也有减1版-1-101-1。具体的next数组是为具体的代码服务的,代码不同,next数组也不同。
不过错位版的代码量是最少的。

四.代码编写流程

1.KMP匹配

那么我们查找模式的流程因该是:主串指针用s表示,匹配串指针用p表示。

  1. 如果匹配到,那么++s,++p,进行下一个匹配,一直循环直到s或者p遍历完整个主串S或模式串P。
  2. 如果没匹配到,p指针回溯,回溯到前面重新匹配。
  3. 当我们循环完成1、2过后,判断下p指针是否指向P的末尾,这样就可以判断模式串是否全部被匹配到了。

2.next数组的构造

  1. 初始化 next[0] = 0, s=1, p=0
  2. 开始遍历,直到遍历完S
  3. 如果相等,++p,next[s] = p(此时p的含义是指向前缀的末尾的下一个,也就是前缀的长度), ++s ,两个指针都向前移动
  4. 如果不相等,先判断p是否等于0
  5. 在p=0时,匹配不相等,那么说明,匹配不到,则next[s] = 0, s++
  6. 在p!=0时,那么p=next[p-1] (此时右边的p含义是当前前缀的末尾的下一个,所以需要减1),左边的p含义是下一次匹配前缀的起点
  7. 遍历完S

五.代码实现

因为原理说明白了,我们的代码实现也很简单。那就用上一篇文章写的字符串来实现。

int StringKMP(sstring *bstr, sstring *mstr)
{
    
    
    int *next=(int *)malloc(sizeof(int)*mstr->len);
    int s = 1, p = 0;

    next[0] = 0;
    // 构造next数组
    while(s < mstr->len)
    {
    
    
        if (*(mstr->ch+s) == *(mstr->ch+p))     // 这里是匹配到了
        {
    
    
            ++p;    // 先给p+1,使得p代表当前公共缀的储长度,或者是公共缀下标的下一个位置
            next[s] = p;    // 当前位置的公共缀长度
            ++s;    // 下一轮匹配
        }
        else if(p == 0)     // 从这里开始,肯定就是匹配失败的情况了
        {
    
    
            next[s] = 0;    // 先判断是不是到达了回溯边缘,是就无公共缀
            ++s;    //进行迭代
        }
        else
        {
    
    
            p = next[p-1];  //也不在边缘,可以回溯,但注意,我们的p永远是指向公共缀下标的下一个位置故要减一
        }
    }
    // next 数组构造完毕,开始KMP查找
    s = 0;
    p = 0;
    while(s < bstr->len && p < mstr->len)
    {
    
    
        if (*(bstr->ch+s) == *(mstr->ch+p)) // 匹配,进行下一轮匹配
        {
    
    
            ++s;
            ++p;
        }
        else if (p == 0)    // 匹配不上,且在回溯边缘,++s进行下一轮匹配
        {
    
    
            ++s;
        }
        else
        {
    
    
            p = next[p-1];    // 匹配不上,回溯到上一个前缀
        }
    }
    if (p == mstr->len)    // 是否把模式串匹配完了
    {
    
    
        return s-p;     // 作差即为下标
    }
    else
    {
    
    
        return -1;  // 没找到
    }
}

至于如何使用,加到上一节博客的内容后就行了,算了还是放一个完整代码:

#include <stdio.h>
#include <stdlib.h>

#define     OK          1
#define     ERROR       0

typedef struct sstring{
    
    
    char *ch;
    int len;
}sstring;

sstring* StringInit(char* str);
int StringShow(sstring *str);
int StringConcat(sstring* str1, sstring* str2);
int StringCompare(sstring *str1, sstring *str2);
sstring *StringGet(sstring *str, int index, int len);
int StringFind(sstring *bstr, sstring *mstr);
int StringKMP(sstring *bstr, sstring *mstr);

int main()
{
    
    
    // 主函数随便改
    sstring *test1 = StringInit("ababdabcababc");
    sstring *test2 = StringInit("ababc");
    //StringConcat(test1, test2);
    //printf("%d", StringCompare(test1, test2));
    //StringShow(StringGet(test2,0,3));
    //printf("%d", StringFind(test1,test2));
    printf("\nanseris %d\n", StringKMP(test1, test2));
    return 0;
}

sstring* StringInit(char* ss)
{
    
    
    int lenth = 0;
    //首先创建要返回的
    sstring *str = (sstring*)malloc(sizeof(sstring));
    
    //动态生成失败,直接退出
    if (str == NULL) exit(1);
    //如果传入的是空字符串,我们就返回一个空的字符串
    if (ss == NULL)
    {
    
    
        str->ch = NULL;
        str->len = 0;
        return str;
    }
    // 通过依次遍历,获得传入字符串中,非/0部分长度。
    while(*(ss + lenth) != '\0')
    {
    
    
        ++lenth;
    }
    // 修改我们字符串的长度和动态分配它的储存空间
    str->len = lenth;
    str->ch = (char*)malloc(sizeof(char)*lenth);
    --lenth;
    // 通过遍历,将C语言字符串的内容,复制到我们的新字符串中
    while(lenth >= 0)
    {
    
    
        *(str->ch+lenth) = *(ss+lenth);
        --lenth;
    }

    return str;
}

int StringShow(sstring *str)
{
    
    
    int ptr = 0;
    printf("the string len is %d context is: ", str->len);
    while(ptr < str->len)
    {
    
    
        printf("%c", *(str->ch + ptr));
        ++ptr;
    }
    printf("\n");
    return OK;
}

int StringConcat(sstring* str1, sstring* str2)
{
    
    
    sstring* stringNew = NULL;
    int ptr = 0;
    // 如果两个串的长度都是0,那就直接返回即可
    if (str1->len + str2->len == 0) 
    {
    
    
        return OK;
    }
    // 否则就先生成我们的新串,修改长度与内容
    stringNew = (sstring*)malloc(sizeof(sstring));
    stringNew->ch = (char*)malloc(sizeof(char)*(str1->len+str2->len));
    stringNew->len = str1->len+str2->len;
    // 通过循环,将str1的值写入新串
    for(;ptr < str1->len; ++ptr)
    {
    
    
        *(stringNew->ch+ptr) = *(str1->ch+ptr);
    
    }
    // 在str1写入新串的基础上,向新串写入str2
    for(ptr = 0;ptr < str2->len; ++ptr)
    {
    
    
        *(stringNew->ch+ptr+str1->len) = *(str2->ch+ptr);
    }

    // 然后这里优点坑,因为传递过来的指针是形参,并不是引用
    // 所以 我们只能把新串的值赋值给原来的串
    // 此时,传入函数字符串的地址没变,但是len变了, ch的地址变了
    *str1 = *stringNew;
    return OK;
}

int StringCompare(sstring *str1, sstring *str2)
{
    
    
    int i = 0;

    // 长度都不一样,所以通过长度,反应关系
    if (str1->len > str2->len)
    {
    
    
        return 1;
    }
    else if (str1->len < str2->len)
    {
    
    
        return -1;
    }
    else
    {
    
    
        // 长度一样了,只有依次对比了
        for (; i < str1->len; ++i)
        {
    
    
            // 只要有一个字符不一样,那就根据ascii的关系去返回大小关系
            if (*(str1->ch+i) < *(str2->ch+i))
            {
    
    
                return -1;
            }
            else if (*(str1->ch+i) > *(str2->ch+i))
            {
    
    
                return 1;
            }
        }
        // 循环完了也没有找到不同,所以它俩是一样的
        return 0;
    }
}

sstring *StringGet(sstring *str, int index, int len)
{
    
    
    sstring *rstr = NULL;
    int i = 0;

    // 如果目标串的长度小于我们要求的长度,所以直接返回空的
    if (str->len < index+len)
    {
    
    
        return NULL;
    }
    else
    {
    
    
        // 动态生成我们的返回串
        rstr = (sstring *)malloc(sizeof(sstring));
        rstr->ch = (char *)malloc(sizeof(char)*str->len);
        rstr->len = len;
        // 然后把目标串里的值复制到我们的返回串里
        for (i = 0; i < len; ++i)
        {
    
    
            *(rstr->ch+i) = *(str->ch+index+i);
        }
        return rstr;
    }
    
}

int StringFind(sstring *bstr, sstring *mstr)
{
    
    
    int fptr = 0, lptr = 0;
    int mark = 0;

    // 如果我们要查找的串的长度大于了目标串,那肯定找不到的,直接返回-1
    if (bstr->len < mstr->len)
    {
    
    
        return -1;
    }
    // lptr是指向 我们目标串的开始指针
    // 它只需要从0遍历到(目标串长度-要查找的串的长度)就行了
    for (;lptr <= (bstr->len-mstr->len); ++lptr)
    {
    
    
        // mark是标记位,如果有不同,那就是1 没有不同就还是0
        mark = 0;
        // 这个是查找指针,我们要对比的内容因该是lptr+fptr
        // 它的范围是 0到查找串的长度-1
        for (fptr = 0; fptr < mstr->len; ++fptr)
        {
    
    
            // 对比的内容是 lptr+fptr
            if (*(bstr->ch+lptr+fptr) != *(mstr->ch+fptr))
            {
    
    
                // 有不同,更新标识,并跳出这一轮 fptr的遍历
                mark = 1;
                break;
            } 
        }
        // fptr遍历完了,都还没有不同的,说明找到了
        if (mark == 0)
        {
    
    
            // 那么就因该返回我们lptr的起始位置
            return lptr;
        }
    }
    // 查遍了整个串都没找到,那就只能返回 -1了
    return -1;
}

int StringKMP(sstring *bstr, sstring *mstr)
{
    
    
    int *next=(int *)malloc(sizeof(int)*mstr->len);
    int s = 1, p = 0;

    next[0] = 0;
    // 构造next数组
    while(s < mstr->len)
    {
    
    
        if (*(mstr->ch+s) == *(mstr->ch+p))     // 这里是匹配到了
        {
    
    
            ++p;    // 先给p+1,使得p代表当前公共缀的储长度,或者是公共缀下标的下一个位置
            next[s] = p;    // 当前位置的公共缀长度
            ++s;    // 下一轮匹配
        }
        else if(p == 0)     // 从这里开始,肯定就是匹配失败的情况了
        {
    
    
            next[s] = 0;    // 先判断是不是到达了回溯边缘,是就无公共缀
            ++s;    //进行迭代
        }
        else
        {
    
    
            p = next[p-1];  //也不在边缘,可以回溯,但注意,我们的p永远是指向公共缀下标的下一个位置故要减一
        }
    }
    // next 数组构造完毕,开始KMP查找
    s = 0;
    p = 0;
    while(s < bstr->len && p < mstr->len)
    {
    
    
        if (*(bstr->ch+s) == *(mstr->ch+p)) // 匹配,进行下一轮匹配
        {
    
    
            ++s;
            ++p;
        }
        else if (p == 0)    // 匹配不上,且在回溯边缘,++s进行下一轮匹配
        {
    
    
            ++s;
        }
        else
        {
    
    
            p = next[p-1];    // 匹配不上,回溯到上一个前缀
        }
    }
    if (p == mstr->len)    // 是否把模式串匹配完了
    {
    
    
        return s-p;     // 作差即为下标
    }
    else
    {
    
    
        return -1;  // 没找到
    }
}

猜你喜欢

转载自blog.csdn.net/u011017694/article/details/109576175