KMP算法

简介: 只有聪明人才能看见的简介~( ̄▽ ̄~)~

KMP算法用于在一个字符串(文本串)查找另一个字符串(模式串)的位置。

核心思想是,在对模式串和文本串进行遍历对比时:

根据已知的已匹配成功字符的信息。在下一个字符不匹配时,尽可能将模式串后移多个字符,达到降低时间复杂度的目的。

如何能在合理的情况尽可能跳过多的字符,得到最低的时间复杂度。需要满足如下条件:

(初始情况)部分字符匹配成功时而下一个字符不匹配

文本串 1 2 3 4 5 6
模式串 1 2 3 4 5 6
相等 相等 相等 相等 相等 不相等

条件:跳过尽可能多的字符并保证不错过某一可匹配情况(也就是假设跳过n个字符,且跳过<n个字符时,模式串和文本串必然不匹配。这就说明跳过n个字符是合理的,不会错过某种可匹配的情况)。(上方表格的123456仅表示当前正在匹配的字符的位置而不是实际字符)此时思考:如果跳过一个字符是合理的,如下

跳过一个字符为合理情况时

文本串 1 2 3 4 5 6 7
模式串 1 2 3 4 5 6
相等 相等 相等 相等 未知 未知

此时必然有文本串的2345字符和模式串的1234字符相等,也就是模式串的2345字符和模式串的1234字符相等。也就是12345有个相等前后缀,长度为4。此时可以继续往后匹配,也就是模式串的5和文本串的6进行匹配,匹配能否成功在匹配前是未知的。

跳过两个字符为合理情况时

文本串 1 2 3 4 5 6 7 8
模式串 1 2 3 4 5 6
相等 相等 相等 未知 未知 未知

再思考:如果跳过两个字符是合理情况,首先文本串的345应和模式串的123相等。且文本串的2345不可和模式串的1234相等(否则跳过一个字符后就应该继续匹配,否则可能错过能匹配成功的情况)。换句话说,也就是12345有个相等前后缀,长度为4,且不可有相等前后缀长度为3。再换句话说,12345的最长相等前后缀长度为3(对于12345的字符串来说,12345不可称为前缀或后缀,最长前缀是1234,最长后缀是2345,即字符串的最长前缀或最长后缀的长度为字符串长度减一)。

以此类推:当,模式串和文本串已经匹配部分字符,而下一字符不匹配时。模式串的,合理且尽可能多的,可跳过字符数为,已匹配字符的(长度 - 最长前后缀长度)

现在已经了解了KMP算法的核心思路,如何将其转化为算法逻辑?

首先我们有文本串和模式串。从两者的第一个字符开始进行匹配。成功则继续,不成功那么模式串应该可以向后移动多少个字符?应该和当前已匹配字符的情况有关(也就是模式串中该字符前面的字符串):已匹配0个字符?模式串直接后移一位继续比较;已匹配1个字符?模式串也后移一位(已匹配字符的(长度1-最长前后缀长度0)=1);已匹配2个字符?模式串后移(已匹配字符的(长度2-最长前后缀长度))位;... ...;已匹配n位字符... ...

很显然,按上述方法遍历到文本串结束为止。遍历前,自然需要提前求出模式串每种匹配情况的后移位数(由模式串中不匹配字符前的字符串的最长前后缀长度决定)。如下:

1 模式串 a a b a a f 备注:
2 下标 0 1 2 3 4 5
3 长度 1 2 3 4 5 6
4 最长前后缀长度(前缀表) 0 1 0 1 2 0 这就是KMP算法中的前缀表
5

假设当前字符不匹配

对应的文本串字符情况:

对应的模式串字符情况:

后移位数

b

a

1

ab

aa

1

aaa

aab

1

aabb

aaba

3

aabab

aabaa

3

aabaab

aabaaf

3

就是上文所说的,已匹配0个、1个、2个... ...n个的情况
6

模式串应该如何移动?

模式串后移后继续和文本串那个不匹配字符比较的模式串字符下标(next数组)

-1 0 1 0 1 2

为 -1 时,文本串和模式串应该均下标+1再去比较

