KMP算法
KMP算法是用于解决字符串匹配的经典算法,就是判断一个字符串是否是另一个字符串的子串。
例如现在我们有两个字符串,字符串s: "aabaabaaf"和字符串t: “aabaaf”,我们该如何判断字符串t是否是字符串s的一个子串呢?如果使用暴力匹配,即两个for循环,暴力的时间复杂度是两个字符串长度的乘积。
而KMP算法有什么不同呢:
就比如字符串s和t的匹配中,当我们匹配到第六个字符时,发现b和f并不匹配,此时按照暴力法我们应该将s的下标+1,继续从t的开头开始匹配;但是如果是KMP算法,就会保持s的下标不变,将t的下标回退到2,即第三个字符’b’,这样是不是效率就提高了。
明白了KMP算法的优越性,我们想知道为什么它可以这样回退,回退的规则又是什么。其实,我们看字符串t,这里存在着相同的前后缀"aa",正是因为有了相同的前后缀,我们就可以省去再次匹配相同字符的过程。那为什么t的下标回退到的位置2是怎么得来的呢?这就要用到字符串t的前缀表了。
前缀表
所谓前缀表,就是一个字符串中相同前后缀的长度所组成的表。
对于字符串t:
前缀包括:a, aa, aab, aaba, aabaa (从前往后不包含尾字符)
后缀包括:f, af, aaf, baaf, abaaf (从后向前不包含首字符)
a ----------不存在相同前后缀,0
aa --------“a”, 1
aab -------不存在,0
aaba ------“a”, 1
aabaa ---- “aa”, 2
aabaaf —不存在,0
所以字符串t的前缀表为{0,1,0,1,2,0}
对于这个前缀表,当匹配到字符’f’时,退回的位置要看前缀表中它的前一位。
也可以将前缀表整体右移,变成{-1,0,1,0,1,2},这样退回的位置就取决于字符本身对应的那一位了。
也可以将前缀表整体-1,这样退回的位置也是字符本身对应的那一位了。它们的本质都是利用字符串的前缀表找到回退的位置,只是实现起来有略微的不同,我们就以原本的前缀表为例进行演示。
next数组
next数组其实就是前缀表在我们实际代码中的存在形式,这里我们要讲的是如何通过一个字符串找到它的前缀表,即next数组。找next数组并不难,只要记住四步操作:初始化,前后缀不相等,前后缀相等,赋值。
-
初始化
首先要定义两个下标i , j,用来标识前后缀对应的字符;还要赋值next[0]=0;因为下标为0时无法再回退了。
-
前后缀不相等
前后缀不相等时,我们就需要回退,即j = next[j - 1]。同时还要保证j > 0,同样因为j = 0时无法再回退。
-
前后缀相等
前后缀相等时,我们就继续匹配,j++。
-
赋值
给next数组赋值,此时相同前后缀的长度对应j的大小,即next[i] = j。
代码实现:
void getNext(int *next, string t){
//初始化
int i = 1, j = 0;
next[0] = 0;
for (; i < t.size(); i++) {
//前后缀不相等,注意回退是一直回退到可以匹配的位置,所以是while
while (j > 0 && s[i] != s[j]) {
j = next[j - 1];
}
if (s[i] == s[j]) { //前后缀相等
j++;
}
next[i] = j; //赋值
}
}
例题实现:
函数实现的过程与寻找next数组大体一致:
bool isSonString(string s, string t) {
int next[s.size()];
getNext(next, t);
int i = 0, j = 0; //i = 0,从头开始匹配
for (; i < s.size(); i++) {
while (j > 0 && s[i] != t[j]) {
j = next[j - 1];
}
if (s[i] == t[j]) {
j++;
}
if (j >= t.size()) { //字符串t匹配完
return true;
}
}
return false;
}