1. 朴素的字符串匹配方法
1.1 比较过程
以主串T为基础,根据索引i对每个字符进行遍历,依次与目标串P的每个索引j的字符进行匹配。若字符相同,则两索引均后移,继续匹配;若不相同,则j恢复为0,从头开始,i回溯到开始成功匹配时索引的下一位。
1.2 比较结果
遍历索引i结束之后,若此时j已经达到目标串P的最后,则证明目标串的所有字符均匹配成功,目标串在主串中的位置即length(T) - j;否则匹配失败,P不是T的子串。
1.3 主要实现
|
|
2. KMP的字符串匹配方法
KMP方法的作用主要是去掉了一些可避免的比较,提高了匹配效率。
2.1 匹配过程
KMP的主要匹配过程与朴素一致,都是字符依次比较。优化点是在字符匹配失败时,尽量让主串T的索引i不回溯,目标串的索引j回溯到需要的位置(而不是直接到0)。从而达到减少比较次数的目的。
如上所述,KMP的核心就是在任意位置匹配失败时,目标串的索引j回溯到的位置。
我们将所有回溯位置的信息保存到数组next中,故最终目的就是计算出next数组的完整信息。
2.1.1 匹配表(Partial Match Table)
要说next数组,就先要了解匹配表PMT的含义。以字符串“abababca”为例,其匹配表如下:
其中,value行即为PMT的值。其意义是什么呢?
value的值是子串中前缀与后缀集合中,最长的公共子串的长度。例如,下表分别为当index为2和5时,value的得出过程:
索引 | 子串 | 前缀集合 | 后缀集合 | 交集 | 最长子串长度 |
---|---|---|---|---|---|
2 | “aba” | {“a”,“ab”} | {“ba”,“a”} | {“a”} | 1 |
5 | “ababab” | {“a”,“ab”,“aba”,“abab”,“ababa”} | {“b”,“ab”,“bab”,“abab”,“babab”} | {“ab”,“abab”} | 4 |
故目标字符串有几位,就会有几个PMT中对应的值。
2.1.2 next表(PMT表的修改形式)
先抛图(主串“ababababca”和子串“abababca”进行匹配的过程之一)。
如图所示,当在主串的i处匹配失败,其之前长度为6的字符串“ababab”均已匹配成功。此时,由PMT表我们可知,子串“ababab”的value为4,即其四位前缀与四位后缀是一致的。又因为在主串中,i之前的四位与j之前的四位字符均一致。故,目标串的四位前缀与主串中i之前的四位字符一致。我们可以省略对该子串的比较,直接在目标串的j=4的位置与主串的当前位置i继续比较。
在上面是叙述中,我们发现,匹配失败,并不会导致主串索引i改变,只会影响目标串索引j出现变化。并且,有趣的是,以图中的过程为例,当j=6时,匹配失败,即最后一个成功匹配的j是5。之后j回溯的位置4正好是PMT表中index为5时的value值。根据这个原理,我们就可以跳过不需要的比较,只通过修改目标串索引的位置即可进行快速匹配。
那么,我们引入next表的目的是什么呢?由于在某一位置匹配失败时,都要查看其前一个位置对应的PMT值,故我们将PMT数据均偏移一位,直接对应于当前位置,以便在快速查找下一步时,可以直接得到目标串索引j的位置。因此,我们把PMT值偏移后的数据叫做next数据,为了补位,next的首位赋值为-1。
因此,有了next的方式,我们就可以对朴素匹配法进行优化了。
2.2 匹配过程
|
|
2.3 生成Next数组的过程
我们知道,生成Next数据的过程,即在每一个位置上,获取前面子串里,前后缀集合中最长的公共子串的长度。
如目标字符串“ABCDABDE”,我们以k作为前缀索引,j为后缀索引。如下图,当在字符索引在“D”时(即j为6时),其子串“ABCDAB”的最长前后缀公共子串长度为2(子串“AB”的长度),故next[6] = 2。我们还可知,在j为5时,next[5] = 1,即在j = 5时,公共子串最大长度为1(子串“A”的长度)。故我们可以得到:在当前字符存在有效next值时(Pk == Pj时),下一个字符的next即为之前next位置的下一个,即next[j + 1] = next[j] + 1 = k + 1。
若Pk与Pj不相等,如图中k为2,j为6时。按照KMP的思想,后缀j不变,需要更新前缀k,以便找到另一个字符与Pj相同。故将前缀索引k修改为next[k]以继续进行比较(缩小前缀查找范围),直到找到相同的字符“D”或没有相同字符为止。
按照这个思想,生成next数据的实现为:
|
|
Objective-C实现的代码地址:点击查看
2.4 Next数组的优化
我们设想一下这种情况:目标串为“abcabc”,主串为“abacababc”,其中一种比较情况如下图:
即此时,第四位比较失败(“c”与“b”不同)。我们知道,此时目标串下一次比较的索引j为next[j],即第二位。也就是目标串右移j-next[j]位(2位)。
如图所示,由于移动后,第二位的字符仍然为“b”,显然比较失败。在这种情况下,本次比较实际上是没有意义的。那么应该如何避免这种比较呢?
其实,在正常情况下,当匹配失败时,我们还会进一步缩小匹配范围(j = next[j]),故在这种情况下(next[j]指向的字符与当前字符相同时),我们只需再次递归next[j],跳过相同字符即可。
因此,解决办法就是,在生成next数组时,在当前Pk和Pj相同的情况下,若下一位的j与k指向的字符相同,直接设置该位置的next为next[k]即可:
|
|
最终,优化后的next数组信息如下表所示(以目标串“abab”为例):
使用优化后的next数组,可以再次减少一些不必要的比较。
2.5 时间复杂度
KMP方式的运算次数为计算next数组的次数与匹配主串的次数之和,即O(m + n)。
参考引用: