【数据结构与算法】KMP 算法

KMP 算法

应用场景 - 字符串匹配问题

字符串匹配问题:

  1. 有一个字符串 str1 = "哈喽 哈喽啊哈喽 哈喽啊哈喽哈喽啊",和一个子串 str2 = "哈喽啊哈喽哈"
  2. 现在判断 str1 是否含有 str2,如果存在,就返回第一次出现的位置,如果没有,就返回 -1。

暴力匹配算法

如果使用暴力匹配的思路,并假设现在 str1 匹配到 i 位置,子串 str2 匹配到 j 位置,则有:

  1. 如果当前字符匹配成功(即 str1[i] == str2[j]),则 i++,j++,继续匹配下一个字符;
  2. 如果失配(即 str1[i] != str2[j]),令 i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为 0;
  3. 用暴力方法解决的话就会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间。(不可取)

算法实现

// 暴力匹配
public static int violenceMatch(String str1, String str2) {
    char[] s1 = str1.toCharArray();
    char[] s2 = str2.toCharArray();

    int s1Len = s1.length;
    int s2Len = s2.length;

    int i = 0; // i 索引指向 s1
    int j = 0; // j 索引指向 s2
    while (i < s1Len && j < s2Len) { // 保证匹配时不越界
        if (s1[i] == s2[j]) { // 匹配成功
            i++;
            j++;
        } else { // 没有匹配成功
            i = i - (j - 1);
            j = 0;
        }
    }
    // 判断是否匹配成功
    if (j == s2Len) {
        return i - j;
    } else {
        return -1;
    }
}

KMP 算法的最佳实践 - 字符串匹配问题

  1. KMP是一个解决模式串在文本串是否出现过,如果出现过,得到最早出现的位置的经典算法。
  2. KMP方法算法就利用之前判断过的信息,通过一个 next 数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过 next 数组找到,前面匹配过的位置,省去了大量的计算时间。

举例

有一个字符串 str1 = "BBC ABCDAB ABCDABCDABDE",判断,里面是否包含另一个字符串str2 = "ABCDABD"

思路分析

  1. 首先,使用 str1 的第一个字符和 str2 的第一个字符去比较,不符合,关键词向后移动一位,直到找到 str1 有一个字符与 str2 的第一个字符符合为止;
  2. 然后接着比较字符串和搜索词的下一个字符;
  3. 如果过程中有一个字符没有匹配上,这时候,想到的是继续遍历str1的下一个字符,重复第1步。(其实是很不明智的,因为此时 BCD 已经比较过了,没有必要再做重复的工作,一个基本事实是,当空格与 D 不匹配时,你其实知道前面六个字符是 ABCDAB。KMP算法的想法是,设法利用这个已知信息,不要把”搜索位置”移回已经比较过的位置,继续把它向后移,这样就提高了效率。)
  4. 这时候可以对 str 计算出一张《部分匹配表》:
搜索词 A B C D A B D
部分匹配值 0 0 0 0 1 2 0
  1. 已知空格与D不匹配时,前面 6 个字符”ABCDAB”是匹配的。查表可知,最后一个匹配字符B对应的”部分匹配值”为 2,因此按照下面的公式算出向后移动的位数:
    移动位数 = 己匹配的字符数 - 对应的部分匹配值
    因为 6 - 2 等于 4,所以将搜索词向后移动 4 位。
  2. 逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。

部分匹配值

“部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。以”ABCDABD”为例,一”A”的前缀和后缀都为空集,共有元素的长度为0;

  • ”AB”的前缀为[A],后缀为[B],共有元素的长度为0;
  • ”ABC”的前缀为[A,AB],后缀为[BC, C],共有元素的长度0;
  • ”ABCD”的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
  • ”ABCDA”的前缀为[A AB, ABC, ABCD],后缀为[BCDA, CDA, DA,A],共有元素为”A”,长度为1;
  • ”ABCDAB”的前缀为[A,AB,ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB,AB,B],共有元素为”AB",长度为2;
  • ”ABCDABD”的前缀为[A, AB,ABC,ABCD, ABCDA,ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD,D],共有元素的长度为0。

代码演示

/**
 * kmp搜索算法
 *
 * @param str1 源字符串
 * @param str2 子串
 * @param next 部分匹配表
 * @return
 */
public static int kmpSearch(String str1, String str2, int[] next) {
    // 遍历 str1
    for (int i = 0, j = 0; i < str1.length(); i++) {
        // 需要处理 str1.charAt(i) != str2.charAt(j)
        while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
            j = next[j - 1];
        }

        if (str1.charAt(i) == str2.charAt(j)) {
            j++;
        }
        if (j == str2.length()) {
            return i - j + 1;
        }
    }
    return -1;
}

// 获取字符串部分匹配值
public static int[] kmpNext(String dest) {
    // 创建数组
    int[] next = new int[dest.length()];
    next[0] = 0; // 如果字符串长度为 1,部分匹配值就为 0
    for (int i = 1, j = 0; i < dest.length(); i++) {
        // 当 dest.charAt(i) != dest.charAt(j) 我们需要从 next[j - 1]获取新的 j
        // 直到 dest.charAt(i) == dest.charAt(j) 满足才退出
        // 核心点
        while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
            j = next[j - 1];
        }

        // 当 dest.charAt(i) == dest.charAt(j) 满足时,部分匹配值为 + 1
        if (dest.charAt(i) == dest.charAt(j)) {
            j++;
        }
        next[i] = j;
    }
    return next;
}

你可能感兴趣的:(数据结构和算法,算法)