最近在准备面试,偶尔会遇见有面试官让同学们完成字符串模式匹配的情况,模式匹配问题中久负盛名的自然当属KMP(Knuth-Morris-Pratt )算法,关于KMP算法,July大神在今年八月份对此进行了全面总结(详见http://blog.youkuaiyun.com/v_july_v/article/details/7041827),在此处我就不再重复叙述,只简要介绍其基本思想及应用的优缺点。
KMP算法的目的是减少回溯造成的时间浪费,而其基本思想是给出如下:
1.输入字符串s=“BBC ABCDAB ABCDABCDABDE”,其游标为i;
2.模式字符串p=“ABCDABD”,其游标为j;
3.匹配时,判断s[i] == s[j]与否
4.如果某次从i=i0开始的匹配到达i=i0+k处匹配失败,即s[i0] == p[0] && s[i0+k] != p[k],传统的暴力搜索方法,将进一步从i=i0+1进行搜索,而KMP则不同,利用模式串中某字符前面与整个模式串所具有的最长公共前缀逃避前溯工作。
具体而言,如果存在0<t<k,使得从i = i0+t开始进行匹配直到i = i0+k都能准确匹配的话(如果不存在这样的t,那么中间的这些位置都不能实现匹配过程),那么就会有s[i0+t ~ i0+k] == p[0 ~ (k-t)],而s[i0+t ~ i0+k-1] == p[t ~ k-1],这就表明,为了实现准确匹配,那么模式串p[k]元素之前必然与p有着(k-1
- t + 1)位的最长公共前缀,而这公共前缀是能实现与s[i0+k]的前面字符相匹配的,所以,如果继续匹配下去,就可以将这一段的匹配工作省去。
根据前面的叙述,我们最终匹配到i = i0+k处,那么,如果我们知道了p[k]前面的最长公共前缀(假设是u,即next[k] = u),那么我们就可以从s[i0+k]与p[u]开始比较,也就是令j=u,而i保持i0+k不变,从而避免了回溯过程。
之后的工作就是循环往复避免回溯就行了,至于next数组,里面存储的是模式串中各位字符前面(从第二位开始,到当前字符的前一位止)与整个模式串的公共前缀。如ABCDABD的next数组记录为『-1,0,0,0,0,1,2』,此数组的计算可以通过动态规划完成(next[0]
= -1),当i > 0时,
- next[i] = next[i-1]+1, if p[i-1] == p[next[i-1]]
- next[i] = 0, else
KMP算法时间复杂度为O(m+n),空间复杂度为O(m)(next数组存储),效率非常高,声明不显的一个重要原因是其比较难懂,正因如此,能够写出KMP算法的同学则会得到面试官的青眼相看,曾有位师姐去面试某家企业,人家也不出别的题目,直接将KMP祭出来,这位师姐恰好头天晚上复习过,巧之又巧的通过了面试,而师姐的一位朋友面试相同公司的时候依然遭遇了KMP,这回运气就不怎么好,没能通过。从这里就可以看出来KMP在面试官心中的地位,绝对杀器。
废话少说,今天这里还要介绍的一个算法是最小表示法,最小表示法常用在判别同构问题上,举个简单的例子,给出两个相同的数组,请判断这两个数组是否同构,同构在此处的含义是其中元素一一对应,我们知道长度为n的数组,其同构数组有n!个,要想将这些同构数组一个个列出来肯定是不现实的,最简便的方法就是化无序为有序,将两个数组按照同样的规则进行排序,排好序之后直接按照顺序进行比对,这样就能轻松愉快的将整个问题大化小,小化了。
当然,平常遇到的同构问题可能不是这么简单的,不过基本思想就是这样了,按照某种规则对两个序列进行转换,之后再一一比对即可,而最小表示法之所以叫“最小”这么个名字,其实也是因为通常我们都是按照升序原则,如字典序法中的最小顺序来进行转换(当然也可以按照降序原则,取之为最大表示法,原理相同)。
来个例子吧,关于最小表示法,目前百度上最容易搜到的比较详细的资料就来源于一高中生的PPT(却让众多研究生们直呼看不懂,白发三千里,缘愁似个长)http://wenku.baidu.com/view/a22ec3615727a5e9856a61c4.html。比如,给出两个字符串s1="babba",s2="bbaba",判断s1,s2是否同构(此处的同构是指循环同构,即abs与sab以及bsa相同构,其他类似的题目还有比如判断由各种颜色的珠子组成的两串珠链是否同构,循环同构)。
长度为n的序列的循环顺序共有n种,如何找到它的最小字典序?我们来看一下最小表示法是怎么做的。
1.先将两串字符串均复制一遍,U=s1s1="babbababba",W=s2s2="bbababbaba",这样做的好处是将循环转换成线性的(这也是处理循环问题中的一种常用手段),这样每个字符串的任一循环同构字符串均能在相应的重复串中找到匹配,或者说重复串中任意连续的n个字符都是原串的一个同构串。
2.令i为U的游标,而j为W的游标,从i=0,j=0开始依次比较判断,U[i+k] == W[j+k]?
3.当某个k不满足上述等式时,U[i+k] != W[j+k],假设U[i+k] > W[j+k](反之也有相似的处理方式),那么我们可以知道U[i~i+k-1]
== W[j~j+k-1],那么保持较小元素对应的游标j不变,将i置为i+k,之所以如此做,是因为我们认为在在刚才给出两段相等字符段中与较大字符相对应的字符段中,不可能存在最小表示法的起始字符,口头证明如下:
反证法:假设存在i<t<k,使得从U[t]开始的n长度的字符串是s1的最小表示法,那么这一段字符串就应该在任何一位上都比其他的同构字符段要不大,但是我们很容易可以看到W[t]开始的字符段到W[j+k]必定比U[t]开始的字符段到U[i+k]要小,而显然这样的一段字符段的长度并没有超出n,如此可以判别,在相等区域中不存在最小表示法的开始位置。(这个方法的一个假设是s1与s2同构,至于假设是否成立只需将整个程序运行完即可知道,如果最终的结构是两者同构,那么假设成立,如果最终的结果是两者不同构,那么只是多运行了一段时间而已,并未对结果的有效性有任何的损害)
4.之后依照上述的判别过程重复进行判别,我们可以看到,每一次判别都会导致i或者j前增k位,所以这个程序的运行时间不会超过2n,即时间复杂度为O(n)。
至此,就完成了最小表示法的实现,其图示过程如下(摘自周源PPT)
这篇博文简要介绍了KMP算法与最小表示法,这两种方法分别应用在不同的场合中,均是为了实现匹配,均有着运行效率高的优点,不同的是KMP主要用于查找最长模式匹配(而如果是最长公共字串,则应该使用DP算法实现),最小表示法常常跟循环同构相关联(如果不是循环的,就直接排序完了),思路非常出彩,都是经典算法,不能错过。