【数据结构与算法】2 - 12 KMP 算法

§2-12 KMP 算法

2-12.1 字符串匹配算法

字符串匹配是一个常见的需求,用于解决这一需求的算法就是字符串匹配算法。字符串匹配算法通常有:

  • 暴力算法(Brute-Force):也称简单匹配算法,采用穷举的方法,将主串与模式串中的字符一一比较,若每个字符对应相等,则匹配成功;否则,发生失配时,模式串指针重置回初始位置,主串指针回溯到已匹配序列的首字符位置,并在此位置上向后移动一个单位,继续上述匹配过程,直至找到子串或找不到为止;
  • KMP 算法:KMP 算法是由 D. E. Knuth、J. H. Morris 和 V. R. Pratt 共同提出的一种匹配算法,由他们的名字命名。该算法较暴力算法有很大改进,避免了主串指针回溯,尽可能地利用已经掌握的信息,提高了查找效率;

实际上,还有其他的字符串匹配算法,如 BM 算法、Sunday 算法等,其效率比 KMP 算法更高。

本节内容将介绍 KMP 算法。

2-12.2 KMP 算法思想与实现

算法详细解释见

【中文字幕】Knuth–Morris–Pratt(KMP)_Pattern_Matching(Substring_search)_哔哩哔哩_bilibili

2-12.2.1 避免查找重复内容

若在一个非常长的字符串中匹配一个子串(模式串),而二者同时具有大量重复字符,使用暴力匹配算法,则会把大量时间浪费在了重复字符串的匹配,效率不高。

KMP 算法引入了针对模式串的 next 数组,用于计算每一个字符处的最大公共前后缀,获得重复字符串的信息。

换一种说法,next 数组记录的,就是既为前缀,同时也为后缀的最大字串长度。

以模式串 abcaby 为例,创建 next 数组,数组长度与模式串长度相等,然后依次遍历,计算最大公共前后缀长度。

下图演示了 next 数组的建立过程:

image

简而言之:建立 next 数组时,需要始终寻找在当前字符(含当前字符)之前,同时是前缀,又是后缀(重复出现)的最长子串。

算法实现

创建并返回next 数组:

private static int[] getNext(String pattern) {
    // 建立next数组,长度同模式串长度,用于记录每一个字符所对应出现的最大公共前后缀长度
    // 该数组的信息可用于避免再次查找、匹配已经满足匹配的重复字符
    // 应当保证记录的是最大公共前后缀长度
    // 首字符处不存在最大公共前后缀,因此为零,无需变动
    int[] next = new int[pattern.length()];

    // 使用双指针,计算每一个字符所对应的最大公共前后缀长度
    // i 从第二个字符开始,j 从首字符开始,计算过程实际上就是判断二者所对应的字符是否相同
    for (int i = 1, j = 0; i < next.length; ) {
        // 若双指针遇到相同字符,则说明遇到一个公共前后缀
        if (pattern.charAt(i) == pattern.charAt(j)) {
            // i 处字符具有公共前后缀,长度为 j + 1
            next[i] = j + 1;
            // 递增双指针,计算、匹配下一字符
            i++;
            j++;
        } else {
            // 若字符不同,则不存在包含 i 处字符的最大公共前后缀
            // 这时,是否存在一个由 i 处字符和前面部分字符所组成的另外一个最大公共前后缀?
            // 若 j 不在首字符处,则说明目前为止,i 字符处前 j 个字符是重复的,重新定位 j
            if (j != 0)
                // 利用 j 前一索引处的数据,移动 j 指针
                j = next[j - 1];
            else {
                // 若 j 位于首字符处,则在 i 字符处找不到最大公共前后缀
                next[i] = 0;
                // 移动 i 指针,计算下一字符
                i++;
            }
        }
    }

    // 返回该数组
    return next;
}

建立 next 数组的时间复杂度为 \(O(n)\),空间复杂度为 \(O(n)\)

2-12.2.2 利用 next 数组在主串中匹配

在主串中查找、匹配模式串的过程与计算生成 next 数组的方法十分类似。

安排双指针,其中一个位于主串,另一个位于模式串。为了避免重复查找,主串中的指针从不回溯,当指针指向的字符相同时,双指针同时递增;否则,则先检查模式串指针的位置。若模式串指针位于串首,则当前字符串完全不匹配,只需要递增主串中指针,继续比较即可;若模式串指针在串中,则调取其在 next 数组中对应的前一索引处最大公共前后缀长度 l,并将模式串指针移动到 l 索引处,继续比较。

重复上述过程,直到模式串指针越界(位于最大索引 + 1 处)时,表明已经找到该子串,返回该子串在主串中的起始索引即可;若主串指针越界(位于最大索引+ 1 处)时,表明不存在该子串,返回 -1 即可。

代码实现

public static int KMPSearch(String original, String pattern) {
    // 判断主串和模式串是否为空
    if (original == null || pattern == null)
        return -1;

    // 计算 next 数组,获得最大公共前后缀信息
    int[] next = getNext(pattern);

    // 在主串中线性查找(i),在匹配串中匹配(j)
    for (int i = 0, j = 0; i < original.length(); ) {
        // 若对应字符匹配
        if (original.charAt(i) == pattern.charAt(j)) {
            //递增双指针
            i++;
            j++;

            // 若匹配串指针越界,则说明已经完全匹配
            if (j == pattern.length()) {
                // 返回首次出现的起始索引即可
                return i - pattern.length();
            }
        } else {
            // 对应字符不匹配,主串指针不动(不回顾)
            // 根据 next 数组中的最大公共前后缀移动 j 指针
            if (j != 0) {
                // 若 j 不在首字符处,防止越界
                j = next[j - 1];
            } else {
                // 位于首字符处,递增 i 指针
                i++;
            }
        }
    }

    // 循环体结束,未遇到返回语句,说明珠串指针 i 已越界,找不到匹配结果
    return -1;
}

设主串长度为 \(m\),模式串长度为 \(n\),遍历主串时间复杂度 \(O(m)\),结合建立 next 数组的时间复杂度,整个算法的时间复杂度为 \(O(m + n)\),空间复杂度为 \(O(n)\)

posted @ 2024-01-14 17:07  Zebt  阅读(16)  评论(0)    收藏  举报