KMP 子串查找算法

如何在目标字符串S中,查找是否存在子串P? 

朴素解法:

//字符串S中查找子串P的位置
int sub_str_index(const char* s, const char* p)
{
    int ret = -1;

    int s1 = strlen(s);
    int p1 = strlen(p);
    int len = s1 - p1;

    for(int i = 0; (ret < 0) && (i <= len); i++)
    {
        bool equal = true;

        for(int j = 0; equal && (j < p1); j++)
        {
            equal = equal && (s[i + j] == p[j]);
        }

        ret = equal ? i : -1;
    }

    return ret;
}

朴素解法的一个优化线索

因为,Pa != Pb != Pc  且 Pc== Sc;  所以,Pa != Sb Pa != Sc

结论,子串p右移1位比较,没有意义。完全可以无脑右移3位再比较

示例 

 

伟大的发现 

 -匹配失败时的右移位数与子串本身相关,与目标串无关 

 -移动位数=已匹配的字符数对应的部分匹配值 (当部分匹配值为0时,即可无脑右移已匹配的字符数)

 -任意子串都存在一个唯一的部分匹配表 

部分匹配表示例 

 

前缀:除了最后—个字符以外,一个字符串的全部头部组合 

后缀:除了第—个字符以外,—个字符串的全部尾部组合 

部分匹配值:前缀和后缀最长共有元素的长度 

 

#include <iostream>
#include <string.h>

using namespace std;

int* make_pmt(const char* p)    //O(n)
{
    int len = strlen(p);
    int* ret = static_cast<int*>(malloc(sizeof(int) * len));

    if(ret != NULL)
    {
        int ll = 0; // 前后缀交集元素最大长度 large length

        ret[0] = 0; //长度为1的字符串ll值为0

        for(int i = 1; i < len; i++)
        {
            // 第一次循环不会执行,先跳过这里,理解后面的
            while((ll > 0) && (p[i] != p[ll]))
            {
                cout << "p[" << i << "] != p[" << ll << "]: " << ll << " -> " << ret[ll-1] << "(ret[" << ll-1 << "])" << endl
                // 说明当前ll之前的部分匹配成功,p[ll]匹配失败,那么ll的索引需要调整
                // 现在需要考虑的是p[i]需要和谁比较,已经知道p[ll-1]是匹配成功,ll-1位置的是否有公共部分呢,有如果没有,那么ret[ll-1]就是0,p[i]直接可以从头开始比较
                // 如果有公共部分,那么p[i]就只需与公共部分的后一个比较即可,看结果分析图
                ll = ret[ll-1]; 
            }

            // 相当于子串的自身比较,例如:
            // ABCDABD    i = 1, 2, 3, 4
            //  ABCDABD   ll : 两个含义:当前正在匹配的下标 或 部分匹配值
            if(p[i] == p[ll]) 
            {
                cout << "p[" << i << "] == p[" << ll << "]: " << ll << " -> " << ll+1 << endl;
                ll++; // i位置匹配到了
            }

            cout << "ret[" << i << "] : " << ll << endl << endl;
            ret[i] = ll;    //部分匹配表存放部分匹配值
        }
    }

    return ret;
}

int main()
{
    int* pmt = make_pmt("ABCDABCABCDABCD");

    for(int i = 0; i < strlen("ABCDABCABCDABCD"); i++)
    {
        cout << i << " : " << pmt[i] << endl;
    }

    return 0;
}

 

部分匹配表的使用(KMP算法) 

            

KMP子串查找算法的实现 

#include <iostream>
#include <cstring>
#include <cstdlib>

using namespace std;

int* make_pmt(const char* p)    //O(n)
{
    int len = strlen(p);
    int* ret = static_cast<int*>(malloc(sizeof(int) * len));

    if(ret != NULL)
    {
        int ll = 0;

        ret[0] = 0; //长度为1的字符串ll值为0

        for(int i = 1; i < len; i++)
        {
            while((ll > 0) && (p[ll] != p[i]))
            {
                ll = ret[ll-1];
            }
            if(p[ll] == p[i])
            {
                ll++;
            }

            ret[i] = ll;    //部分匹配表存放部分匹配值
        }
    }

    return ret;
}
int kmp(const char* s, const char* p)    //O(m)+O(n)=O(m+n)
{
    int ret = -1;
    int s1 = strlen(s);
    int p1 = strlen(p);
    int* pmt = make_pmt(p);

    if((pmt != NULL) && (0 < p1) && (p1 <= s1))
    {
        for(int i=0, j=0; i < s1; i++)
        {
            while((j > 0) && (s[i] != p[j]) )
            {
                j = pmt[j-1];
            }

            if(s[i] == p[j])
            {
                j++;
            }

            if(j == p1) // 成功完全匹配
            {
                ret = i + 1 - p1;
                break;
            }
        }
    }

    free(pmt);

    return ret;
}

int main()
{
    cout << kmp("ABCDABD", "ABD") << endl;   // 4
    cout << kmp("ABCDABD", "ABDC") << endl;  // -1

    return 0;
}

发现从求PMT,到KMP两者的步骤非常相似...

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值