KMP算法

术语介绍:最长前缀+后缀匹配子串
在这里插入图片描述

KMP算法:

  1. 对模式串预处理,生成next数组

  2. 进入主循环,遍历主串

    2.1. 比较主串和模式串的字符

    2.2. 如果发现坏字符,查询next数组,得到匹配前缀所对应的最长可匹配前缀子串,移动模式串到对应位置

    2.3.如果当前字符匹配,继续循环

时间复杂度是O(m+n)
空间复杂度是O(m),m为模式串的长度,n为主串长度

// next[i] 表示s[0...i+1]中,最长可匹配的前缀子串的下一个位置
    private static int[] getNexts(String pattern) {
        int[] next = new int[pattern.length()];
        // i为next数组索引,等待填充的下一个元素位置
        // j代表最长可匹配前缀子串的下一个位置,所以next[i]要置为j
        int j = 0;
        for (int i=2; i<pattern.length(); i++) {
            while (j != 0 && pattern.charAt(j) != pattern.charAt(i-1)) {
                //发现不匹配,找当前最长可匹配前缀子串的最长可匹配前缀子串,即j= next[j],直到匹配到(更新next数组为j)或者j=0(next数组为0)
                j = next[j];
            }
            // 如果已经匹配上,最长可匹配前缀子串的位置后移1位
            if (pattern.charAt(j) == pattern.charAt(i-1)) {
                j++;
            }
            next[i] = j;
        }
        return next;
    }

    // kmp算法的特殊性,在于每次都找最长前缀/后缀匹配子串bothSub,移动模式串的时候不用每次暴力移动1,可以找到bothSub的下一个位置进行继续匹配,不过并不是直接模式串后移bothSub的长度
    // 另一个难点在于getNexts数组,其实理解了也没那么难
    private int kmp(String str,String pattern){
        int[] next = getNexts(pattern);
        int m = pattern.length();
        int n = str.length();
        // j代表模式串中当前最长可匹配前缀子串的下一个位置
        int j = 0;
        // 遍历主串
        for(int i = 0;i< n;i++){
           while(j!=0 && pattern.charAt(j) != str.charAt(i)){
               // 遇到坏字符,从next数组中找pattern中最长前缀子串的下一个位置,j从此位置继续和主串的i位置进行匹配。不要理解为此时模式串一次性移动了next[j]哦
               j = next[j];
           }
           if(pattern.charAt(j) == str.charAt(i)){
               j++;
           }
           // 全部匹配完成 返回主串中匹配串的头部索引:(i+1 -m)
            if(j == m){
                return i-m+1;
            }
        }
        return -1;
    }

public static void main(String[] args) {
    String str = "ATGTGAGCTGGTGTGTGCFAA";
    String pattern = "GTGTGCF";
    int index = kmp(str, pattern);
    System.out.println("首次出现位置:" + index);
}

附上力扣题型,28题 找出字符串中第一个匹配项的下标

//给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。
//如果 needle 不是 haystack 的一部分,则返回 -1 。 
//
// 
//
// 示例 1: 
//
// 
//输入:haystack = "sadbutsad", needle = "sad"
//输出:0
//解释:"sad" 在下标 0 和 6 处匹配。
//第一个匹配项的下标是 0 ,所以返回 0 。
// 
//
// 示例 2: 
//
// 
//输入:haystack = "leetcode", needle = "leeto"
//输出:-1
//解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。
// 
//
// 
//
// 提示: 
//
// 
// 1 <= haystack.length, needle.length <= 10⁴ 
// haystack 和 needle 仅由小写英文字符组成 
// 
//
// Related Topics 双指针 字符串 字符串匹配 👍 2161 👎 0

package leetcode.editor.cn;

import com.sun.xml.internal.ws.util.StringUtils;

import java.util.ArrayList;
import java.util.List;

