引言
LeetCode第28题要求实现一个函数,找出在一个字符串(主串)中第一次出现另一个字符串(模式串)的下标。KMP算法是解决这个问题的理想选择,因为它在最坏情况下也能提供线性时间复杂度的性能。本文将详细介绍KMP算法的原理,并展示如何用Java实现它。
KMP(Knuth-Morris-Pratt)算法是一种高效的字符串搜索(或模式匹配)算法,由Donald Knuth、Vaughan Pratt和James H. Morris于1977年共同发明。它的核心思想是当模式串(pattern)与主串(text)不匹配时,利用已经部分匹配的信息,避免从头开始匹配,从而提高匹配效率。
KMP算法原理详解
1. 部分匹配表(也称为前缀函数或失败函数)
KMP算法首先对模式串进行预处理,生成一个部分匹配表(π数组),该表记录了模式串中每个位置之前的子串中,有多少个字符是既匹配前缀也匹配后缀的。
例如,对于模式串"ABCDAB",在位置5(字符’B’)的π值是3,因为"CDAB"的前缀"CDA"和后缀"DA"有3个字符是相同的。
2. 搜索过程
在搜索过程中,KMP算法使用两个指针,分别指向主串和模式串的当前位置。当遇到匹配的字符时,两个指针同时向后移动;当遇到不匹配的字符时,根据π数组的值调整模式串的指针位置,而不是主串的指针。
3. π数组的计算
π数组的计算是KMP算法的第一步,以下是计算π数组的步骤:
- 初始化π[0]为0,因为没有任何字符之前,没有相同的前缀和后缀。
- 初始化两个指针i和j。i用于遍历模式串,j用于跟踪相同前缀和后缀的最长长度。
- 当模式串的第i个字符与第j个字符相同时,增加j,同时π[i] = j。
- 如果第i个字符与第j个字符不匹配,且j不为0,将j更新为π[j-1],因为可以假设模式串的前j-1个字符是匹配的。
- 如果j为0,且第i个字符与之前的任何字符都不匹配,那么π[i] = 0,并将i加1。
4. 匹配过程
- 初始化两个指针,分别指向主串和模式串的起始位置。
- 比较两个指针所指向的字符,如果相等,移动到下一个字符。
- 如果字符不匹配:
- 使用π数组找到模式串中可以跳过的最远位置。这通常是π[j-1],除非j为0,在这种情况下,指针i加1,继续匹配。
- 移动模式串的指针到这个新位置,然后继续比较。
- 如果模式串的指针到达模式串的末尾,记录当前主串指针的位置,这表示找到了一个匹配。
5. 优点
KMP算法的主要优点是它消除了在主串中的无效搜索,因为它利用了模式串的内部信息来决定下一步的搜索位置。这使得KMP算法在最坏情况下也能保持线性时间复杂度O(n+m),其中n是主串的长度,m是模式串的长度。
KMP算法的Java实现
1.KMP搜索算法
private void getNext(int[] next, String s) {
int j = 0;
next[0] = 0;
for (int i = 1; i < s.length(); i++) {
while (j > 0 && s.charAt(j) != s.charAt(i))
j = next[j - 1];
if (s.charAt(j) == s.charAt(i))
j++;
next[i] = j;
}
}
2. 测试代码
class Solution {
//前缀表(不减一)Java实现
public int strStr(String haystack, String needle) {
if (needle.length() == 0) return 0;
int[] next = new int[needle.length()];
getNext(next, needle);
int j = 0;
for (int i = 0; i < haystack.length(); i++) {
while (j > 0 && needle.charAt(j) != haystack.charAt(i))
j = next[j - 1];
if (needle.charAt(j) == haystack.charAt(i))
j++;
if (j == needle.length())
return i - needle.length() + 1;
}
return -1;
}
结论
KMP算法是一种强大的字符串搜索工具,它通过预处理模式串来避免在主串中的无效搜索。通过本文的介绍,我们学习了KMP算法的原理和Java实现,以及如何在LeetCode第28题中应用它来找出字符串中第一个匹配项的下标。
扩展阅读
希望本文能够帮助读者深入理解KMP算法,并在实际编程中熟练应用。