KMP算法

KMP算法

KMP算法用于在文本串S内查找模式串P的出现位置 时间复杂度为O(M + N)

最大长度表

前缀和后缀

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

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

前缀后缀的最长公共元素长度

举例说明,假如模式串为P = "ABCABD"

模式串ABCABD
最大前缀后缀公共元素长度000120
  1. “A” :前缀为空集,后缀为空集,共有元素长度为0
  2. “AB”:前缀为[A],后缀为[B],共有元素长度为0
  3. “ABC”:前缀为[A, AB],后缀为[C, BC],共有长度为0
  4. “ABCA”:前缀为[A, AB, ABC],后缀为[A, CA, BCA],共有长度为1
  5. “ABCAB”:前缀为[A, AB, ABC, ABCA],后缀为[B, AB, CAB, BCAB],共有长度为2
  6. “ABCABD”:前缀为[A, AB, ABC, ABCA, ABCAB],后缀为[D, BD, ABD, CABD, BCABD],共有长度为0

next数组

next数组是KMP算法的核心部分,其中next[i]的定义为:在模板串P[0, i - 1]范围内的前缀后缀最长公共元素长度

根据定义可以知道next数组可以由最大长度表向右移动一位得到

为了计算方便next[0]通常设为-1

示例

模板串ABCABD
next[i]-100012

next[i+1]的推导

假如已经匹配到了next[i],接下来要得到next[i+1],要分两种情况考虑:

1. P[i] = P[k]
模板串ABCABCD
next[i]-1000123

i = 5时,next[i-1] = 1,前缀后缀最长公共子串长度为1,此时k = 0P[k+1] = BP[i] = B,可以看到前缀后缀最长公共子串为"AB",长度为2,所以next[5] = 2

结论:next[i+1] = next[i] + 1

2. P[i] != P[k]
模板串ABCABDD
next[i]-100012

i = 5时,next[i] = 2,前缀后缀最长公共子串为"AB",此时k = 2P[k] = CP[i] = DP[k] != P[i],但是因为已知前面有相同的"AB",可以找P[0,k-1] = P[0, 1]P[i-k+1, i] = P[4,5]的最长重合串,根据next数组的定义可知即为求next[k]

k = next[k] = 0,重新比较P[i] = CP[k] = A,两者不匹配,此时next[k] = -1,说明P[0, i ]的前缀后缀最长公共子串长度为0,所以next[6] = 0

推荐看参考2的博客,这部分讲的很详细

结论:k = next[k]

个人对于k = next[k]的理解:

p[i] != p[k]时,就需要找出比next[i] + 1小的新前缀后缀最长公共子串长度。next数组本身的作用是找出[0,i-1]部分的前缀和后缀的最长公共子串长度,假设要找的新前缀后缀最长公共子串长度为 l 1 l_1 l1,相比于逐个减一去判断,可以利用next数组做优化, k = next[k],判断的前缀和后缀也相应的缩短到P[0,k]P[i-k,i],如果得到了新的前缀和后缀,结束迭代,否则继续k = next[k]直至k == -1为止

实现

public int[] createNext(String P) {
    char[] p = P.toCharArray();
    int[] next = new int[p.length];
    int i = 0, k = -1;
    while (i < p.length - 1) {
        if (k == -1 || p[i] == p[k]) {
            i++;
            k++;
            next[i] = k;
        } else {
            k = next[k]; // 重点是这一行
        }
    }
}

KMP算法

KMP算法就是利用next数组对暴力算法进行了优化。暴力算法在失配之后会回到最开始重新匹配,在S[i]P[j]失配的时候,利用next数组将模板串P往右移动j - next[j]

思路

设置两个变量i = 0j = 0,文本串S和模式串P

  1. 如果S[i]P[j]匹配,文本串和模式串继续向后匹配:i++;j++;
  2. 如果S[i]P[j]不匹配,保持i不变,将模板串向右移动j - next[j]j = next[j];
  3. 如果j = -1,说明当前S[i]开头的字符串无法与P匹配,j++让模板串P回到开头P[0]i++S[i+1]开头的字符串与P开始匹配

实现

public int KMP(String text, String template) {
    char[] textChar = text.toCharArray();
    char[] templateChar = template.toCharArray();
    int[] next = createNext(templateChar);
        
    int i = 0, j = 0;
        
    while (i < textChar.length && j < templateChar.length) {
    	if (j == -1 || textChar[i] == templateChar[j]) {
            i += 1;
            j += 1;
        } else if (j != -1 && textChar[i] != templateChar[j]) {
            j = next[j];
        }
    }
    if (j >= templateChar.length) {
        return i - t.length; // 匹配成功
    } else {
        return -1;
    }
}

优化

i0123456
模式串ABCDABD
next[i]-1000012

i = 5时匹配失败,此时应该把i = 1处的字符拿来继续比较,但是P[5] == P[1] == B,两个字符一样

实现

public int[] createNext(String P) {
    char[] p = P.toCharArray();
    int[] next = new int[p.length];
    int i = 0, k = -1;
    while (i < p.length - 1) {
        if (k == -1 || p[i] == p[k]) {
            i++;
            k++;
            if (P[i] != P[k]) {
                next[i] = k;
            } else {
                next[i] = next[k]; // 相同情况下就继续往前找
            }
        } else {
            k = next[k];
        }
    }
}

参考

  1. 从头到尾彻底理解KMP(2014年8月22日版)
  2. KMP算法—终于全部弄懂了
  3. 字符串匹配的KMP算法
  4. KMP 算法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值