KMP 算法学习之旅

385 阅读1分钟

「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战」。

一、应用场景

字符串匹配问题

  • 有一个字符串 str = "absabc1bac",和一个字串 match = "abc"
  • 现在要判断 str 是否包含 str2,如果存在,就返回第一次出现的位置,如果没有,则返回-1

二、暴力匹配法

如果用暴力匹配的思路,并假设现在str匹配到i位置,字串match匹配到j位置,则有:

  1. 如果当前字符串匹配成功(即str[i] == match[j]),则i++,j++,继续匹配下一个字符
  2. 如果匹配失败(即str[i] != match[j]),令i=i-(j-1),j=0。相当于每次匹配失败时,i回溯,j被置为0。
  3. 用暴力方法解决的话会有大量的回溯,每次移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间。
  4. 暴力匹配算法实现
  5. 代码
    // 暴力匹配 时间复杂度O(N*M)
    public static int violenceMatch(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 s1Len = str1.length;
        int s2Len = str2.length;
        // i索引指向s1
        int i = 0;
        // j索引指向s2
        int j = 0;
        while (i < s1Len && j < s2Len) {
            if (str1[i] == str2[j]) {
                i++;
                j++;
            } else {
                i = i - (j - 1);
                j = 0;
            }
        }
        if (j == s2Len) {
            return i - j;
        } else {
            return -1;
        }
    }
    

三、KMP 算法介绍

  1. KMP是一个解决模式串在文本串是否出现过,如果出现过,最早出现的问题的经典算法
  2. Knuth-Morris-Pratt 字符串查找算法,简称为“KMP 算法”,常用于在一个文本串S内查找一个模式串P的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H.Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。
  3. KMP算法就利用之前判断信息,通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next数组找到,前面匹配过的位置,省去了大量的计算时间。

四、KMP 算法最佳应用

字符串匹配问题

有一个字符串str1 = "BBC ABCDAB ABCDABCDABDE",和一个字串str2 = "ABCDABD",现在要判断str1是否包含str2,如果包含,就返回第一次出现的位置,如果不包含,则返回-1。

要求:使用KMP算法完成判断,不能使用简单的暴力匹配算法

  1. 首先,用str1的第一个字符和str2的第一个字符去比较,不符合,关键词向后移动一位

image.png

  1. 重复第一步,还是不符合,再后移

image.png

  1. 一直重复,直到str1有一个字符与str2的第一个字符符合为止

image.png

  1. 紧接着比较字符串和搜索词的下一个字符,还是符合。

image.png

  1. 遇到str1有一个字符与str2对应的字符不符合

image.png

  1. 这时候,想到的是继续遍历str1的下一个字符,重复第1步。(其实是很不明智的,没有必要再做重复的工作,一个基本事实是,当空格与D不匹配时,其实知道前面六个字符是“ABCDAB”。KMP算法的想法是,设法利用这个已知信息,不要把搜索位置移回到已经比较过的位置,继续把它向后移,这样就提高了效率。)

  2. 怎么做到把刚刚重复的步骤省略掉?可以对str2计算出一张《部分匹配表》

  3. 介绍《部分匹配表》怎么产生的,先介绍前缀和后缀

    image.png

根据前缀与后缀的定义,可以退出匹配表数组arr[0] = -1,arr[1] = 0。

arr[i]:当前i位置往前(不包括i)即 0 ~ i-1 范围内前缀与后缀最长公共字串长度。

代码如下:

// 时间复杂度为O(N)
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 x = 0;
    int y = 0;
    int[] next = getNextArray(str2);
    while (x < str1.length && y < str2.length) {
        if (str1[x] == str2[y]) {
            x++;
            y++;
        } else if (next[y] == -1) {
            x++;
        } else {
            y = next[y];
        }
    }
    return y == str2.length ? x - y : -1;
}

// 匹配表
private 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; // 目前在哪个位置上求next数组的值
    int cn = 0; // 当前是哪个位置的值再和i-1位置的字符比较
    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是什么?KMP能解决什么问题?最重要的是要理解匹配表的代表的含义。