KMP算法

KMP算法

KMP算法是一种改进的字符串匹配算法。核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。

具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。

算法设计

特点

主串的i不会回退,并且j也不会移动到0号位置。

主串i为什么不回退?

在这里插入图片描述

  • 如上图情况,假设在2处匹配失败,就算主串退回到1位置,这个位置的b和子串0位置的a仍然不相等,非常浪费比较次数。

子串j回退的位置?

在这里插入图片描述

  • 当匹配失败时,我们不将主串i进行回退。因为在这个地方匹配失败说明i的前面和j前面有一部分是相同的
  • 结合上图分析,当j回退到2下标时,i不回退,就是最好的情况。
  • 为什么会这样回退呢?大致分析可知:我们找到i位置前面和子串0位置开始有没有已经匹配的子串。我们可以发现子串0,1位置a,bi之前3,4位置a,b已经匹配,因此将j退回子串2位置即可。

在这里插入图片描述

既然我们得到了最佳的回退方案,所以我们可以使用一个数组来存储子串每次匹配失败最佳回退位置,这样就能利于我们匹配了。

next数组

KMP的精髓就next数组,它用next[i] = k来表示。不同的j来对应一个k值,这个k就是匹配失败后j要移动的位置。

k值的求法

  • 找到匹配成功部分的两个相等的真子串(不包含本身),一个以下标0开始,另一个以下标j-1结尾。
  • 不管什么数据next[0] = -1; next[1] = 0; 这里我们以下标开始,第几个都是从1开始算。

用上面的方法求next数组

练习1:
子串:     a b a b c a b c d a b c d e
next数组:-1 0 0 1 2 0 1 2 0 0 1 2 0 0

练习2:
子串:     a b c a b c a b c a b c d a b c d e
next数组:-1 0 0 0 1 2 3 4 5 6 7 8 9 0 1 2 3 0
  • 通过上面的练习我们可以看出next数组的值增加时一定是连续的,不会产生跨度,这点可以检查正确性。

递推公式

通过上面我们学会了next[i] = k的求法,但是想要获得整个数组的值,我们能不能由next[i] = k求出next[k+1]的值呢?如果可以实现那么整个数组就变成递推可求的了。

  • 假设1:next[i] = k成立,那么p[0]...p[k-1] = p[i-k]...p[i-1]
  • 假设2:p[i] = p[k]成立,那么p[0]...p[k] = p[i-k]...p[i]
  • 由上述条件可得:next[i+1] = k+1

在这里插入图片描述

  • 当假设2不成立时,也就是p[i] != p[k]时,回退到k位置仍然不相等,所以我们应该一直回退,直到找到p[i] = p[k]为止。

在这里插入图片描述

代码实现

c语言

初始next数组前两项为-1,0

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

void getNext(char* sub, int* next, int lenSub) {
    next[0] = -1;
    next[1] = 0;
    //当前i的下标
    int i = 2;
    //存储的前一项的k值。
    int k = 0;
    while (i < lenSub) {
        if (k == -1 || sub[i - 1] == sub[k]) {
            next[i] = k + 1;
            i++;
            k++;
        }
        else {
            k = next[k];
        }
    }
  
}
int KMP(char* str, char* sub, int pos) {
    assert(str && sub);
    int lenStr = strlen(str);
    int lenSub = strlen(sub);
    if (lenStr == 0 || lenSub == 0) {
        return -1;
    }
    if (pos < 0 || pos >= lenStr) {
        return -1;
    }

    //求next数组
    int* next = (int*)malloc(sizeof(int) * lenSub);
    assert(next);
    getNext(sub, next, lenSub);

    //遍历主串
    int i = pos;
    //遍历子串
    int j = 0;

    while (i < lenStr && j < lenSub) {
        if (j == -1 || str[i] == sub[j]) {
            i++;
            j++;
        }
        else {
            j = next[j];
        }
    }
    free(next);
    if (j >= lenSub) {
        return i - j;
    }
    else {
        return -1;
    }
}

int main() {
    printf("%d\n", KMP("abcababcabc", "abcabc", 0));
}

java

初始next数组前两项为0,1

import java.lang.reflect.Array;
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        char[] t = "ABABABAAABABAA".toCharArray();
        char[] p = "ABABAAABABAA".toCharArray();
		
        int[] next = new int[p.length];
        int i = 1, j;
        //next数组的创建
        while(i < next.length){
            j = i - 1;
            while(true){
                if(next[j]==0 || p[i-1] == p[next[j]-1]){
                    next[i] = next[j] + 1;
                    break;
                }
                j = next[j] - 1;
            }
            i++;
        }
        //进行字符串匹配
        i=0;
        j=0;
        while(i<t.length){
            if(t[i] == p[j]){
                i++;
                j++;
            }
            else{
                if(j==0){
                    i++;
                }
                else{
                    j = next[j] - 1;
                }
            }
            if(j == p.length){
                System.out.println("匹配成功!");
                break;
            }
        }
    }
}

算法分析

时间复杂度:O(m+n)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值