KMP算法原理

KMP

KMP,是字符串的模式匹配算法,利用已经得到的信息,不需要回溯,所以比暴力解法效率高。关键是求next数组,也有称为前缀数组,那next数组是什么含义。
不需要回溯指的是:主串s的指针i不需要回溯。

1. 暴力解法

暴力解法有个很动听的名字,朴素字符串匹配算法。
暴力解法思路:
模式串中第一个字符和主串中的某个字符比较,如果相等,分别比较主串和模式串中的下一个字符;如果不相等,将这次比较的匹配信息全部丢掉,主串回到这次比较的第一个字符的下一个字符,和模式串的第一个字符比较。
思路清晰,代码实现:

#include <iostream>
using namespace std;
class Solution {
public:
    /* 如果存在,则返回第一次在主串中出现的下标,否则返回-1。 */
    int find_pos(const char *s, const char *p) {
        int slen, plen, i, j;
        slen = strlen(s);
        plen = strlen(p);
        for (i = 0; i  < slen; i ++) {
            for (j = 0; j < plen; j ++) {
                if (s[i + j] != p[j])
                    break;
            }
            if (j == plen)
                return i;
        }
        return -1;
    }
};
int main()
{
    char *s = "abcdefgk";
    char *p = "def";
    Solution sol;
    int pos = sol.find_pos(s, p);
    cout << "pos=" << pos << endl;
    return 0;
}

2. next数组

next数组含义如:next[j] = k
当模式串p的第j个字符与主串s中第i个字符不匹配时,主串s的指针i不变,模式串p中的指针j退到next[j]指向的位置k,重新进行比较。
next数组前缀后缀的含义:当模式串p的指针j和主串s中的指针i不匹配时,求出在模式串p中指针j前的字符中长度最大的前缀和后缀相等的字符串。
这样的解释好像还没有那么清晰。


next[j] = k含义:
理解[1]:代表p[j]之前的模式串子串中,有长度为k的相同前缀和后缀;
理解[2]:当模式串中第j处字符失配时,下一步next[j]处的字符继续跟主串匹配,相当于模式串向右移动j - next[j]个字符。

把next数组过程理解清楚,代码实现也会容易。


已知next[j] = k,求next[j + 1]的值。
证明:
next[1] = 0(为什么不是1?因为当模式串中第一个字符和主串第i个字符不匹配时,此时模式串中相等的前缀和后缀长度是0,所以是0)
假设next[j] = k, 那么next[j+1] 是多少?
(1)当p[j] = p[k]时,则next[j+1] = k + 1。
因为next[j] = k,则有p[1…k-1] = p[j-k+1…j-1],当p[j] = p[k],则有p[1…k-1、k] = p[j-k+1…j-1、j],next[j+1] = k + 1=next[j] + 1。
(2)当p[j] != p[k]时,则现在模式串p中p[1…k-1、k] != p[j-k+1…j-1、j]。看了半天没看懂,参考严书,学的时候就记得严书写的很好。
此时,可把求next数组值的问题再看成一个模式匹配的问题,整个模式串既是主串又是模式串(前缀和后缀匹配的问题),而当前的匹配过程中,p[j] != p[k]时,应将模式向右滑动至以模式中的第next[k]个字符和主串中的第j个字符相比较。若next[k]=k’,且p[j]==p[k’],则说明在主串中第j+1个字符之前存在一个长度为k’的最长子串,和模式串中从首字符起长度为k’的子串相等,即p[1…k’-1、k’] != p[j-k’…j-1、j],也就是说next[j + 1] = next[k] + 1。
同理,如果p[j] != p[k’],则将模式继续向右滑动直至将模式中第next[k’]个字符和p[j]对齐,……,依次类推,直至p[j]和模式中某个字符匹配成功或不存在任何k’满足p[1…k’-1、k’] != p[j-k’…j-1、j],则next[j + 1] = 1。
数组下标从1开始。
书上举的例子,理解起来会容易一些。看下图,直接截图的。
严书P83
如图4.6中的模式串,已求出前6个字符的next的值,现求next[7],因为next[6]=3,又p[6] != p[3],则需比较p[6]和p[1](因为next[3]=1),这相当于将子串模式向右滑动,由于p[6] != p[1],且next[1]=0,所以next[7]=1,而因为p[7]==p[1],所以p[8]=2。

