KMP算法实现

又是群里小伙伴在问,想到俺以前也只是看过相关文章,知道个大概,如果面试到,还真不一定能写出来。便想着试试。
目前我看过两种思路:
1.类动态规划的方式
2.比较前缀和后缀

无论是哪种思路,匹配原理都是一样的,不过求状态转移数组的方式不一样。

第一种,类动态规划的思路

这种思路就与昨天做的骑士拨号器算法题类似,不过“拨号”范围变为了256个ASCII码,而状态转移的规则由匹配串得来。
比如匹配串为ababacba
下表表示了如果主串字符为啥时,下一个字符应该用匹配串的哪一位去匹配。(0~n)

尝试匹配字符\匹配串ababacba
a11315118
b02040470
c00000600
其他省略…00000000

可以发现如果某位匹配不上时,就会跳转到上次匹配上的情况,而且一个字符匹配上了,其他字符肯定是匹配不上的必然需要状态回转。利用这个现象我们就可以写出来dp数组和它的转移方程了。
dp数组定义: dp[i][j]:到达i状态时(成功匹配第i-1个字符时,也就是说i待匹配),当前字符为j时状态应该如何转移(下一个字符应该匹配匹配串的哪一位)。
转移方程: dp[i][j] = 如果匹配得上 dp[i][j]=i+1; 如果匹配不上 dp[i][j] = dp[上一次匹配上的状态][j]
注意由于刚才提到一个字符匹配上必然其他字符匹配不上,所以上一次匹配上的状态是本次真实字符的情况。
初始化: dp[0][0位置时的字符] =1;其他都为0

/**
 * @创建人 YDL
 * @创建时间 2020/5/24 17:08
 * @描述
 */
public class KMP {
    private char[] matched;
    private char[] s;
    private int len;
    private int[][] dp;
    public KMP(String matched, String s) {
        this.matched = matched.toCharArray();
        this.s = s.toCharArray();
        len = matched.length();
        dp = new int[len][256];
    }

    private void kmp(){
        dp[0][matched[0]] = 1;
        int pre = 0;
        for(int i=1;i<len;i++){
            for(int j=0;j<256;j++){
                if(j==matched[i])
                    dp[i][j] = i+1;
                else
                    dp[i][j] = dp[pre][j];
            }
            pre = dp[pre][matched[i]];
        }
    }

    public int serch(){
        kmp();
        for(int i=0,inx =0;i<s.length;i++){
            inx = dp[inx][s[i]];
            if(inx==len){
                return i-len+1;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        KMP kmp = new KMP("abababaca","abababababababaca");
        System.out.println(kmp.serch());
    }
}

比较前缀和后缀

这个求状态转移的方式,就是求某位置为止的子串最长相同前后缀。这个公共的前后缀长度就是最少回退的内容,举个例子

尝试匹配字符\匹配串ababacba
当前子串最长公共前后缀00123001

因为后缀与前缀的公共串的存在,那么如果没匹配上那么当前位置形成的后缀就可以替代之前的前缀,自然就无需再重复比较。
那么如果当前没有匹配上,且已经匹配到当前串的某个位置了,状态转移就可以这么写(就是将最大公共前后缀的位置往前移一位辣)

尝试匹配字符\匹配串ababacba
当前子串最长公共前后缀00123000
next00012300

代码如下:

    private void kmp(){
        next[0] = 0;
       for(int i=1;i<matched.length-1;i++){
           int left=0,right = 1,ans =0;
           while(right<=i){
               if(matched[left]==matched[right]){
                   left++;right++;ans++;
               }else {
                   left = 0;
                   right++;
                   ans = 0;
               }
           }
           next[i+1] = ans;
       }

    }

但是这种写法很粗糙,求next数组时,相当于也用的暴力算法求的最大公共前后缀。那么有没有什么取巧的办法呢?
这不就是缩小化子串匹配问题吗
那么按照差不多的思路
用自己来匹配自己,0位不用匹配(必然为0),然后从1位开始,如果匹配上计1(相当于)
在这里插入图片描述
后面就不一一画图了,总之被匹配串相当于后缀,匹配串相当于前缀。如果能够匹配上就同时前进,如果匹配不上就跳转上一次能够匹配上的情况。
优化后的完整版。

/**
 * @创建人 YDL
 * @创建时间 2020/5/24 17:08
 * @描述
 */
public class KMP {
    private char[] matched;
    private char[] s;
    private int len;
    private int[] next;
    public KMP(String matched, String s) {
        this.matched = matched.toCharArray();
        this.s = s.toCharArray();
        len = matched.length();
        next = new int[len];
    }

    private void kmp(){
        next[0] = -1;
       for(int i=1,inx=-1;i<matched.length;i++){
           if(inx==-1||matched[i]==matched[inx]){
               inx++;
               next[i]=inx;
           }else {
               inx = next[inx];
               i--;
           }
       }

    }

    public int serch(){
        kmp();
        for(int i=0,inx=0;i<s.length;i++){
            if(next[inx]==-1||s[i]==matched[inx])
                inx++;
            else {
                i--;
                inx = next[inx];
            }
            if(inx==len){
                return i-inx+1;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        KMP kmp = new KMP("abababaca","abababababababaca");
        System.out.println(kmp.serch());
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值