(转载自朋友的博客http://blog.csdn.net/msdnwolaile/article/details/51287911#comments)
看了好多关于KMP算法的书籍和资料,总感觉没有说的很清楚,为什么会产生next数组,为什么给出了那么简短的程序,没有一个过程,而有的帖子虽然next及其字符串匹配说的很清楚,但是推理的一些过程相当复杂,比较抽象,今天在这里简单的提一下我的理解,尽可能的把这个过程讲的简单,容易理解
从模式匹配之初:我们先是研究的是BF算法,鉴于我们经常行的需要回溯,总是做一些无用功,为了提高算法的时间度和空间度,引入了next数组(至于为什么提高时间,下面会提到),也就是采用next数组,我们不需要再去回溯了,可以直接比较,这也就是KMP算法了
所以说:从BF到KMP,只是简单的嫌弃BF算法,花费的时间空间太长太大了而已,一切都在进步!!!
串的模式匹配或者串匹配:
子串的定位操作是找子串在主串中从POS个字符后首次出现的位置
一般将主串S称为目标串,子串T称为模式串
BF算法:
Brute-Force,也称为蛮力(暴力)匹配算法
1,从主串S的第pos个字符开始,和模式串T的第一个字符进行比较,
2,若相等,则逐个比较后续的字符;不然,就回溯到主串的第POS+1个字符开始,继续和模式串T的第一个字符进行比较,反复执行步骤2,知道模式串T中的每一个字符都和主串相等(返回当前主串S匹配上的第一个字符)或者找完主串S(POS位置后的所有字符(返回0)),
程序1:
程序2:
算法比较简单,但是在最坏的情况下,算法的时间复杂度为O(n×m),n,m分别为主串和模式串的长度。从第一个程序就可以很容易的看出来了,主要时间都耗费在失配后的比较位置有回溯,主要都给拉回来,因而比较次数过多
为了时间都不浪费在主串的回溯上面,那么我们引入了next数组
什么是next数组呢?
1,首先:next数组是针对于子串而言的
求子串相对的前缀和后缀,看是否相等(相等即退出),最大的相等个数即就是next的值
如上:上面的数字代表字符在字符数组中的下标
next[0]: "a" (无前缀,无后缀) next[0] = 0
next[1]: "ab" ("a" != "b") next[1] = 0
next[2]: "aba" ("ab"!="ab", "a" == "a") next[2] = 1
next[3]: "abab"
("aba" != "bab" , "ab" == "ab") next[3] = 2
next[4]: "ababc"
("abab" != "babc", "aba" != "abc", "ab" != "bc", "a" != "c")
next[4] = 0
这样求到的next串没有问题,代码也没有问题
程序:
运行结果:
嗯嗯,对,这是我初步的想法,这样求得,但是,但是,但是,翻了翻资料,感觉在时间复杂度和空间复杂度方面太low了,别人的时间空间都是极少的,一定有优化的办法的,一定有
经过长时间的思考,终于想明白了,
哈哈,也就是说,上面的写法做了很多的无用功,
在求后续的next串的时候,完全没有必要去从最大的前缀去求,可以借用之前求到的next
如:串s = "ababc" 求next[4]
由于我们已经知道了,next[3] = 2,("aba" != "bab" , "ab" == "ab"),所以,我们非常的没有必要去做求
(“abab”是不是等于“babc”)等等这些判断,因为从之前的next[1] = 0(可以知道“a”不等于“b”,也就是s[1]和s[0]直接的比较),
所以当前我们要处理的就是:
1,当前的字符(s[4] = 'c')是不是等于next[3]也就是(s[2]),即就是判断s[2]是不是等于s[4],如果相等:next[4] = next[3] + 1
2,如果不相等的话,那么我们需要比较的就是s[4]是不是和s[next[next[3]]],由于next[3] = 2,那就是next[2],
next[2]等于1,那么就是s[4]和s[1]进行比较,判断是否相等,如果相等next[4] = next[2] + 1并且退出;
3,如果不想等的话,那么需要继续执行类似于我们上面的步骤,当带比较的字符为0的时候,那么就退出
这里:我们是通过next[3]简易的求了一下next[4],当然,这里完全可以通过递归或者循环来求出后面的next数组,因为next[0]是知道的(next[0]没有前缀,也没有后缀,所以说:next[0] = 0),由此,我们就可以求出next[1,2,3,4,....]等等
为了便于理解,我没有采用i,j的说法,觉得那样的话,可能会越说越糊涂
下面是一个简单的程序:
通过程序的验证,证明了我们的猜想,也就是说:我们上面说的完全正确,可以大大的削减时间和空间来达到我们的目的,不需要多余占用额外的存储空间,不需要多次循环,不需要做一些无畏的事情
可是,可是,可是???有人到这里就会问了,这个和我们所说的KMP有什么关系啊,这个又和我们上面所说的不用回溯又有什么关系啊???哈哈哈,这可算是问到重点上了,,,
下面我们讨论一下回溯的问题,以及什么是KMP算法
1,先看这个问题(给定一个子串,一个主串,求模式匹配的过程时)
可以知道的是:
子串的next数组是:0 0 1 2 0
主串是从0开始的
我们开始匹配:
1,s[0]与t[0]进行比较,如果不想等,那么主串往后移动,如果相等,那么两者一起移动,对于当前的情况是,两者不想等,那么主串后移
2,s[1]与t[0]进行比较,不相等的话,继续主串后移,相等的话,一起移动, 直到两者字符不相等,或者完全匹配后退出,当前的情况是:
可以直观的看到:s[4] != t[3],这里就调用了next,为什么要调用next呢???因为前面的部分字符“aba”在主 串和子串中已经完全匹配,为了不用回溯和浪费,也为了保证能保留剩余部分的匹配,故引入了next
由于已经比较到了t[3],所以,我们求next[2]的值,然后比较t[next[2]]和s[4](即就是s[4]和t[1]),可以看到
s[4] != t[1] ,然后继续如上的步骤,比较s[4]和t[next[1]](即就是比较s[4]和s[0]),继续比较:
1,两者相同,还是以上的步骤,两者整体移动
2,不同,将主串后移
当前的情况满足条件1,故s[4] == t[0],继续两者后移
3,当前的位置如下:
可知:s[8] != t[4],嗯嗯,如上,继续调用next数组:
比较s[8]和t[next[3]],即就是比较(s[8]和t[2]),判断两者是否相等,可知,对于上图,两者是相等的,所以此时(i = 8 , j = 2),(这里就明显的体现了不用回溯的直接价值啊,哈哈哈),然后,同步后移
4,当前的状态如下:
同理,还是上面的判断步骤,就可以找到了(完全匹配完成了),然后返回匹配后的第一个值
代码如下:
上面的这些也就是所谓的KMP算法了
Knuth-Morris-Pratt算法(简称KMP),是模式匹配中的经典算法,和BF算法相比,KMP算法的不同点是消除BF算法中主串S指针回溯的情况,从而完成的模式匹配,这样的结果使得算法的时间复杂度为O(m+n)
这就是所谓的KMP算法了!!!