【数据结构与算法】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
数组的建立过程:
简而言之:建立 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)\)。