超级详细解释KMP算法(next数组的前世今生)

KMP算法

KMP算法的作用

可以在O(m+n)的时间复杂度内,在文本串S中找到模式串P是否存在。

KMP算法的思想

首先要想理解KMP算法,就必须先理解了解暴力解法的思想

假设有文本串

A B B B A A B B A

模式串

A B B A

如果是暴力解法,那么会有两个指针,一个指针i指向文本串。一个指针j指向模式串。

j未走完的中间发现不匹配都需要将i调整为i+1j要变为0.

for(int i=0;i<S.size();i++) {
    for(int j=i;j<S.size();j++) { // 简化写法,并不考虑S不够的情况
        if(S[i+j]!=P[j]) {
            break;
        }
    }
}

A B A B A A B A A

A B A A

发现不匹配

A B A B A A B A A

A B A A

A B B B A A B B B

A B A A

A B A B A A B A A

​ A B A A

匹配成功

算法4中的KMP算法

KMP算法思想

算法4中的KMP算法的表象就是i会一直的往前走,遇到不匹配的情况就只需要调整j也就是下一次匹配的地方

其实KMP算法就是充分利用了匹配失败时候的已知的信息。

如果匹配失败

A B A B A A B A A

A B A A

其实不难发现,我们知道匹配失败时候i指向的字符是B也知道,前面匹配成功的字符就是前j-1个字符,也就是A B B

这时候如果要让i往前走j调整到合适的匹配位置。

也就是这时候i变成i+1,而i之前的与P匹配的最多越好。

其实也就是在匹配失败时候i到匹配失败的位置之间,重新找到那个能和P匹配上的点

A B A B

这个问题就转变为找这个字符串的最长相同的前后缀长度了。

  • 前缀, 是包含第一个字符,不包含最后一个字符的的子串
  • 后缀,包含最后一个字符,但是不包含第一个字符的子串

A B A B

的前缀有 A ,A B,A B A

后缀有 BA B, B A B

可以看出最长的公共前后缀就是A B

这时候就变成了

A B A B A A B A A 黄色为当前i的位置

​ A B A B 黄色为j当前调整后的位置,需要与当前i元素比较

参考

这样的会i只管往前跑,只需要调整j的位置

这就是KMP算法的核心思想。

KMP算法的表示

  • 如何取去示,当遇到字符S[i]的时候时候,j跳转的位置

这时候设置二维数组dfa[S[i]][j]代表的含义就是,当遇到S[i]字符j需要跳转的位置

由上面可以知道:

跳转的位置其实就是,最长公共前后缀的长度
所以这个含义也是:
P[0-j-1]与S[i]组合而成的字符串(也就睡以S[i]结尾长度为j+1的字符串)的最长公共前后缀长度
怎么求解dfa数组

其实就只有两种情况

  • 情况一: 当S[i]P[j]相等匹配的时候

    如果匹配其实就是dfa[s[i]][j]=j+1

  • 情况二: 当S[i]P[j]不相等不匹配的时候

    这个时候,j的跳转位置,是已经匹配字符串与匹配失败字符结合字符串的最长公共前后缀长度。

    其实也就是dfa[S[i]][k],其中k代表的是

这个问题可以转化为:已知一个字符串S,它的前缀字符串和后缀字符串的最长共有元素的长度为k,现将一个字符拼接在S的尾部组成新的字符串S',求S'的前缀字符串和后缀字符串的最长共有元素的长度k'。

首先能够确定的是,k'的大小不会超过k+1,也就是说k'只可能<= k+1。分两种情况讨论:

当拼接的字符等于S[k]时,k'的大小为k+1;
当拼接的字符不等于S[k]时,k'等于S[0]、S[1]、S[2]、...、S[k-1]与该字符组成的字符串的前缀字符串和后缀字符串的最长共有元素的长度;
实际上这正是dfa[t[i]][k]的含义

所以dfa的构造就是

dfa[pat.charAt(0)][0] = 1;
for (int k = 0, j = 1; j < pat.length(); j++) {
	// 计算dfa[][j]
  for (int c = 0; c < 256; c++) {
    dfa[c][j] = dfa[c][k];   // 匹配失败的情况
  }
  dfa[pat.charAt(j)][j] = j + 1;  // 匹配成功的情况
  k = dfa[pat.charAt(j)][k];      // 这时候的S就是之前匹配上的最长公共前后缀
}

如果将j的位置当做一种状态,这就是一个自动状态机了。

所以

public static int search(String pat, String txt) {
    int j, M = pat.length();
    int i, N = txt.length();
    for (i = 0, j = 0; i < N && j < M; i++)
      j = dfa[txt.charAt(i)][j];  // 状态的转移
    if (j == M)
        return i - M;
    else
        return N;
}

这时候我们知道i是一直往前走的,所以必须要S中匹配失败的一个字符和T中已经匹配成功字符的信息。

// 这时候就想,能不能不要和S牵扯上关系,只需要和P有关

这时候,如果匹配成功i=i+1.如果匹配失败了,这时候i不变,直接调整j,这时候S[i]就是要匹配的下一个字符,这时候要转移多少其实就是已经匹配字符串的最长公共前后缀的长度。

A B A B A A B A A

A B A A

这时候下一个匹配的还是B

前缀: A,AB 后缀A,BA

所以

A B A B A A B A A

​ A B A A

因为B的前一个是A,所以一定是以A为结尾,而且必须重头开始匹配,所以以A开头,这也是为什么用前后缀 这是理解KMP算法的一个关键点,为什么是前后缀

这样做的好处是,去除了一维,只剩下一维了

怎么去求这一维的值就是重点了,已经S[0-i]的公共前后缀长度,求S[0-i+1]公共前后缀的长度

这也就是next数组的前身

用数组pre_next[]来表示,所以pre_next[j]=k代表的含义是:模式串中前j个字符子串的最长公共前后缀的长度为k

参考链接

求解pre_next[]

那么问题是怎么去求模式串各个子串的最长公共前后缀的长度

A B A A

来说

这就是next数组的来历

例题

NC149 kmp算法

知识点字符串

描述

给你一个文本串 T ,一个非空模板串 S ,问 S 在 T 中出现了多少

示例1

输入:

"ababab","abababab"

返回值:

2

示例2

输入:

"abab","abacabab"

返回值:

1
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 计算模板串S在文本串T中出现了多少次
     * @param S string字符串 模板串
     * @param T string字符串 文本串
     * @return int整型
     */
    int kmp(string S, string T) {
        // write code here
       vector<int> next(S.size());
        int ans = 0;
        GetNext(next, S);
        int j = 0;
        int i = 0;
        while(i<T.size()) {
            if(j==-1 || T[i]==S[j]) { // 匹配成功 j==-1的含义是第一个匹配失败,重新开始匹配
                if(j==S.size()-1) { // 匹配成功
                    ans++;
                   // 下一个要匹配的位置就是
                    j=next[j];  // 假设当前失败后,这个位置要去哪里匹配
                } else {
                    i++;
                    j++;
                }
                
            } else { // 匹配失败
                j=next[j];  // 跳转位置
            }
        }
        return ans;
    }
    
    
    
    void GetNext(vector<int>& next,string& Pattern) {
        // 初始化
        next[0]=-1;
        int k=-1,j=0;
        while(j<Pattern.size()-1) {  // 这时候的next相当于pre_next的往前推了一位。所以最后一位不考虑
            if(k==-1 || Pattern[j]==Pattern[k]) {
                j++;
                k++;
                next[j]=k;
            } else { // 不相等
                k=next[k];
            }
        }
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值