1. 字符串匹配的暴力算法
传统的字符串匹配是从父串的第一个元素开始,与子串进行匹配
如果匹配到某一个元素父串与子串不匹配了,则此时父串指针滑动到第二个元素重新开始匹配,直到顺利匹配或者继续顺次滑动
2. 从字符串匹配暴力算法得到的启示
字符串匹配的暴力算法的时间复杂度是O(MN),而让时间复杂度如此高的重要原因在于当在某个位置父串与子串不匹配的时候,父串会直接回到最初元素的下一个元素重新进行匹配,这耗费了大量时间复杂度
那么有没有一种算法,当某个位置不匹配时,父串指针不用回调,而可以直接保持在当前位置呢?
KMP算法能够做到这点
3. KMP算法的核心逻辑
KMP算法的核心逻辑在于充分利用已匹配的数组段的字符串信息从而达到在不匹配时快速进行重新匹配的目标。
如上图所示,在已经匹配的字符串中,是存在这样的一种情况,即字符串的左右两端的一段子字符串相同,这既表示父串的这两段相同,也表示子串的这两段相同
那么,当在下标j-1处不匹配时,子串的a部分可以快速滑动到父串的b部分进行匹配,从而达到节省时间的目的
4. 实现KMP算法核心逻辑的数组
KMP算法通过维护一个数组来实现该核心逻辑
数组的每一个下标的元素存储着以当前下标为终点,以子串起点为起点的字符串中,左右两端相等字符串的左字符串的终点下标,也即指示着当子串的下一个下标元素不匹配时,子串要滑动到哪个位置去匹配父串
维护该数组分为两种情况
当当前元素匹配时,我们根据当前下标的前一个下标去找与其匹配的左端字符串,然后判断该左端字符串的下一个元素是否与当前元素匹配,迭代求解直到获得匹配下标,子串指针不变
当当前元素不匹配时,我们根据当前下标的前一个下标去找与其匹配的左端字符串,然后判断该左端字符串的下一个元素是否与父串元素匹配,迭代求解直到获得匹配下标,然后子串指针滑动到该下标
5. 代码
private static int kmp(String s1, String s2) {
// 传统的匹配如果s2在某个位置匹配不上s1,那么就顺次后移
// kmp算法能够实现较快的移动
int i1 = 0;
int i2 = 0;
// 一边遍历一边维护kmp的数组
int[] kmp = new int[s2.length()];
// 如果在当前i1匹配,那么要根据已有的kmp数组维护i1
// 用一个while循环去寻找能和当前下标的元素符合kmp的最长数组
// 如果找不到,那么当前下标的kmp设置为-1
// 如果不匹配,当前i1就要回退,根据已有的kmp数组来判断
// 不匹配同样可以用while循环迅速找到能和当前i2匹配的元素,然后直接可以i1++i2++
Arrays.fill(kmp, -1);
while (i1 <= s1.length() - s2.length() + i2 && i2 < s2.length()) {
if (s1.charAt(i1) == s2.charAt(i2)) {
int temp = i2 - 1;
loop:
while (temp != -1) {
// 不断判断当前位置的下一个元素能否凑对
if (s2.charAt(kmp[temp] + 1) == s2.charAt(temp + 1)) {
kmp[i2] = kmp[temp] + 1;
break loop;
}
temp = kmp[temp];
}
} else {
i2--;
while (i2 != -1 && s2.charAt(i2 + 1) != s1.charAt(i1)) {
i2 = kmp[i2];
}
}
i1++;
i2++;
}
if (i2 < s2.length()) {
return -1;
}
return i1 - s2.length();
}