一文读懂kmp算法

自己创建了个博客网站,hofe’s blog ,欢迎大家收藏_
博客将会两边同步更新,体验会更佳。

一、KMP算法是什么?

kmp算法是用于解决字符串匹配的算法
本文用约定用 pat 表示模式串,长度为 Mtxt 表示文本串,长度为 N。KMP 算法是在 txt 中查找子串 pat,如果存在,返回这个子串的起始索引,否则返回 -1

首先来看一道例题:link

题目描述

字符串旋转:

给定两字符串A和B,如果能将A从中间某个位置分割为左右两部分字符串(都不为空串),并将左边的字符串移动到右边字符串后面组成新的字符串可以变为字符串B时返回true。

例如:如果A=‘youzan’,B=‘zanyou’,A按‘you’‘zan’切割换位后得到‘zanyou’和B相同返回true。

输入描述:

2个不为空的字符串(说明:输入一个字符串以英文分号";"分割为2个字符串)
例如:youzan;zanyou 即为A=‘youzan’,B=‘zanyou’
输出描述:
输出true或false(表示是否能按要求匹配两个字符串)

示例1

输入
youzan;zanyou
输出
true

二、为什么要用KMP算法?

我们知道从字符串中寻找子串的问题一般可以用暴力遍历来解决,每一次只能往后移动一个位置,且遇到不匹配的字符时,指针需要回溯,时间复杂度为O(n*m)

int search(String pat, String txt) {
    int M = pat.length;
    int N = txt.length;
    for (int i = 0; i < N - M; i++) {
        int j;
        for (j = 0; j < M; j++) {
            if (pat[j] != txt[i+j])
                break;
        }
        // pat 全都匹配了
        if (j == M) return i;
    }
    // txt 中不存在 pat 子串
    return -1;
}

KMP算法具有两个特性:

  1. 仅仅后移模式串
  2. 指针不回溯

什么意思呢?请看以下两种情况下的kmp算法

(1)txt = “aaaaaaab” pat = “aaab”

(2) txt = “aaacaaab” pat = “aaab”:

到此为止大家应该已经理解为什么要用kmp算法代替暴力遍历找子串了

三、KMP怎么解决问题?

现在来看看kmp如何实现

首先我们可以知道显著的区别是遇到失配的情况主串不必再回退到当前的下一个字符开始匹配,而是保持不变,不进行回溯。由子串进行回溯重新匹配,而且回退之后,回退点之前的元素需要和主串匹配才行,这样主串才不用回退。那关键就在于子串回退的位置,它该回退多少的问题。

要保证一个模式串进行移动j位之后,回退点之前的元素仍然和主串匹配,说明模式串的(真)前后缀有一段是相同的。

步骤1:解决回退几位的问题

这里引入next[j]表示失配点j,对于字符串aaab它有以下几种情况(这里表示的都是真前后缀)

next[0]表示失配点在pat[0]=a这个位置,也就是它前面的元素为"",没有前缀与后缀,令next[0] = -1;

next[1]表示失配点在pat[1]=a这个位置,也就是它前面的元素为a,没有前缀与后缀,故next[0] = 0;

next[2]表示失配点在pat[2]=a这个位置,也就是它前面的元素为aa,有前缀与后缀a,故next[1] = 1;

next[3]表示失配点在pat[3]=b这个位置,也就是它前面的元素为aaa,有前缀与后缀aa,故next[2] = 2;

那就可以得到这串子串的部分匹配表

i 0 1 2 3
pat[] a a a b
next[] -1 0 1 2

那现在用这个匹配表验证前面两种主串

(1)txt = “aaaaaaab” pat = “aaab”

失配点 j = 3, pat[j] = b, next[j] = 2;也就是说j回退到2的位置,往回走1位, 即pat[2] = a

(2)txt = “aaacaaab” pat = “aaab”:

图片是经过优化算法得出的步骤,因为知道子串中未出现过c,所以可以直接回退到起点,但算法优化前是按照下面流程走的:

第一次: 失配点 j = 3, pat[j] = b, next[j] = 2;j回退到2的位置, 即pat[2] = a

第二次: 失配点 j = 2, pat[j] = a, next[j] = 1;j回退到1的位置, 即pat[1] = a

第三次: 失配点 j = 1, pat[j] = a, next[j] = 0;j回退到next[j]的位置, 即pat[0] = a

第四次: 失配点 j = 0, pat[j] = a, next[j] = -1; j回退到next[j]的位置, 即pat[-1] = “”

代码如下:

public static int kmp(char[] txt, char[] pat){
        int[] next = getNext(pat);
        int i = 0, j = 0;
        while (i < txt.length && j < pat.length) {
            if (txt[i] == pat[j]) {
                i++;
                j++;
            } else if (next[j] != -1) {
                j = next[j];
            } else {
                // next[j] = -1说明这时已经在头部位置之前了
                i++;
            }
        }
        return j == pat.length ? i-j : -1;
    }
步骤2:解决求next数组的问题

使用双指针遍历该位置前的串中前后缀相同的值,可令next[0] = -1,代表无匹配,回退到该字符串前一位置;

令next[1] = 0,真前后缀是不包含自身的。

代码如下:

public class static int[] getNext(char[] pat){
        int[] next = new int[arr.length];
        next[0] = -1;
        next[1] = 0;
        int i = 2;
        int j = 0;
        while(i < pat.length){
            if(pat[i-1] == pat[j]){
                next[i] = j + 1;
                i++;
                j++;
            }else if(j > 0){
                // 首尾匹配,但次前缀和次后缀不匹配;
                // 那该位置的值就等于子串次前缀位置的值
                j = next[j];
            }else{
                // 首尾不匹配
                next[i] = 0;
                i++;
            }
        }
        return next;
    }

完整代码:

import java.util.*;

public class Main{
    
    public static int kmp(char[] txt, char[] pat){
        int[] next = getNext(pat);
        int i = 0, j = 0;
        while (i < txt.length && j < pat.length) {
            if (txt[i] == pat[j]) {
                i++;
                j++;
            } else if (next[j] != -1) {
                j = next[j];
            } else {
                // next[j] = -1说明这时已经在头部位置之前了
                i++;
            }
        }
        return j == pat.length ? i-j : -1;
    }
    public static int[] getNext(char[] pat){
        int[] next = new int[pat.length];
        next[0] = -1;
        next[1] = 0;
        int i = 2;
        int j = 0;
        while(i < pat.length){
            if(pat[i-1] == pat[j]){
                next[i] = j + 1;
                i++;
                j++;
            }else if(j > 0){
                // 首尾匹配,但次前缀和次后缀不匹配;
                // 那该位置的值就等于子串次前缀位置的值
                j = next[j];
            }else{
                // 首尾不匹配
                next[i] = 0;
                i++;
            }
        }
        return next;
    }
    
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        String str = sc.nextLine();
        String s1 = str.split(";")[0];
        String s2 = str.split(";")[1];
        if (s1.length() != s2.length()) {
            System.out.println("false");
            return;
        }
        // 转化为判断txt是否包含sub
        String txt = s1 + s1;
        String sub = s2;
        System.out.println(
            kmp(txt.toCharArray(), sub.toCharArray())==-1?false:true);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章