解释上表:

第四行是其他KMP算法讲解中经常出现的名词:前缀表。它表示的是,对应下标为止的字符串中,和后缀相同的最长前缀,的长度(其实就是最长相同前后缀的长度)。它的作用显而易见,可以确定字符不匹配时后移的位数。

第五行是对应下标的那个字符和文本串不匹配时,模式串后移的位数。这个其实用处不大,因为算法的逻辑中需要对这个值处理后再使用。他只是一个中间产物。看了下面就懂了。

第六行是next数组,什么叫next数组。非常好理解:发现模式串中某下标的字符不匹配。直接将这个下标换成另一个下标去和文本串继续匹配(next完全可以理解为下一个用来匹配的下标的意思)。这就是非常适合在算法逻辑中使用一个产物值了。仔细想想就知道如何计算该值:笨方法可以每种情况都手动尝试移动然后算出值(作为验证);第二种方法,显然下一步用来匹配的下标应该 = 当前字符下标 - 后移位数(所以说第五行的后移位数也不是完全没用);第三种方法,自己画个前缀表和匹配过程的图很容易看出next数组的值(下一步用来匹配的下标)就是该位置前的字符串的最长相等前后缀。因此next数组也就是前缀表后移一位,且第一位赋值 -1。为什么是 -1,没有什么原因,第一个位置是特殊位置,这个值算出来就是 -1。

next数组和KMP的原理无关,它只是一种实现方式。比如上述所说,将前缀表右移一位再初始值赋 -1 作为next数组,这样实现的next数组可以直接在代码中使用,每当发现不匹配就把遍历的下标换成next数组对应的值,如果下标是负一那么文本串和模式串的匹配下标都加一。这种实现方式在使用上非常方便。即使直接把前缀表当为nxet数组也是可以的。无非是代码使用next数组时多做一点处理。

还有最后一个问题:如何计算最长相等前后缀(后面简称等缀,其长度简称缀长)?

和KMP的核心思路有部分相似之处。同样利用已知的信息去减少时间复杂度。

常规的计算方法是遍历每个子串的每个长度的前后缀检查是否相等。但我们可以使用递推,从长度较小的字符串的缀长推导长度较大的字符串的缀长。

假设已知字符串a1的缀长,那么对于a1加一个字符产生的字符串a2。只需比较a2的尾部字符和a1等缀的后一个字符。

如果相等,a2的缀长 = a1的缀长 + 1(为什么a2的等缀不可能更长些?反证法,假设a2有个更长的前后缀,去掉尾部的字符会发现a1也有更长的前后缀)。

如果不相等,应该用a1的等缀的等缀。假设a1为aabaa,如下图。a1的等缀的等缀相当于a1的二级等缀,如果a2的尾部字符 = a1二级等缀的后一个字符,a2的缀长 = a1二级缀长 + 1。(a2的等缀有没有可能更长些?同样是反证法,不可能)

image.gif编辑

如果仍不相等,使用三级等缀,四级等缀,直到缀长为0了。此时比较a2的尾部字符和n级等缀的后一个字符,相等则a2的缀长为1,否则为0。

现在已经很清楚如何实现KMP算法,下面给出java代码实现:

package com.yt.string;
import java.util.Arrays;
import java.util.Objects;
/**
 * @类描述: 找出字符串中第一个匹配的下标,LeetCode 28
 * @作者: **
 * @日期: ***
 */
