KMP算法

203 阅读3分钟

字符串匹配问题

字符串匹配问题是计算机科学中一个重要的问题,通常涉及在一个字符串中查找一个特定的子字符串。具体来说,给定一个文本串和一个模式串,字符串匹配问题的目标是找到模式串在文本串中的出现位置。

常用的字符串匹配算法包括暴力匹配、KMP算法、Boyer-Moore算法等。其中,暴力匹配算法是最简单的一种算法,它的基本思想是将模式串从文本串的第一个字符开始,逐个字符比较直到匹配或到达文本串末尾。但暴力匹配算法的时间复杂度为O(mn),其中m和n分别是模式串和文本串的长度,因此对于长文本串和长模式串来说,暴力匹配算法的效率很低。

暴力匹配算法

假设现在我们面临这样一个问题:有一个文本串S1,和一个模式串S2,现在要查找S2在S1中的位置,怎么查找呢?

采用暴力破解的的思路,假设文本串S1匹配到的位置是i,模式串S2匹配的位置是j,则:

    • 如果当前位置匹配成功,则有S1[i]==S2[j],i++,j++;
    • 如果当前位置匹配失败,即S1[i]!=S2[j],让 i=i-(j-1),j=0;

具体代码实现:

public static int ViolentMatch(String s1, String s2) {
    char[] str1 = s1.toCharArray();
    char[] str2 = s2.toCharArray();

    int i = 0;
    int j = 0;
    while (i < str1.length && j < str2.length) {
        //  如果当前位置匹配成功,则有S1[i]==S2[j],i++,j++;
        if (str1[i] == str2[j]) {
            i++;
            j++;
        } else {//  如果当前位置匹配失败,即S1[i]!=S2[j],让 i=i-(j-1),j=0;
            i = i - (j - 1);
            j = 0;
        }
    }
    //匹配成功,返回模式串s2在文本串s1中的位置,否则返回-1
    if (j == str2.length) {
        return i - j;
    } else {
        return -1;
    }
}

注意:每次匹配失败对于模式串来讲都是从0开始,而对于文本串来讲匹配失败就意味着当前匹配位置匹配不成功,需要到达下一个位置进行匹配,故i=i-(j-1

为了提高字符串匹配的效率,KMP算法和Boyer-Moore算法等更加高效的算法被提出。KMP算法通过预处理模式串,生成一个next数组,用于指导匹配过程中的后续字符比较,从而避免不必要的字符比较。而Boyer-Moore算法则是从模式串的末尾开始匹配,通过预处理模式串中字符出现的位置,选择合适的字符跳过位置,从而避免了大量的字符比较。

KMP算法

这里写下我对KMP算法详解:

KMP算法是一种字符串匹配算法,通过预处理模式串,使匹配时避免回溯到已经匹配过的位置,从而提高匹配效率。以下是KMP算法的流程:

  1. 预处理模式串
    • 计算出模式串的前缀函数(也称为部分匹配表),用于确定匹配失败时模式串的移动位置。
    • 前缀函数的计算可以通过递推实现,从模式串的第二个字符开始,依次计算每个位置的前缀函数值。
    • 前缀函数的定义为:对于模式串P的第i个字符,它的前缀函数值next[i]表示P[0:i]的最长公共前和后缀的长度,其中P[0:i]表示P的前i+1个字符。例如:在字符串中,P[0:i] 表示由字符串 P 的前 i 个字符组成的子串。其中,下标从 0 开始。例如,P[0:3] 就是由字符串 P 的前四个字符组成的子串。
  1. 在文本串中查找模式串
    • 从文本串的第一个字符开始,与模式串的第一个字符进行匹配。
    • 如果匹配成功,继续比较下一个字符,直到模式串全部匹配完成,返回当前匹配的起始位置。
    • 如果匹配失败,利用前缀函数计算出模式串的移动位置,然后将模式串向右移动,继续匹配。

KMP算法的关键在于前缀函数的计算,可以通过动态规划实现,具体流程如下:

  1. 前缀函数的第一个值next[0]默认为-1,第二个值next[1]为0。
  2. 对于i>=2的位置,计算next[i]的值:(前缀字符和后缀字符相同)
    • 定义前缀字符的下一个位置cn,对模式串来说,cn位置的字符与后缀字符的最后一个位置i-1的字符比较,即str2[i-1]==str2[cn],如果相同i位置的next[i]的值等于cn+1,即next[i]=cn+1,对比成功后i++;
    • 如果str2[i-1]!=str2[cn]并且cn > 0成立,表示i-1位置和cn位置的字符串不相等,并且cn的位置有意义,此时cn的值等于next[cn]的值(从后面往前面跳,前面的next数组的值已经确定),表示需要不断回溯,直到找到一个位置cn满足str2[i-1]!=str2[cn]
    • 如果没有找到这样的位置,令next[i]=0。继续判断下一个位置,令i++;

KMP算法的时间复杂度为O(n),其中n为文本串的长度。由于预处理了模式串,使得匹配过程中无需回溯到已经匹配过的位置,因此KMP算法的效率比暴力匹配算法高很多。

计算next数组的方法

public static int[] getNextArray(char[] str2) {
    if (str2.length == 1) {
        return new int[]{-1};
    }
    int[] next = new int[str2.length];
    next[0] = -1;
    next[1] = 0;
    int i = 2;
    int cn = 0;
    while (i < next.length) {
        if (str2[i - 1] == str2[cn]) {
            next[i++] = ++cn;
        } else if (cn > 0) {
            cn = next[cn];
        } else {
            next[i++] = 0;
        }
    }
    return next;
}

KMP主函数

public static int getIndexOf(String s1, String s2) {
    if (s1 == null || s2 == null || s2.length() < 1 || s1.length() < s2.length()) {
        return -1;
    }
    char[] str1 = s1.toCharArray();
    char[] str2 = s2.toCharArray();
    int[] next = getNextArray(str2);
    int x = 0;
    int y = 0;
    while (x < str1.length && y < str2.length) {
        if (str1[x] == str2[y]) {
            x++;
            y++;
        } else if (next[y] == -1) { // y==0
            // 表示y跳跃到0位置时依然不等于x位置上的字符
            x++;
        } else {
            // next[y]表示从y-1位置上往前推文本串和模式串相同的字符
            // 到达此判断条件表示,上面的判断未找到str1[x]==str2[y],修改y的值,继续判断,y的取值范围不超过str2的长度
            y = next[y];
        }
    }
    // 当跳出循环时y的值等于str2的长度,表示匹配到对应的字符串,返回在文本串的位置,否则未匹配,返回-1
    return y == str2.length ? x - y : -1;
}