为什么用KMP算法
KMP是一种高效的模式匹配的一种算法
模式匹配,说直白就是找寻子字符串在主字符串的位置
(其实KMP这三个字没啥意义,这算法是3个人研究出来的,取3人名字首字母也就是KMP)
暴利匹配算法
与KMP相对立的一个算法就是暴力匹配算法,逐个字符一一对比
形象地理解就是
可以看到在这个简短的例子中,当主字符串(以下简称主串)对比到3时,与子字符串(简称子串)不同
那么就要折返到开始位置的下一位
如果主串再长点,对比的字符再离奇点(比如刚好和子串最后一位不同步)那么主串折返的次数会更多
假设主串长度为m,子串长度为n,那么主串要比对m-n+1个字符(除了最后那个长度比子串还短的都要比对)
最晦气的情况是刚好每次都是子串的最后一位不同步,那么每个主串字符都要比对n次
最坏情况的时间复杂度将是O(m-n+1)n 即 O(mn-n2+n),实际情况是m远大于n,所以可以简写为O(mn)
KMP算法思路
暴力匹配算法最大的弊端就是要折返匹配
比如在上图这个例子中,明显看到在比对第4位的时候出现的不同步
暴力匹配就会重新从主串第2位开始与子串匹配
显然当前的前3位是同步的,如果错开一位必然不同步,造成浪费时间
但是
已经匹配前3位还是有信息可以用的,当前计算机已经知道了主串前3位的序列,我们直观感受的话,下一次匹配应该是把主串的第3位和子串的第1位开始匹配
而不必再去和主串第2位比,这个直观感觉的什么原理呢?
对于已经匹配的子串序列,可以取共同元素的下一位作为下一次匹配的起点
看不懂也没关系,请看下面分析
研究子串
(以下示例图,第一个数列是主串截取)
如果是第5位不同步
我们能直观感受到,第4位和第1位是相同元素,可以作为下次匹配的起点
如果是第4位不同步
可以看到,第3位于第1位有相同元素,可以作为下次匹配的起点
如果是第3位不同步,那没办法了,前两位都不同,逐一比较了
如果是第2位不同步,第一位相同没啥用……同上图
如果是第1位不同步,那肯定是没什么好折腾了,老老实实下一个数字比较吧
回顾刚才的思路,可以发现我们是在找寻当前位置之前的字符的共同点
再精确点,当前比对位置是j,前1~j-1个字符组成字符串s
找寻s的最长相等前后缀以实现对已经匹配好的字符的复用
前缀:包含第一个字符但不包含最后一个
后缀:包含最后一个字符但不包含第一个
比如这个子串
当j=6时,1~5组成的字符串是abaab
最长相等前后缀就是ab,当第6位不同步时,直接跳转到第3位
这样,主串就不必返回到之前的状态,始终都是子串在自行调节对比位序
那么研究一下这个方案:找寻当前比对字符之前的字符,的最长相等前后缀
前方的字符串 | 最长相等前后缀 | 下一个比对位置 |
---|---|---|
abaab | ab | 3 |
abaa | a | 2 |
aba | a | 2 |
ab | 无 | 1 |
a | 无 | 1 |
可以看到下一个比对位置是最长相等前后缀的长度+1
当比对第一个字符就不同步时,那么主串要向后移一格,我们就记为0,以示意主串后移
那么就得到这么一个数组
这就是KMP算法中的next数组,用于指示如果当前匹配失败,下次子串比对位置
流程图
最后一个判断语句,如果是no,那么意味着是i超出了主串长度
也就是这种情况
主串后面无元素了,依然是匹配失败的情况
进一步优化
观察这个子串及其next数组
可以看到前4位都相同且为a
当第4位不匹配时
j应该等于next[4]即3
显然子串的4号位是a,与主串的4号位不同步,那么位于子串3号位的a也必然不同步
同理,当j=2时,也不同步,j继续指向1
那为什么不直接跳转到第一位呢?
所以,在构建好next数组后,还可以进一步优化
对于子串中的相同元素,取位序小的next值作为当前next值
以减少这种不必要的比对
那么新的next数组nextval应该变为
时间复杂度
主串长度为m,子串长度为n
则
暴利匹配算法时间复杂度是O(mn)
KMP算法时间复杂度为O(m+n),因为主串没有回退,子串也是近似没有回退
但在一般情况下暴力匹配算法执行时间也近似为O(m+n)毕竟我们寻找的子串大概率是一组无序的字符组成的,而不是有很多重复。
这也是为什么暴力匹配算法沿用至今。其实是懒
都看到这里了,留个赞再走呗。