算法1:

void calNext(const char p[], int next[])
{
    int i, j;
    int len = strlen(p);
    i = 1;
    j = 0;
    next[1] = 0;
    /* 书上写的是i<len,我总觉得该是i<=len */
    while (i < len) {
        if (0 == j || p[i] == p[j]) {
            ++ i;
            ++ j;
            next[i] = j;
        } else {
            j = next[j];
        }
    }
    return;
}            

上面是《严书》写的,代码我稍微修改了下。

void calNext(const char p[], int next[])
{
    int i, j;
    int len = strlen(p);
    i = 1;
    j = 0;
    next[1] = 0;
    /* 
     * 下标从1开始,next数组长度是len+1,但char数组比较下标还是  从0开始 
     */
    while (i <= len) {
        if (0 == j || p[i - 1] == p[j - 1]) {
            ++ i;
            ++ j;
            next[i] = j;
        } else {
            j = next[j];
        }
    }
    return;
}            

我是比较清楚了,还有问题的同学也可以看七月算法官网上有视频。
前面定义的next函数在某些情况下还有缺陷。如……不清楚的同学还是请看严奶奶写的《数据结构》课本吧,写的太详细了P84,这里我仅截图。看到这个图种next数组的值,发现和我之前简单理解的有些偏差,证明过程的才是正确的求法。
改进next数组
算法2:

void calNextval(const char p[], int nextval[])
{
    plen = strlen(p);
    i = 1;
    nextval[1] = 0;
    j = 0;
    while (i < plen) {
        if (0 == j || p[i] == p[j]) {
            ++ i;
            ++ j;
            if (p[i] != p[j])
                nextval[i] = j;
            else 
                nextval[i] = nextval[j];
        } else {
            j = nextval[j];
        }
    }
}

改进的nextval数组还不是完全理解,先写到这,音乐一直在响,该走了。

2. 使用KMP算法实现字符串模式匹配

现在时明白KMP算法是怎么回事了,那赶紧来实现以下吧。

#include <iostream>
using namespace std;
/* 求出next数组的值,从数组下标0开始 */
void calNext(const char p[], int next[])
{
    int i, j;
    int len = strlen(p);
    i = 0;
    j = -1;
    next[0] = -1;
    while (i < len) {
        if (-1 == j || p[i] == p[j]) {
            ++ i;
            ++ j;
            next[i] = j;
        } else {
            j = next[j];
        }
    }
    return;
}
int main()
{
    char *s = "abcdabcefghi";
    char *p = "abce";
    int *next = (int *)malloc(strlen(p));
    calNext(p, next);
    int i, j, slen, plen, res;
    i = j = 0;
    slen = strlen(s);
    plen = strlen(p);
    while (i < slen && j < plen) {
        if (-1 == j || s[i] == p[j]) {
            ++ i;
            ++ j;
        } else {
            j = next[j];
        }
    }
    if (i == strlen(s))
        cout << "fail " << endl;
    if (j == strlen(p))
        cout << "i =" << i - plen << endl;
    cout << "all right" << endl;
    return 0;
}

暴露了用C的习惯。对数组名取sizeof,得出的值是数组中所有元素占的字节数,即使将数组名赋给指针p,对指针p取sizeof,返回的是保存指针p所占的字节数,而不是数组中元素占的字节数。
sizeof得出的值实际占用的内存空间的字节数。

参考:
[1] http://www.cnblogs.com/c-cloud/p/3224788.html
[2] 《数据结构》严书讲的好

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值