[java] KMP算法

本文深入讲解KMP算法的原理及应用,对比BF和RK算法,详细介绍如何通过提取并利用加速匹配信息来消除主串指针回溯,提高匹配效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言

题目: 28. 实现 strStr()

实现 strStr() 函数。

给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。

示例 1:

输入: haystack = "hello", needle = "ll"
输出: 2

示例 2:

输入: haystack = "aaaaa", needle = "bba"
输出: -1

说明:

  • 当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。

  • 对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。

解法1:双指针暴力(BF(Brute Force))

class Solution {
    public int strStr(String haystack, String needle) {
        if(haystack.length()<needle.length()) return -1;
        int l=0;
        while(l<haystack.length()-needle.length()+1){
            if(needle.equals(haystack.substring(l,l+needle.length()))) return l;
            l++;
        }
        return -1;
    }
}

解法2:哈希码(RK(Rabin-Karp))
在这里插入图片描述
这份代码能基本体现思想以及解决大部分用例,但是对于一些长度超级长的字符串,会出现溢出导致结果不对,我一直没处理好,先这样吧。

class Solution {
    public int strStr(String haystack, String needle) {
        if(haystack.length()<needle.length()) return -1;
        long hashcode_needle=0,hash_temp=0;
        for(int i=0;i<needle.length();i++){
            hashcode_needle+=((needle.charAt(i)-'a')*Math.pow(26,i));
            hash_temp+=((haystack.charAt(i)-'a')*Math.pow(26,i));
        }
        int l=0;
        if(hash_temp==hashcode_needle) return l;
        while(l<haystack.length()-needle.length()){
            hash_temp-=(haystack.charAt(l)-'a');
            hash_temp/=26;
            hash_temp+=((haystack.charAt(l+needle.length())-'a')*Math.pow(26,needle.length()-1));
            l++;
            if(hash_temp==hashcode_needle) return l;
        }
        return -1;
    }
}

KMP算法

引入

为了引入KMP算法以及为了展示KMP算法的优越性,我先举个字符串匹配的例子,例如还是一开始的那一题
在这里插入图片描述

好,如果字符串是这个样子,我们最暴力的解法是什么,就是逐个去比嘛,先把needle放在最左边,然后一个个去比,当走到下面这一步发现不匹配了
在这里插入图片描述
这个时候needle就往右移动一格重新来比嘛
在这里插入图片描述

以此类推,这个随便一看就知道费时费力,肯定不好用,这个时候KMP就出来了。

KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 暴力算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。

正题:KMP

上面说道 KMP 算法主要是通过消除主串指针的回溯来提高匹配的效率的,那么,它是怎么样来消除回溯的呢?就是因为它提取并运用了加速匹配的信息!

举个简单的例子,比如刚刚那个
在这里插入图片描述
当逐个匹配到这一步的时候,暴力解法是右移一格重新开始比。KMP就觉得重新开始比好麻烦,它先观察了一下needle串的特征,发现这个时候needle串可以直接跳到下面这一步来比
在这里插入图片描述
这可能在你看来只是KMP小小的一步,但是实际上对整个算法流程改进了不止一点点。首先

  • 当发现不匹配时,needle串没有按照暴力法那样右移一格重新开始比,而是直接找到一个前缀已经匹配正确的位置和当前位置再去比
  • 当needle串这样走的时候,我们就会惊奇的发现haystack串根本不需要一点回溯,直接一次遍历完就好了

为了更好的理解这个过程,我们把上面的题目一步步的执行一遍。

  1. 两个串从第一个位置开始比,一直右移到第五个位置发现不匹配
    在这里插入图片描述
  2. 这个时候needle串跳到下一个位置,继续和haystack下标为4的那个B继续比较
    在这里插入图片描述
  3. 发现仍然不匹配,继续跳到下一个位置
    在这里插入图片描述
  4. 发现needle的第一个字符也不匹配了,这个时候needle右移一格,重新开始比,也就是只要needle的第一个字符都不匹配就右移一格
    在这里插入图片描述
  5. 发现已经匹配了,结束。

整个过程可以发现,haystck是完全没有回溯的,needle也通过跳转那一步减少了大量的回溯过程。那么问题来了,跳转那一步到底是什么原理呢,这个解释起来比较复杂,比较正规的解释就是前缀和后缀相同,比如ABCAB的相同前后缀就是AB,这个时候如果needle是ABCABD,当比较到D的时候不匹配这个时候怎么利用这个公共前后缀来跳转呢。首先我们要搞清楚当执行到这一步的时候,有哪些信息是已知的了:

首先,集然已经开始匹配needle的D了,说明前面ABCAB肯定已经是匹配的,如下图:
在这里插入图片描述
这个时候就可以用到前后缀了,既然ABCAB已经重合了,那么我们就可以直接把前缀移动到后缀的位置上就好了
在这里插入图片描述
跳转的原理就是这么简单,紧接着我们就会发现,跳转的实现似乎只提取了needle串的特征,当某个位置不匹配时,needle串跳转到下一个位置继续比,如果跳到开头还不匹配就用needle的开头和haystack的下一个位置比。这也正是KMP算法厉害的另外一个点,只要提取needle串的特征,就可以和任意一个haystack串去匹配。

next数组

上面说了那么多的跳转,其实官方一点就是定义一个next数组,比如上面的图,D的下标是5,D不匹配时跳转过来比较的是C,下标为2,所以next[5]=2。而第一个元素的next也就是next[0]设为-1,这样如果跳转的下标是-1就在haystack上右移一个。

next数组代码如下

public int[] Getnext(String t,int[] next)
    {
        int j=0,k=-1;
        next[0]=-1;
        while(j<t.length()-1)
        {
            if(k == -1 || t.charAt(j) == t.charAt(k))
            {
                j++;k++;
                next[j] = k;
            }
            else k = next[k];
        }
        return next;
    }

当 j 的值为 0 或 1 的时候,它们的 k 值都为 0,即 next[0] = 0、next[1] =0。但是为了后面 k 值计算的方便,我们将 next[0] 的值设置成 -1。代码里面k的值可以理解为公共前后缀的长度。

needle串简化定义为字符串t

  1. 当t[k]==t[j],那肯定next[j+1]=k+1,因为前k个都是重合的。
    在这里插入图片描述
  2. 当t[k]!=t[j]时,k=next[k],这一步我看懂了但是我讲不清楚,正应了那句话只可意会不可言传。
    在这里插入图片描述

next数组的一点改进

我们看这个例子
在这里插入图片描述
如果按照上面的代码走的话,next[5]=2,但是我们发现其实t[5]和t[2]是一样的,即使5先跳到了2,它下一步还是要跳到next[2],所以这里可以改进一下。

public int[] Getnext(String t,int[] next)
    {
        int j=0,k=-1;
        next[0]=-1;
        while(j<t.length()-1)
        {
            if(k == -1 || t.charAt(j) == t.charAt(k))
            {
                j++;k++;
                if(t.charAt(j) == t.charAt(k))//当两个字符相同时,就跳过
                    next[j] = next[k];
                else
                    next[j] = k;
            }
            else k = next[k];
        }
        return next;
    }

KMP代码

```java
	int KMP(String s,String t)
	{
        int[] next=new int[t.length()];
        int i=0,j=0;
        next=Getnext(t,next);
        while(i<s.length() && j<t.length())
        {
            if(j==-1 || s.charAt(i)==t.charAt(j))
            {
                i++;
                j++;
            }
            else j=next[j];               //j回退。。。
        }
        if(j>=t.length())
            return i-t.length();         //匹配成功,返回子串的位置
        else
            return -1;                  //没找到
	}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值