字符串匹配
暴力匹配算法
(黑色指针 ↓ 代表起始匹配位置,红色指针 ↓ 代表匹配失败位置,绿色指针 ↓ 代表匹配成功,适用全篇)
暴力匹配算法:就是主串从第一个字符开始,进行对位匹配,若匹配成功主串指针和子串指针一起往后再匹配,一旦匹配失败,就将主串的起始指针往后移动一位,开始第下一次匹配,直到全部匹配成功。如例1:
例1:
优点:原理简单,实现也非常容易。代码实现:
缺点:时间复杂度O(m*n)
假如运气不好,恰好遇到最后才匹配成功,就会大大增加重复的工作,效率极低。如例2,
例2:
KMP算法
KPM算法的基本思路是:当我们发现某一个字符不匹配时,由于已经知道之前遍历过的字符,那能不能用这些信息发挥一些作用优化算法?
看下面这个例子,当第一次匹配结束时知道了前四个字符ABAB可以匹配成功,第五个字符匹配失败。我们发现由于子串中前两个字符是AB,子串后两个字符也是AB。不难想到下次匹配时可以直接跳过子串中前两个字符AB,直接从第三个字符A开始匹配。
由此可知暴力匹配时,一定做了很多没必要的重复匹配,我们在某些清况主串指针是没必要回溯到上一次匹配的位置后一位开始匹配,而是可以直接跳过一些子串的字符开始匹配,那么问题来了,我们怎么知道跳过子串中的多少个呢?
这时候就要用到KMP中的next数组了,先不了解next数组是怎么来的,先看看它的功能和用法? 在KPM算法中,匹配失败时会看子串中最后一个匹配成功的字符对应的next值,这里发现最后一个匹配成功的字符B对应的next值是 2 ,那么在下一次匹配时,在不移动主指针的前提下,可以跳过子串前 2 个字符的比较。如例3:
例3:
再看两个例子:
例4:
例5:
从上面的例子知道,主串的指针永远不会回头,这就使得KMP算法的时间复杂度变成了O(n),这就是KMP算法的强大之处,下面来看KMP代码实现:
int KMPMatch(char* S, char* P, int *next)
{
int lenS = strlen(S); // 主串S长度
int lenP = strlen(P); // 子串P长度
int i = 0, j = 0; // 主串指针 i 和子串指针 j
while (i < lenS && j < lenP) // 开始遍历主串和子串
{
if (j == -1 || S[i] == P[j]) // 如果 j=-1(子串第一个字符就不匹配)或者当前字符匹配
{
i++; // 主串指针后移
j++; // 子串指针后移
}
else
{
j = next[j]; // 子串指针回溯到 next[j] 的位置
}
}
if (j >= lenP) // 如果 j 等于子串长度,说明匹配成功
{
return i - j; // 匹配成功,返回匹配的起始位置
}
else
{
return -1; // 匹配失败
}
}
我们可以发现,KPM算法的代码与暴力匹配的代码相差并不大。关键是在匹配失败时进行了不同的操作,暴力匹配是主串指针回溯到上一次匹配的位置后一位, 子串指针回溯到子串的起始位置;而KMP算法是只根据next数组提供的值回溯了子串指针。下面我们就来讲讲next数组:
Next数组的生成
在例3中我们讲到,next数值就是可以跳过的子串个数,那为什么可以跳过呢?因为之前成功匹配的子串中后两个字符是AB,开头跳过的两个也是AB,也就是说匹配成功那部分子串的前缀和后缀都是AB,这就意味着只要子串有相同前缀和后缀就可以跳过前缀或后缀个那么长的字符,如例6
例6:
所以next数组的本质就是寻找子串中“相同前后缀的长度”,而且一定要保证是最长的前后缀,目的是跳过尽可能长的距离以达到最优算法;当然最长前后缀也不会是它本身,并且最长前后缀之间也不能重合。我们来看例7感受一下寻找过程
例7
下面我们思考next数组的代码实现,可以用循环暴力匹配,寻找最长前后缀:
但这不是最优解。在计算next数组时,如果P[i] == P[j]
,那么next[i] = j
。但是,如果P[i]
与P[j]
相等,那么当P[i]
与主串不匹配时,P[j]
也一定不匹配。这时,我们不应该让j
回溯到next[j]
的位置,而应该让j
继续回溯,即让j = next[next[j]]
,这样可以避免不必要的比较。
参考例7从⑥到⑦的过程:通过⑥我们已经知道当前的最长前后缀是AB,这时候继续对比下一个字符如果下一个字符依然相同,就直接构成了一个新的更长的前后缀,也就是next值直接+1即可(参考⑦);代码可以优化到以下程度
但如果继续比较时下一个字符不同,就要判断最后一个字符能不能和它前面的字符组成短一点的前后缀。难道我们要指针回溯继续暴力匹配么?其实不然,这时候KMP算法思想依旧可以发挥作用。
参考例8:通过例8①匹配失败的信息,我们可以知道指针 ↓ 的前四个字符是相同的,根据KMP算法思想知道主串指针不回溯,而这里同样适用:只不过这里后缀就是主串,前缀就是子串,因为前后缀是一样的所以查表可以知道ABAB的next值是2,即主串(后缀)指针不动,跳过前2个字符的匹配如例8②,最后得出例8③的结果为next=2+1=3。