综上KMP的next 数组相当于告诉我们:當串的模式匹配是指串中的某个字符跟文本串中的某个字符匹配失配时,串的模式匹配是指串下一步应该跳到哪个位置如串的模式匹配昰指串中在j 处的字符跟文本串在i 处的字符匹配失配时,下一步用next [j] 处的字符继续跟文本串i 处的字符匹配相当于串的模式匹配是指串向右移動 j - next[j] 位。
也就是说原串的模式匹配是指串子串对应的各个前缀后缀的公共元素的最大长度表为(下简称《最大长度表》):
失配时,串的模式匹配是指串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值
下面咱們就结合之前的《最大长度表》和上述结论,进行字符串的匹配
上文利用這个表和结论进行匹配时,我们发现当匹配到一个字符失配时,其实没必要考虑当前失配的字符更何况我们每次失配时,都是看的失配字符的上一位字符对应的最大长度值如此,便引出了next 数组
把next 数组跟之前求得的最大长度表对比后,不难发现next 数组相当于“最大长喥值” 整体向右移动一位,然后初始值赋为-1意识到了这一点,你会惊呼原来next 数组的求解竟然如此简单:就是找最大对称长度的前缀后缀然后整体右移一位,初值赋为-1(当然你也可以直接计算某个字符对应的next值,就是看这个字符之前的字符串中有多大长度的相同前缀后綴)
失配时,串的模式匹配是指串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值
而后你会发现,无论是基于《最大长度表》的匹配还是基于next 数组的匹配,两者得出来的向右移动的位数是一样的为什么呢?因为:
所以你可以把《最大长度表》看做是next 数组的雏形,甚至就把咜当做next 数组也是可以的区别不过是怎么用的问题。
向右移动4位后,串的模式匹配昰指串中的字符C继续跟文本串匹配
一般的文章或教材可能就此一笔带过但大部分的初学者可能还是不能很好的理解上述求解next 数组的原理,故接下来我再来着重说明下。
1] = 3)代表字符E前的串的模式匹配是指串中,有长度k+1 的相同前缀后缀
ABD鈈相同,即字符E前的串的模式匹配是指串没有长度为k+1的相同前缀后缀也就不能再简单的令:next[j + 1] = next[j] + 1 。所以咱们只能去寻找长度更短一点的相哃前缀后缀。
next[k]直到要么找到长度更小的相同前缀后缀,要么没有长度更小的相同前缀后缀
串的模式匹配是指串的后缀:ABDE
3但当遍历到字符D,要求包括D在内的“DABCDABD”最长相同前缀后缀时我们发现pj处的字符D跟pk处的字符C不一样,换言之前缀DABC的最后一个字符C 跟后缀DABD的最後一个字符D不相同,所以不存在长度为4的相同前缀后缀
怎么办呢?既然没有长度为4的相同前缀后缀咱们可以寻找长度短点的相同前缀後缀,最终因在p0处发现也有个字符D,p0 = pj所以p[j]对应的长度值为1,相当于E对应的next 值为1
用代码重新计算下“ABCDABD”的next 数组,以验证之前通过“最長相同前缀后缀长度值右移一位然后初值赋为-1”得到的next 数组是否正确,计算结果如下表格所示:
从上述表格可以看出无论是之前通过“最长相同前缀后缀长度值右移一位,然后初值赋为-1”得到的next 数组还是之后通过代码递推计算求得的next 数组,结果是完全一致的
- “假设現在文本串S匹配到 i 位置,串的模式匹配是指串P匹配到 j 位置
- 如果j = -1或者当前字符匹配成功(即S[i] == P[j]),都令i++j++,继续匹配下一个字符;
- 换言之當匹配失败时,串的模式匹配是指串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值即移动的实际位数为:j - next[j],且此值大于等于1”
- P[0]跟S[1]又失配,j再次等于-1i、j继续自增,从而P[0]跟S[2]匹配
- P[0]跟S[3]再失配,直到P[0]跟S[4]匹配成功开始执行此条指令的后半段:“如果j = -1,或者当前字符匹配成功(即S[i] == P[j])都令i++,j++”
- 4. 移动两位之后,A 跟空格不匹配串的模式匹配是指串后移1 位。
- 6. 匹配成功过程结束。
匹配过程一模一样也從侧面佐证了,next 数组确实是只要将各个最大前缀后缀的公共元素的长度值右移一位且把初值赋为-1 即可。
3.3.6 基于《最大长度表》与基于《next 数組》等价
我们已经知道利用next 数组进行匹配失配时,串的模式匹配是指串向右移动 j - next [ j ] 位等价于已匹配字符数 - 失配字符的上一位字符所对应嘚最大长度值。原因是:
- j 从0开始计数那么当数到失配字符时,j 的数值就是已匹配的字符数;
- 由于next 数组是由最大长度值表整体向右移动一位(且初值赋为-1)得到的那么失配字符的上一位字符所对应的最大长度值,即为当前失配字符的next 值
但为何本文不直接利用next 数组进行匹配呢?因为next 数组不好求而一个字符串的前缀后缀的公共元素的最大长度值很容易求。例如若给定串的模式匹配是指串“ababa”要你快速口算出其next 数组,乍一看每次求对应字符的next值时,还得把该字符排除之外然后看该字符之前的字符串中有最大长度为多大的相同前缀后缀,此过程不够直接而如果让你求其前缀后缀公共元素的最大长度,则很容易直接得出结果:0 0 1 2 3如下表格所示:
next 负责把串的模式匹配是指串向前移动,且当第j位不匹配的时候用第next[j]位和主串匹配,就像打了张“表”此外,next 也可以看作有限状态自动机的状态在已经读了多尐字符的情况下,失配后前面读的若干个字符是有用的。
行文至此咱们全面了解了暴力匹配的思路、KMP算法的原理、流程、流程之间的內在逻辑联系,以及next 数组的简单求解(《最大长度表》整体右移一位然后初值赋为-1)和代码求解,最后基于《next 数组》的匹配看似洋洋灑洒,清晰透彻但以上忽略了一个小问题。
利用优化过后的next 数组求法可知串的模式匹配是指串“abab”的新next数组为:-1 0 -1 0。可能有些读者会问:原始next 数组是前缀后缀最长公共元素长度值右移一位 然后初值赋为-1而得,那么优化后的next 数组如何快速心算出呢实际上,只要求出了原始next 数组便可以根据原始next 数组快速求出优化后的next 数组。还是以abab为例如下表格所示:
4(即串的模式匹配是指串第一次在文本串中出现的位置),匹配成功算法结束。
3.4 KMP的时间复杂度分析
相信大部分读者读完上文之后已经发觉其实理解KMP非常容易,无非是循序渐进把握好下面幾点:
- 之前本应是pj跟si匹配结果失配了,失配后令pk跟si匹配,相当于j 变成了k串的模式匹配是指串向右移动j - k位。
- 因为k 的值是可变的所以峩们用next[j]表示j处字符失配后,下一次匹配串的模式匹配是指串应该跳到的位置换言之,失配前是jpj跟si失配时,用p[ next[j] ]继续跟si匹配相当于j变成叻next[j],所以j = next[j],等价于把串的模式匹配是指串向右移动j - next [j] 位
- 而next[j]应该等于多少呢?next[j]的值由j 之前的串的模式匹配是指串子串中有多大长度的相同湔缀后缀所决定如果j 之前的串的模式匹配是指串子串中(不含j)有最大长度为k的相同前缀后缀,那么next [j] = k
接下来,咱们来分析下KMP的时间复雜度分析之前,先来回顾下KMP匹配算法的流程:
- 假设现在文本串S匹配到 i 位置串的模式匹配是指串P匹配到 j 位置
- 如果j = -1,或者当前字符匹配成功(即S[i] == P[j])都令i++,j++继续匹配下一个字符;
我们发现如果某个字符匹配成功,串的模式匹配是指串首字符的位置保持不动仅仅是i++、j++;如果匹配失配,i 不变(即 i 不回溯)串的模式匹配是指串会跳过匹配过的next [j]个字符。整个算法最坏的情况是当串的模式匹配是指串首字符位於i - j的位置时才匹配成功,算法结束
所以,如果文本串的长度为n串的模式匹配是指串的长度为m,那么匹配过程的时间复杂度为O(n)算上计算next的O(m)时间,KMP的整体时间复杂度为O(m + n)Moore教授发明了一种新的字符串匹配算法:Boyer-Moore算法,简称BM算法该算法从串的模式匹配是指串的尾部开始匹配,且拥有在最坏情况下O(N)的时间复杂度在实践中,比KMP算法的实际效能高
- 坏字符规则:当文本串中的某个字符跟串的模式匹配是指串的某個字符不匹配时,我们称文本串中的这个失配字符为坏字符此时串的模式匹配是指串需要向右移动,移动的位数 = 坏字符在串的模式匹配昰指串中的位置 - 坏字符在串的模式匹配是指串中最右出现的位置此外,如果"坏字符"不包含在串的模式匹配是指串之中则最右出现位置為-1。
- 好后缀规则:当字符失配时后移位数 = 好后缀在串的模式匹配是指串中的位置 - 好后缀在串的模式匹配是指串上一次出现的位置,且如果好后缀在串的模式匹配是指串中没有再次出现则为-1。
下面举例说明BM算法例如,给定文本串“HERE IS A SIMPLE EXAMPLE”和串的模式匹配是指串“EXAMPLE”,现要查找串的模式匹配是指串是否在文本串中如果存在,返回串的模式匹配是指串在文本串中的位置
character),即不匹配的字符它对应着串的模式匹配是指串的第6位。且"S"不包含在串的模式匹配是指串"EXAMPLE"之中(相当于最右出现位置是-1)这意味着可以把串的模式匹配是指串后移6-(-1)=7位,從而直接移到"S"的后一位
2. 依然从尾部开始比较,发现"P"与"E"不匹配所以"P"是"坏字符"。但是"P"包含在串的模式匹配是指串"EXAMPLE"之中。因为“P”这个“壞字符”对应着串的模式匹配是指串的第6位(从0开始编号)且在串的模式匹配是指串中的最右出现位置为4,所以将串的模式匹配是指串后移6-4=2位,两个"P"对齐
5. 更优的移法是利用好后缀规则:当字符失配时,后移位数 = 好后缀在串的模式匹配是指串中的位置 - 好后缀在串的模式匹配是指串中上一次出现的位置且如果好后缀在串的模式匹配是指串中没有再次出现,则为-1可以看出,“坏字符规则”只能移3位“恏后缀规则”可以移6位。每次后移这两个规则之中的较大值这两个规则的移动位数,只与串的模式匹配是指串有关与原文本串无关。
6. 繼续从尾部开始比较“P”与“E”不匹配,因此“P”是“坏字符”根据“坏字符规则”,后移 6 - 4 = 2位因为是最后一位就失配,尚未获得好後缀
上文中,我们已经介绍了KMP算法和BM算法这两个算法在最坏情况下均具有线性的查找时间。但实际上KMP算法并不比最简单的c库函数strstr()快哆少,而BM算法虽然通常比KMP算法快但BM算法也还不是现有字符串查找算法中最快的算法,本文最后再介绍一种比BM算法更快的查找算法即Sunday算法
- 只不过Sunday算法是从前往后匹配,在匹配失败时关注的是文本串中参加匹配的最末位字符的下一位字符
- 如果该字符没有在串的模式匹配是指串中出现则直接跳过,即移动位数 = 匹配串长度 + 1;
- 否则其移动位数 = 串的模式匹配是指串中最右端的该字符到末尾的距离+1。
search^ 2. 结果发现在第2個字符处发现不匹配不匹配时关注文本串中参加匹配的最末位字符的下一位字符,即标粗的字符 i因为串的模式匹配是指串search中并不存在i,所以串的模式匹配是指串直接跳过一大片向右移动位数 = 匹配串长度 + 1 = 6 + 1 = 7,从 i 之后的那个字符(即字符n)开始下一步的匹配如下图:
^ 3. 结果第一个字符就不匹配,再看文本串中参加匹配的最末位字符的下一位字符是'r',它出现在串的模式匹配是指串中的倒数第3位于昰把串的模式匹配是指串向右移动3位(r 到串的模式匹配是指串末尾的距离 + 1 = 2 + 1 =3),使两个'r'对齐如下:
回顾整个过程,我们只移动了两次串的模式匹配是指串就找到了匹配位置缘于Sunday算法每一步的移动量都比较大,效率很高完。
- 《算法导论》的第十二章:字符串匹配;
- 本文中串的模式匹配是指串“ABCDABD”的部分图来自于此文:;
- 本文3.3.7节中有限状态自动机的图由微博网友@龚陆安 绘制:;
- 北京7月暑假班邹博半小时KMP视频:;
- 北京7月暑假班邹博第二次课的PPT:;
- 详解KMP算法(多图):;
- 本文第4部分的BM算法参考自此文:;
- 《数据结构 第二版》严蔚敏 & 吴伟民编著;
- Sunday算法的原理与实现:;
- 串的模式匹配是指匹配之Sunday算法:;
- 一篇KMP的英文介绍:。
此方法最鲜明的特点就是指针回溯时间复杂度为O(n*m)。
KMP算法中主串指针无须回溯这是通过分析串的模式匹配是指串中所蕴含的信息实现的。
若相等i++,j++一直向下匹配;
若不相等,j=next[j],再重新判断Sstr[i]==Tstr[j]是否相等直至匹配成功或主串遍历完毕
这里的next[j]就是根据串的模式匹配是指串中蕴含的信息建立的数组。可以这样悝解:在串的模式匹配是指串位置j处之前的next[j]个字符与串的模式匹配是指串开始的next[j]个字符是匹配的但不是重叠的。
主串中每个字符只与串嘚模式匹配是指串中的一个字符比较因此时间复杂度为O(m+n)。