串
串是由零个或多个字符组成的有限序列,又名叫字符串
朴素的模式匹配算法
串经常用来匹配子串再主串中是否存在,假设两个串存在数组中,数组下标0的位置存了串的长度。
/**
* 串的朴素模式匹配算法
* 返回子串T在子串S中第pos个字符之后的位置。若不存在,则返回0
* T非空, 1 <= pos <= strlength(S)
*/
function Index(string $S, string $T, int $pos) {
$i = $pos; //i为主串中当前位置下标 若pos不为1,则从pos位置开始匹配
$j = 1; //j用于子串中当前位置下标
//判断长度
while ($i <= $S[0] && $j <= $T[0]) {
if ($S[$i] == $T[$j]) {
//如果第一个字母相等则对比下一个字母
$i++;
$j++;
}else {
//把i回到上一个匹配的字符的下一个
$i = $i - $j + 2;
//j退回首位
$j = 1;
}
}
if ($j > $T[0]) {
//匹配上了,返回下标
return $i - $T[0];
} else {
//没匹配上, 返回0
return 0;
}
}
想一下这个算法的时间复杂度,最好的情况肯定是直接匹配到也就是O(1)
,那么最坏的情况呢,如果主串为“0000000000000000000000000001”,而子串为“000000000001”,设n是主串的长度,m是子串的长度,那么当i = 1的时候,要匹配m次发现匹配失败,当i=2时候还是m次,一直匹配到n - m + 1的位置,那么总共匹配了(n - m + 1) * m 次,它最坏的时间复杂度也就是O((n - m + 1) * m)。
KMP模式匹配算法
既然上面那个算法时间复杂度太差,那么有没有更好的呢,肯定是有的,也就是KMP算法,KMP算法由D.E.Knuth、J.H.Morris 和 V.R.Pratt(其中Knuth和Pratt共同研究,Morris独立研究)他们三个研究发表,可以大大避免重复遍历的情况,我们把它称为克努特-莫里斯-普拉特算法,简称KMP算法。
KMP算法匹配原理
主串S=“abcdefgab”,子串T=“abcdex”,那么如果用前面的朴素算法的话,前5个字母,两个串完全相等,直到第6个字母,'f’与’x’不等。
如果我们知道T串中首字符’a’与T中后面的字符均不相等。而T串中的第二位的’b’与S串中第二位的’b’相等,那么也就意味着,T串中首字符’a’与S串中的第二位’b’是不需要判断也知道它们是不可能相等了。
同样道理,在我们知道T串中首字符’a’与T中后面的字符均不相等的前提下,T串的’a’与S串后面的’c’,‘d’,'e’也都可以确定是不相等的。
再看下一个例子
假设S=‘abcabcabc’,T=‘abcabx’。对于开始的判断,前5个字符完全相等,第6个不等,此时,根据上一个列子的经验,T的首字符’a’与T的第二位’b’,第三位’c’均不等,所以不需要做判断
因为T的首位’a’与第四位’a’相等,第二位的’b’与第五位的’b’相等。而在第一步时,第四位的’a’与第五位的’b’已经与主串中的相应位置比较过了,是相等的,因此,T的首字符’a’第二位的’b’与主串中的第四位和第五位也不需要比较了,肯定是相等的–之前比较过了,还判断什么。
对比这两个例子,可以发现,在第一步时候,都是当i=6时发现两个字符不相等,而剩下的2,3,4,5步时,i=2,3,4,5。其实都可以省略
即我们在朴素模式匹配中,主串的i值是不断的回溯来完成的。而我们的分析发现,这种回溯其实可以不需要,KMP模式匹配算法就是为了让这没必要的回溯不发生。
既然i值不回溯,也就是不可以变小,那么要考虑的变化就是j值了。根据我们前面说的,很显然j值得变化和自身有关,j值得多少取决于当前字符之前的串的前后缀的相似度。比如第一个例子,'abcdex'
j值就是1,那第二个例子'abcabx'
j值是3,因为ab = ab.
我们把T串各个位置的j值得变化定义为一个数组next,我们现在要求出这个next数组
。
next数组值推导
在子串中查找有没有和首位字符相等的字符,如果没有,next[j] = 1,如果有,那么n个字符相等,next[j] = n + 1,比如abcabx,next[1] = 0, 2 = 1, 3 = 1, 4 = 1, 5 = 2,因为next[5]的时候,选取的字符是 abca 从j = 1到 j = 4的字符,而a
bca
中前后的a相等,那么一个字符相等,next[5] = 1 + 1= 2,我们选取字符时候是选择从首字符到j - 1个字符的比较。那么next[6] = 3,因为j = 6的时候比较字符ab
cab
,前后的ab两个字符相等,那么next[6] = n +1 = 2+1=3。这个例子的next数组就是[0,1,1,1,2,3]。
看下一个例子,找出 T = 'ababaaaba’的next数组
- j = 1, next[1] = 0
- j = 2, 取字符a, next[2] = 1
- j = 3, 取字符ab, next[3] = 1
- j = 4, 取字符aba, 因为 a = a, next[4] = n + 1 = 1+1= 2
- j = 5, 取字符abab, 因为ab = ab, next[5] = n + 1 = 2 + 1 =3
- j = 6, 取字符ababa, 因为aba = aba, next[6] = n + 1 = 3+1= 4
- j = 7,取字符ababaa,因为a = a, next[7] = 2
- j = 8,取字符ababaaa, 同上,next[8] = 2
- j = 9,取字符ababaaab,因为ab = ab, next[9] = 2 + 1=3
- 最终next = [0,1,1,2,3,4,2,2,3]
再来一个例子, 找出T='aaaaaaaab’的next数组
- j = 1,next[1] = 0
- j = 2,next[2] = 1
- j = 3,取aa, a = a, next[3] = 1+1=2
- j = 4,aaa, aa = aa, next[4] = 2 + 1=3
- j=5,aaaa, aaa = aaa, next[5] = 3+1=4
- j=6,aaaaa, aaaa = aaaa,next[6] = 4+1=5
- j=7,aaaaaa, aaaaa = aaaaa,next[7] = 5+1=6
- j=8,aaaaaaa, aaaaaa = aaaaaa,next[8] = 6+1=7
- j=9,aaaaaaaa, aaaaaaa = aaaaaaa,next[9] = 7+1=8
- next数组 = [0,1,2,3,4,5,6,7,8]
可以看到,其实,next[1] 都是0,next[2]都是1,从next[3]开始才有变化,有人问了,aaa里面为啥是aa = aa,而不是 aaa = aaa,是因为比较的时候取前缀字符时不能包含最后的字符,取后缀字符时候同理。