//Java:找出字符串中第一个匹配项的下标
public class FindTheIndexOfTheFirstOccurrenceInAStringXXX28{
public static void main(String[] args) {
Solution solution = new FindTheIndexOfTheFirstOccurrenceInAStringXXX28().new Solution();
// TO TEST
    int i = solution.strStr("sadbutsad", "saed");
    System.out.println(i);
}
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
    public int strStr(String haystack, String needle) {

        /*
        // 普通做法 又称BF(Brute Force 暴力匹配),时间复杂度为m*n
        // 但是就是普通的字符串匹配的解法,因为一般字符串都比较短,这个复杂度可以接受,并且非常简单,Java中的indexOf()之类的都是这么实现的
        if(needle == null || haystack == null) return -1;
        int fast = 0,slow = 0;
        while(fast<haystack.length()){
            int f =0;
            while(f < needle.length() && fast < haystack.length() &&
                    haystack.charAt(fast) == needle.charAt(f)){
                fast++;
                f++;
            }
            if(f == needle.length()){
                return slow;
            }
            slow++;
            fast = slow;
        }
        return -1;*/
        // 第二种办法是,RK算法,名字是两个人命名 不重要。重要的是方法:获取原串中所有跟匹配串相等长度的子串,根据hash值来判断子串和匹配串是否相等
        // 那么时间复杂度1:获取所有子串的hash值:最快为只遍历一遍主串,O(n);复杂度2:子串和匹配串的比较,比较是O(1),共比较n-m+1次,即O(n),所以RK算法的时间复杂度是o(n)
        // 再说一下获取hash值的方法,可以直接指定12345,也可以借助十进制的方式求hash值,这样可以在遍历子串求hash值的时候可以根据前一个子串得出当前子串的hash值,只有O(n)的复杂度
        // 例如,如果字符串都是a-z的小写字母,那么可以指定a的hash值为0,b为1,那么"abc"子串的hash值为0*26^2 + 1*26+2。这样的话,遍历子串求hash值的时候,前后两个子串之间的差值就很有规律(这里的规律就不列出了,纯数学),所以复杂度就只有n


        // 第三种,KMP做法,理论上讲就是讲复杂度降低到m+n,
        // 也就是说去掉一些冗余的操作,找到每次匹配串匹配的后移最大值
        // 有两个概念:最长子前缀和最长子后缀,找最长的相等前缀和后缀(最长公共前后子缀)。例如ababa的最长子前缀为aba,最长后前缀为aba;abaaaab为ab。
        // 找到之后,当需要后移时,每次匹配串后移的距离就是最长子后缀的首字符位置,降低复杂度,这里就需要一个next[]数组来保存最长子后缀的首字符位置,注意next的0和1索引位可置为0,1对应字符串的首位,以便后续处理
        // 这里说明一个next数组的怎么获取,从i=0,j=2开始,如果字符串p[i]!=p[j-1] next[i]=i;
        int n = needle.length();
        char[] p = needle.toCharArray();
        int[] next = new int[n+1];
        for(int j = 2, i = 0; j <= n; j++)  //从j = 2开始计算
        {
            // 要用while循环;
            // 这里i=next[i]解释:此时左指针走到了i,右指针走到了j,当p[i] != p[j - 1]时,找公共最长后缀的首字符位置,不就是(0-i)之间的子串的公共最长后缀的首字符位置吗(因为此时0-i和x-j两串就是最长子缀,在此范围里找最长就是在0-i之间找最长公共或者说是在x-j之间找),也就是next[i]
            while(i > 0 && p[i] != p[j - 1]) i = next[i];
            if(p[i] == p[j - 1]) i++;
            next[j] = i;
        }
        // 下一步就是匹配了,
        char[] s = haystack.toCharArray();
        List<Integer> res  = new ArrayList<>();
        for(int i = 0, j = 0; i < s.length; i++)
        {
            // 匹配失败 调整 j 的位置
            while(j > 0 && s[i] != p[j]) j = next[j];
            // 当前字符匹配成功, j++继续匹配
            if(s[i] == p[j]) j++;
            if(j == p.length)
            {
                // j到了子串p末尾,说明 完全匹配到了一个答案,记录结果,然后继续调整j寻找
                res.add(i - j + 1);
                j = next[j];
            }
        }
        return res.isEmpty()?-1:res.get(0);
    }
}
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,下面我来为您讲解使用KMP算法解决字符串匹配问题的方法。 KMP算法是一种比较高效的字符串匹配算法,它的核心思想是利用已经匹配过的信息,尽可能减少不必要的字符比较次数,从而提高匹配效率。具体实现时,KMP算法利用前缀和后缀的概念,对于每个字符,计算出它的最长前缀和最长后缀的匹配长度,然后根据这些信息来移动模式串,从而减少比较次数。 下面是使用KMP算法解决字符串匹配问题的代码实现: ```python def str_str(s, n): if not n: return 0 if not s: return -1 m, k = len(s), len(n) next = get_next(n) i = j = 0 while i < m and j < k: if j == -1 or s[i] == n[j]: i += 1 j += 1 else: j = next[j] if j == k: return i - k else: return -1 def get_next(n): k, j = -1, 0 next = [-1] * len(n) while j < len(n) - 1: if k == -1 or n[k] == n[j]: k += 1 j += 1 next[j] = k else: k = next[k] return next ``` 需要注意的是,KMP算法中的next数组表示的是模式串中每个位置的最长前缀和最长后缀的匹配长度,而不是暴力匹配算法中的每个位置的最长前缀和最长后缀。因此,KMP算法中的next数组需要先计算出来,然后再根据它来移动模式串。 接下来,我来给您提供一组测试用例,证明KMP算法的正确性: ```python assert str_str("hello", "ll") == 2 assert str_str("aaaaa", "bba") == -1 assert str_str("mississippi", "issip") == 4 ``` 这些测试用例可以验证KMP算法的正确性。相比暴力匹配算法KMP算法的时间复杂度为O(m+n),可以在较短的时间内解决字符串匹配问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值