关于两个字符串的kmp比对算法
假设有字符串X和Y,满足len(X)>len(Y),要比对这两个字符串。
我们知道,最朴实的方法,就是现将二者对齐,然后依次比对对应位置的字符。如果能匹配到Y最后位置,则匹配成功;如果匹配失败,则将Y右移一位,再从头进行匹配。
设字符串X为dababeabafdababcg;字符串Y为ababc。
这种比对方法如下所示:
起始时,二者对其,第一个字符不匹配 :| :dababeabafdababcg :ababc
右移一位,比对位置移动到Y起始位置
: |:dababeabafdababcg: ababc
连续成功4次,再次遇到不匹配
: |:dababeabafdababcg: ababc
右移一位,比对位置移动到Y起始位置
: |:dababeabafdababcg: ababc:
不断重复该过程,直到…………
: |:dababeabafdababcg: ababc
Y完整匹配到X,结束。
毫无疑问,这种方法太笨了。时间复杂度高达O(mn)。最大的问题是,进行了大量的重复比对工作。
kmp算法
正是为了解决这一点而提出的。kmp算法
的中心思想是,已经匹配的部分不需要再次去匹配,相反,应根据已匹配的部分进行多字节的挪动,加快匹配速度。
怎么做呢?kmp给出的答案是:
已经匹配的部分,可以说是已知的,而且是只取决于字符串Y的,对字符串Y的挪动可以直接挪动到下一个满足匹配的位置。
什么叫下一个满足匹配的位置?
假设字符串X和字符串Y已经匹配的部分为abcab
,显然:
: abcab... : abcab... :: abcab... : abcab... :
都是不匹配的,只有如下的
:abcab...: abcab...
才匹配。显然,新的匹配序列,是旧的匹配序列的一个真后缀,而且还是字符串Y的一个前缀;而旧的匹配序列也是字符串Y的前缀。
也就是说,根据已经匹配的部分,我们已经可以排除大量的位置了。kmp算法
正是基于这一原理实现的。
规则1
如果当前比对位置两个字符相同,则比对位置右移1位 规则2 如果当前比对位置两个字符不同,则:如果没有匹配到序列,则比对位置右移1位,字符串Y右移1位
如果已有匹配到序列,则移动字符串Y到下一个匹配位置
如下例,起始
:|:dababeabafdababcg:ababc
字符不匹配,比对位置和字符串Y都右移一位
: |:dababeabafdababcg: ababc
字符匹配,比对位置右移1位
: |:dababeabafdababcg: ababc
连续成功匹配4次,再次遇到不匹配
: |:dababeabafdababcg: ababc
字符不匹配,挪动字符串Y到下一个匹配位置
: |:dababeabafdababcg: ababc
仍旧不匹配,继续挪动字符串Y
: |:dababeabafdababcg: ababc
仍旧不匹配,已经没有匹配序列了,比对位置和字符串Y都右移一位
: |:dababeabafdababcg: ababc
字符匹配,比对位置右移1位
: |:dababeabafdababcg: ababc
连续成功匹配3次,再次遇到不匹配
: |:dababeabafdababcg: ababc
字符不匹配,挪动字符串Y到下一个匹配位置
: |:dababeabafdababcg: ababc
仍旧不匹配,继续挪动字符串Y
: |:dababeabafdababcg: ababc
字符不匹配,无匹配序列,比对位置和字符串Y都右移一位
: |:dababeabafdababcg: ababc
字符不匹配,无匹配序列,比对位置和字符串Y都右移一位
: |:dababeabafdababcg: ababc
字符匹配,比对位置右移1位
: |:dababeabafdababcg: ababc
连续成功匹配5次,到达字符串Y终点,匹配成功
: |:dababeabafdababcg: ababc
由此可见,kmp算法
的比对位置没有倒车的情况,而且同一位置比对的次数也明显少于朴实算法
,比对速率是相当快的。
kmp算法的关键,是根据已有匹配序列计算下一个匹配序列
,而这一部分,是只需要字符串Y就可以实现的,因为下一匹配序列的计算和已有匹配序列之外的部分,无关。
事实上,根据已有匹配序列,完全可以得到多个满足要求的下一级匹配序列(即既是已有匹配序列的真后缀,又是字符串Y的前缀),但是毫无疑问,我们应该选择最长的那个。
kmp算法
的关键,也就是对根据已有匹配序列计算下一个匹配序列
这一个步骤的计算了,因为我们知道下一匹配序列必然是字符串Y的前缀,实际上只需要记录下一匹配序列的长度即可。这也就是所谓的next数组的真面目。(其他人介绍的next数组的值可能和我说的不同,但实际上都是对下一匹配长度进行数学变换的结果)
那么关于next数组的计算,自然也有多种方法,比如很朴实的方法,我就不多说了。事实上,有个很好的方法可以轻松的计算出next数组。这个方法和kmp本身比对的过程有些类似,同时也和后缀树构树的ukkonen算法
有异曲同工之妙。
go代码如下:
// 计算既是s[:i]真后缀又是其真前缀的最长(连续)序列的长度。// 既是真后缀,也是真前缀,称之为(同向)回文序列。// 长度为零的序列是任意序列的(同向)回文序列。// n[i]用来表示s[:i]最长(同向)回文序列长度。func next(s string) []int { // 已知当前比较位置前的字符都匹配 n := make([]int, l) // n[0]、n[1]都是0,从n[2]开始算起 for i, j := 2, 0; i < l; { // i-1表示s[:i]的后缀的当前比较位置 if s[i-1] == s[j] { // j 表示s[:i]的前缀的当前比较位置 n[i] = j+1 // j+1已经是最大的序列长度了 i, j = i+1, j+1 // 同时移动两个字符串的比较位置 continue // 进入下一比较位置的循环 } // 字符不匹配,最长回文不能延长 if j == 0 { // 没有非空的回文序列了 i++ // n[i]的默认值即为0 continue // 从头开始匹配循环 } // 存在非空的回文序列 j = n[j] // 找到下一个匹配位置 } return n}
我们每一次循环的时候,只进行了一个字符的比对!这是因为之前的比对已经保证前面的部分是匹配的。那么如果这一次比对成功,只需要延长下一次匹配的长度就行了;如果比对失败,我们也不需要从头开始去查找下一个匹配的起始位置,因为之前的匹配结果已经告诉了我们下一个匹配的位置。
结果是,kmp算法
构造next数组和比对的过程,都很迅速!
kmp算法
完整代码如下:
// kmp字符串搜索算法func KMP(s, r string) int { l := len(r) // 成员n[i]表示既是s[:i]真后缀又是s前缀的最长序列的长度。 // 真后缀指非自身的非空后缀。如不存在,则置该成员值为0。 n := func(s string) []int { n := make([]int, l) // 从n[2]开始算起 for i, j := 2, 0; i < l; { // 已知前面的字符全部匹配 if s[i-1] == s[j] { j++ n[i] = j i++ continue } if j == 0 { // 申请的n会全部初始化为零 i++ continue } // 类似后缀指针的功用 j = n[j] } return n }(r) // 进行搜索 i, j := 0, 0 for i+l < j+len(s) && j < l { if s[i] == r[j] { i, j = i+1, j+1 continue } if j == 0 { i++ } else { j = n[j] } } if j == l { return i - l } return -1}
kmp算法是一个相当精巧,但是代码却非常简单的算法。相比之下,虽然速度可能逊色于BM算法,但是确实优美得多。