【算法和数据结构】KMP算法

KMP算法是一种字符串匹配算法,这里拿leetcode第28题举例,题目如下:
28. 找出字符串中第一个匹配项的下标

1.暴力算法

最开始看到这个题的时候,我先直接就是一个暴力匹配,突出一个简单无脑。

	public static int strStr(String haystack,String needle){
        char[] haystackArr = haystack.toCharArray();
        char[] needleArr = needle.toCharArray();
        int i = 0;
        int j = 0;
        int k = 0;
        while (i < haystackArr.length && j < needleArr.length){
            if (haystackArr[i] == needleArr[j]){
                i ++;
                j ++;
            }
            else {
                k++;
                i = k;
                j = 0;
            }
        }
        if (j == needleArr.length){
            return k;
        }
        return -1;
    }

下面是暴力匹配的流程:
在这里插入图片描述
在这里插入图片描述
可以看出暴力破解是让主串和子串的字符逐个匹配,直到i=2,j=2。由于该字符匹配失败,所以指针进行了回退,退到了i=1,j=0。

2.KMP 匹配算法

KMP算法的核心思路就是,当出现字符串不匹配时,可以利用之前已经匹配的文本内容,可以利用这些信息来减少回退的步数。
用"sabbutsadc"和"sad"来举例的话,我们可以看出,
1.“sad"可以匹配上"sabbutsadc"开头的"sa”;
2."sad"子串的首字母’s’和后面的字母都不相同;
3.所以当’d’字符匹配失败时,首字母’s’必然不会和前面的’sa’相匹配。
4.此时按照暴力破解的算法,我们应该将子串的指针回到’s’,主串的指针回到’a’来开始重新匹配。
5.但是在KMP算法中,我们充分利用了2和3,因此,我们将子串的指针回到’s’,主串的指针只需要保持在’d’就可以了。
这样一来匹配流程就变成了这样:
在这里插入图片描述
在这里插入图片描述
可以看出,在匹配的过程中,当匹配不上的时候,i指针不再进行回退了,而j指针回退到了0的位置。其实此时j指针回退的位置是有说法的,那么要如何确定j指针回退的位置呢,这个就要由匹配字符前面的字符串的 「最长前后缀相同长度」来决定了。

2.1最长前后缀相同长度

最长前后缀相同长度指的是一个字符串前缀和后缀相等的最长元素。
举个例子,上文的"sad"中:
前缀有s,sa;
后缀有d,ad;
因此前后缀相同长度为0,所以我们将j指针回退到了0的位置。
再来看一个主串为"ababadefgh",子串为"ababac"的例子:
子串前缀为:a,ab,aba,abab
子串后缀为:a,ba,aba,baba
前后缀相同长度有1和3,那么最长前后缀相同长度就是3。
所以当j=5和i=5匹配不上的时候,我们保持i指针不动,j指针回退到3的位置,重新进行匹配。
在这里插入图片描述

最长前后缀相同长度就是我们用来判断主串指针i回退位置的依据。
需要注意的是,前缀后缀不能为同一个元素,比如:在 字符串"a"中,前缀后缀不能同时为"a",所以这个最长前后缀长度为 0。

2.2 next数组

上面我们说最长前后缀相同长度是我们用来判断主串指针i回退位置的依据,next数组就是用来记录子串回退位置的数组。next[j]的公式如下:
1.next[0] = -1;
2.next[j] = 匹配字符前面的字符串的「最长前后缀相同长度」。
我们继续用子串"ababac"作为例子:

  • j=0,匹配字符前面的字符串为null,所以next[0] = -1;
  • j=1,匹配字符前面的字符串为"a",所以next[1] = 0;
  • j=2,匹配字符前面的字符串为"ab",所以next[2] = 0;
  • j=3,匹配字符前面的字符串为"aba",所以next[3] = 1;
  • j=4,匹配字符前面的字符串为"abab",所以next[4] = 2;
  • j=5,匹配字符前面的字符串为"ababa",所以next[5] = 3;
    所以next[] = [-1,0,0,1,2,3]。

2.3实现KMP算法

前面理论已经讲得差不多了,接下来我们用代码实现一下KMP算法,leetcode的28题可以写为:

	public static int strStrKMP(String haystack, String needle) {
        char[] haystackArr = haystack.toCharArray();
        char[] needleArr = needle.toCharArray();
        int[] next = getNext(needleArr);
        int i = 0;
        int j = 0;
        while (i < haystackArr.length && j < needleArr.length){
            if(j == -1 || haystackArr[i] == needleArr[j]){
                //继续往后匹配
                i++;
                j++;
            }else{
                //回溯j指针到next[j]的位置
                j = next[j];
            }
        }
        if(j == needleArr.length){
            return i - j;
        }
        return -1;
    }
	private static int[] getNext(char[] needle) {
    	int[] next = new int[needle.length];
    	int k = -1;
    	int j = 0;
    	next[0] = -1;
    	while (j < needle.length - 1) {
        	if (k == -1 || needle[j] == needle[k]) {
            	k++;
            	j++;
            	next[j] = k;
        	} else {
            	k = next[k];
        	}
    	}
    	return next;
	}

3.算法优化

上面的KMP算法在某些特殊情况下仍然不太完善,比如当主串为"aaaaaxxxxx",子串为"aaaaab"时。

  • j=0,匹配字符前面的字符串为null,所以next[0] = -1;
  • j=1,匹配字符前面的字符串为"a",所以next[1] = 0;
  • j=2,匹配字符前面的字符串为"aa",所以next[2] = 1;
  • j=3,匹配字符前面的字符串为"aaa",所以next[3] = 2;
  • j=4,匹配字符前面的字符串为"aaaa",所以next[4] = 3;
  • j=5,匹配字符前面的字符串为"aaaaa",所以next[5] = 4;
    所以next[] = [-1,0,1,2,3,4]。
    那么匹配流程如下:
    在这里插入图片描述
    从图中我们可以看出,子串的前5个字符都是’a’,所以当j=4匹配失败时,前面4个字符的匹配必然也是失败的。
    所以我们要对next数组进行优化,优化的思路就在于:
    后面的’a’已经不匹配了,那么前面的’a’必然也不能匹配。
    因此next数组我们可以优化为:
  • j=0,匹配字符前面的字符串为null,所以next[0] = -1;
  • j=1,让next[1] = next[0] = -1;
  • j=2,让next[2] = next[1] = -1;
  • j=3,让next[3] = next[2] = -1;
  • j=4,让next[4] = next[3] = -1;
  • j=5,该字符和前面不相同,所以next[5] = 4;
    这样做的意义在于,当j=4的字符不匹配时,可以直接让指针回退到上一个不相等的字符。
    代码的修改非常简单,我们只需要在getNext()方法中加一个判断即可:
private static int[] getNext(char[] needle){
        int[] next = new int[needle.length];
        int k = -1;
        int j = 0;
        next[0] = -1;
        while (j < needle.length-1){
            if(k==-1 || needle[k]==needle[j]){
                k++;
                j++;
                //这里判断前后两个字符是不是相等
                if(needle[k]==needle[j]){
                	//如果两个字符相等,那后一个匹配不上前一个必然也匹配不上
                    next[j] = next[k];
                }else{
                    next[j] = k;
                }
            }else{
                k = next[k];
            }
        }
        return next;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值