public class StrStr {
    public static int strStr(String haystack, String needle) {
        //KMP算法
        System.out.println("haystack: " + haystack);
        System.out.println("needle:   " + needle);
        //如果模式串为空返回0
        Objects.requireNonNull(haystack);
        Objects.requireNonNull(needle);
        if ("".equals(needle)) {
            return 0;
        }
        int[] prefixTable = calculatePrefixTable(needle);
        int[] next = convertToNext(prefixTable);
        int res = doStrStr(haystack, needle, next);
        System.out.println("        结果:" + res);
        return res;
    }
    /**
     * @param needle 模式串
     * @return 前缀表
     */
    private static int[] calculatePrefixTable(String needle) {
        int[] prefixTable = new int[needle.length()];
        prefixTable[0] = 0;
        for (int i = 1; i < prefixTable.length; i++) {
            int nIndex = i - 1;// 第i个字符前的字符串的n级等缀在前缀表的索引
            int nLength = prefixTable[nIndex];// 第i个字符前的字符串的n级缀长
            while (true) {
                if (needle.charAt(i) == needle.charAt(nLength)) {
                    prefixTable[i] = nLength + 1;
                    break;
                } else {
                    if (nLength == 0) {
                        prefixTable[i] = 0;
                        break;
                    }
                    nLength = prefixTable[nLength - 1];
                }
            }
        }
        System.out.println("    前缀表:   " + Arrays.toString(prefixTable));
        return prefixTable;
    }
    /**
     * 将前缀表转换为next数组,使用前缀表右移一位并初始值赋-1实现
     *
     * @param prefixTable 前缀表
     * @return next数组
     */
    private static int[] convertToNext(int[] prefixTable) {
        int[] next = new int[prefixTable.length];
        next[0] = -1;
        System.arraycopy(prefixTable, 0, next, 1, next.length - 1);
        System.out.println("    next数组:" + Arrays.toString(next));
        return next;
    }
    /**
     * 找出字符串中第1个匹配项的下标
     *
     * @param haystack 文本串
     * @param needle   模式串
     * @param next     next数组
     * @return 匹配项的下标,如果没有匹配项返回-1
     */
    private static int doStrStr(String haystack, String needle, int[] next) {
        int i = 0, j = 0;// i为haystack下标,j为needle下标
        while (i < haystack.length() && j < needle.length()) {
            if (haystack.charAt(i) == needle.charAt(j)) {
                i++;
                j++;
                if (j == needle.length()) {
                    return i - needle.length();
                }
            } else {
                j = next[j];
                if (j == -1) {
                    i++;
                    j++;
                }
            }
        }
        return -1;
    }
}

image.gif

自行编写测试,可多插入一些提示输出助于理解。

目录
相关文章
|
5月前
|
算法
数据结构中的KMP算法及其改进算法
KMP算法通过引入部分匹配表,有效避免了重复计算,从而将字符串匹配的时间复杂度降低到O(m+n)。通过进一步优化next数组,KMP算法的效率得到了进一步提升。对于大规模字符串匹配问题,KMP算法及其改进算法提供了高效的解决方案,是计算机科学领域的经典算法之一。
82 3
|
1月前
|
算法
第四章 KMP算法理论基础
第四章 KMP算法理论基础
18 0
|
1月前
|
算法
KMP算法
KMP算法
25 0
|
3月前
|
算法 C++
A : DS串应用–KMP算法
这篇文章提供了KMP算法的C++实现,包括计算模式串的next数组和在主串中查找模式串位置的函数,用于演示KMP算法的基本应用。
|
4月前
|
数据采集 算法 JavaScript
揭开JavaScript字符串搜索的秘密:indexOf、includes与KMP算法
JavaScript字符串搜索涵盖`indexOf`、`includes`及KMP算法。`indexOf`返回子字符串位置,`includes`检查是否包含子字符串。KMP是高效的搜索算法,尤其适合长模式匹配。示例展示了如何在数据采集(如网页爬虫)中使用这些方法,结合代理IP进行安全搜索。代码示例中,搜索百度新闻结果并检测是否含有特定字符串。学习这些技术能提升编程效率和性能。
117 1
揭开JavaScript字符串搜索的秘密:indexOf、includes与KMP算法
|
3月前
|
算法
KMP算法
KMP算法
26 0
|
4月前
|
算法 Java
KMP算法详解及其在字符串匹配中的应用
KMP算法详解及其在字符串匹配中的应用
|
5月前
|
人工智能 算法 BI
一篇文章讲明白KMP算法(俗称看毛片算法)
一篇文章讲明白KMP算法(俗称看毛片算法)
55 0
|
5月前
|
算法 Java
Java数据结构与算法:字符串匹配算法之KMP算法
Java数据结构与算法:字符串匹配算法之KMP算法
|
6月前
|
算法