徐州市城乡建设局门户网站,帝国cms更改网站ico,中介网站模板,网站域名被抢注做商标本人在July大神的博客“从头到尾彻底理解KMP”的帮助下终于理解了KMP算法的核心思想和实现方法#xff0c;最后附上了hihocoder上要求统计匹配次数的的应用实例。 1. 引言 本KMP原文最初写于2年多前的2011年12月#xff0c;因当时初次接触KMP#xff0c;思路混乱导致写也写得…本人在July大神的博客“从头到尾彻底理解KMP”的帮助下终于理解了KMP算法的核心思想和实现方法最后附上了hihocoder上要求统计匹配次数的的应用实例。 1. 引言 本KMP原文最初写于2年多前的2011年12月因当时初次接触KMP思路混乱导致写也写得混乱如此留言也是“骂声”一片。所以一直想找机会重新写下KMP但苦于一直以来对KMP的理解始终不够故才迟迟没有修改本文。 然近期因在北京开了个算法班专门讲解数据结构、面试、算法才再次仔细回顾了这个KMP在综合了一些网友的理解、以及跟我一起讲算法的两位讲师朋友曹博、邹博的理解之后写了9张PPT发在微博上。随后一不做二不休索性将PPT上的内容整理到了本文之中。 KMP本身不复杂但网上绝大部分的文章包括本文的2011年版本把它讲混乱了。下面咱们从暴力匹配算法讲起随后阐述KMP的流程 步骤、next 数组的简单求解 递推原理 代码求解接着基于next 数组匹配谈到有限状态自动机next 数组的优化KMP的时间复杂度分析最后简要给出一个KMP的扩展算法。 全文力图给你一个最为完整最为清晰的KMP希望更多的人不再被KMP折磨或纠缠不再被一些混乱的文章所混乱有何疑问欢迎随时留言评论thanks。 2. 暴力匹配算法 假设现在我们面临这样一个问题有一个文本串S和一个模式串P现在要查找P在S中的位置怎么查找呢 如果用暴力匹配的思路并假设现在文本串S匹配到 i 位置模式串P匹配到 j 位置则有
如果当前字符匹配成功即S[i] P[j]则ij继续匹配下一个字符如果失配即S[i]! P[j]令i i - (j - 1)j 0。相当于每次匹配失败时i 回溯j 被置为0。
理清楚了暴力匹配算法的流程及内在的逻辑咱们可以写出暴力匹配的代码如下 int ViolentMatch(char* s, char* p) { int sLen strlen(s); int pLen strlen(p); int i 0; int j 0; while (i sLen j pLen) { if (s[i] p[j]) { //①如果当前字符匹配成功即S[i] P[j]则ij i; j; } else { //②如果失配即S[i]! P[j]令i i - (j - 1)j 0 i i - j 1; j 0; } } //匹配成功返回模式串p在文本串s中的位置否则返回-1 if (j pLen) return i - j; else return -1; } 举个例子如果给定文本串S“BBC ABCDAB ABCDABCDABDE”和模式串P“ABCDABD”现在要拿模式串P去跟文本串S匹配整个过程如下所示 1. S[0]为BP[0]为A不匹配执行第②条指令“如果失配即S[i]! P[j]令i i - (j - 1)j 0”S[1]跟P[0]匹配相当于模式串要往右移动一位i1j0 2. S[1]跟P[0]还是不匹配继续执行第②条指令“如果失配即S[i]! P[j]令i i - (j - 1)j 0”S[2]跟P[0]匹配i2j0从而模式串不断的向右移动一位不断的执行“令i i - (j - 1)j 0”i从2变到4j一直为0 3
. 直到S[4]跟P[0]匹配成功i4j0此时按照上面的暴力匹配算法的思路转而执行第①条指令“如果当前字符匹配成功即S[i] P[j]则ij”可得S[i]为S[5]P[j]为P[1]即接下来S[5]跟P[1]匹配i5j1 4
. S[5]跟P[1]匹配成功继续执行第①条指令“如果当前字符匹配成功即S[i] P[j]则ij”得到S[6]跟P[2]匹配i6j2如此进行下去 5. 直到S[10]为空格字符P[6]为字符Di10j6因为不匹配重新执行第②条指令“如果失配即S[i]! P[j]令i i - (j - 1)j 0”相当于S[5]跟P[0]匹配i5j0 6
. 至此我们可以看到如果按照暴力匹配算法的思路尽管之前文本串和模式串已经分别匹配到了S[9]、P[5]但因为S[10]跟P[6]不匹配所以文本串回溯到S[5]模式串回溯到P[0]从而让S[5]跟P[0]匹配。 而S[5]肯定跟P[0]失配。为什么呢因为在之前第4步匹配中我们已经得知S[5] P[1] B而P[0] A即P[1] ! P[0]故S[5]必定不等于P[0]所以回溯过去必然会导致失配。那有没有一种算法让i 不往回退只需要移动j 即可呢 答案是肯定的。这种算法就是本文的主旨KMP算法它利用之前已经部分匹配这个有效信息保持i 不回溯通过修改j 的位置让模式串尽量地移动到有效的位置。 3. KMP算法 3.1 定义
Knuth-Morris-Pratt 字符串查找算法简称为 “KMP算法”常用于在一个文本串S内查找一个模式串P 的出现位置这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人同时独立发现故取这3人的姓氏命名此算法。 下面先直接给出KMP的算法流程 如果感到一点点不适没关系坚持下稍后会有具体步骤及解释越往后看越会柳暗花明☺ 假设现在文本串S匹配到 i 位置模式串P匹配到 j 位置 如果j -1或者当前字符匹配成功即S[i] P[j]都令ij继续匹配下一个字符如果j ! -1且当前字符匹配失败即S[i] ! P[j]则令 i 不变j next[j]。此举意味着失配时模式串P相对于文本串S向右移动了j - next [j] 位。 换言之当匹配失败时模式串向右移动的位数为失配字符所在位置 - 失配字符对应的next 值next 数组的求解会在下文的3.3.3节中详细阐述即移动的实际位数为j - next[j]且此值大于等于1。 很快你也会意识到next 数组各值的含义代表当前字符之前的字符串中有多大长度的相同前缀后缀。例如如果next [j] k代表j 之前的字符串中有最大长度为 k 的相同前缀后缀。 此也意味着在某个字符失配时该字符对应的next 值会告诉你下一步匹配中模式串应该跳到哪个位置跳到next [j] 的位置。如果next [j] 等于0或-1则跳到模式串的开头字符若next [j] k 且 k 0代表下次匹配跳到j 之前的某个字符而不是跳到开头且具体跳过了k 个字符。 转换成代码表示则是 int KmpSearch(char* s, char* p) { int i 0; int j 0; int sLen strlen(s); int pLen strlen(p); while (i sLen j pLen) { //①如果j -1或者当前字符匹配成功即S[i] P[j]都令ij if (j -1 || s[i] p[j]) { i; j; } else { //②如果j ! -1且当前字符匹配失败即S[i] ! P[j]则令 i 不变j next[j] //next[j]即为j所对应的next值 j next[j]; } } if (j pLen) return i - j; else return -1; } 继续拿之前的例子来说当S[10]跟P[6]匹配失败时KMP不是跟暴力匹配那样简单的把模式串右移一位而是执行第②条指令“如果j ! -1且当前字符匹配失败即S[i] ! P[j]则令 i 不变j next[j]”即j 从6变到2后面我们将求得P[6]即字符D对应的next 值为2所以相当于模式串向右移动的位数为j - next[j]j - next[j] 6-2 4。 向右移动4位后S[10]跟P[2]继续匹配。为什么要向右移动4位呢因为移动4位后模式串中又有个“AB”可以继续跟S[8]S[9]对应着从而不用让i 回溯。相当于在除去字符D的模式串子串中寻找相同的前缀和后缀然后根据前缀后缀求出next 数组最后基于next 数组进行匹配不关心next 数组怎么求来的只想看匹配过程是咋样的可直接跳到下文 3.3.4节。 3.2 步骤 ①寻找前缀后缀最长公共元素长度 对于Pj p0 p1 ...pj-1寻找模式串Pj中长度最大且相等的前缀和后缀 即寻找满足条件的最大的k使得p0 p1 ...pk-1 pj-k pj-k1...pj-1。也就是说k是模式串中各个子串的前缀后缀的公共元素的长度所以求最大的k就是看某个子串的哪个前缀后缀的公共元素最多。举个例子如果给定的模式串为“abaabcaba”那么它的各个子串的前缀后缀的公共元素的最大长度值如下表格所示 ②求next数组 根据第①步骤中求得的各个前缀后缀的公共元素的最大长度求得next 数组相当于前者右移一位且初值赋为-1如下表格所示 ③匹配失配模式串向右移动的位数为j - next[j]。换言之当模式串的后缀pj-k pj-k1, ..., pj-1 跟文本串si-k si-k1, ..., si-1匹配成功但pj 跟si匹配失败时因为p0 p1 ...pk-1 pj-k pj-k1...pj-1 next[j] k故令j next[j]从而让模式串右移j - next[j] 位使得模式串的前缀p0 p1, ..., pk-1对应着文本串 si-k si-k1, ..., si-1而后让pk 跟si 继续匹配。 注j 是模式串中失配字符的位置且 j 从0开始计数。 综上KMP的next 数组相当于告诉我们当模式串中的某个字符跟文本串中的某个字符匹配失配时模式串下一步应该跳到哪个位置。如模式串中在j 处的字符跟文本串在i 处的字符匹配失配时下一步用next [j] 处的字符继续跟文本串i 处的字符匹配相当于模式串向右移动 j - next[j] 位。 接下来分别具体解释上述3个步骤。 3.3 解释 3.3.1 寻找最长前缀后缀
如果给定的模式串是“ABCDABD”从左至右遍历整个模式串其各个子串的前缀后缀分别如下表格所示 也就是说原字符串对应的各个前缀后缀的公共元素的最大长度表为 下简称《最大长度表》 3.3.2 基于《最大长度表》匹配 因为模式串中首尾可能会有重复的字符故可得出下述结论 失配时模式串向右移动的位数为已匹配字符数 - 失配字符的上一位字符所对应的最大长度值 下面咱们就结合之前的《最大长度表》和上述结论进行字符串的匹配。如果给定文本串“BBC ABCDAB ABCDABCDABDE”和模式串“ABCDABD”现在要拿模式串去跟文本串匹配如下图所示 1. 因为模式串中的字符A跟文本串中的字符B、B、C、空格一开始就不匹配所以不必考虑结论直接将模式串不断的右移一位即可直到模式串中的字符A跟文本串的第5个字符A匹配成功 2. 继续往后匹配当模式串最后一个字符D跟文本串匹配时失配显而易见模式串需要向右移动。但向右移动多少位呢因为此时已经匹配的字符数为6个ABCDAB然后根据《最大长度表》可得失配字符D的上一位字符B对应的长度值为2所以根据之前的结论可知需要向右移动6 - 2 4 位。 3. 模式串向右移动4位后发现C处再度失配因为此时已经匹配了2个字符AB且上一位字符B对应的最大长度值为0所以向右移动2 - 0 2 位。 4. A与空格失配向右移动1 位。 5. 继续比较发现D与C 失配故向右移动的位数为已匹配的字符数6减去上一位字符B对应的最大长度2即向右移动6 - 2 4 位。 6. 经历第5步后发现匹配成功过程结束。 通过上述匹配过程可以看出问题的关键就是寻找模式串中最大长度的相同前缀和后缀找到了模式串中每个字符之前的前缀和后缀公共部分的最大长度后便可基于此匹配。而这个最大长度便正是next 数组要表达的含义。 3.3.3 根据《最大长度表》求出next 数组 由上文我们已经知道字符串“ABCDABD”各个前缀后缀的最大公共元素长度分别为 而且根据这个表可以得出下述结论 失配时模式串向右移动的位数为已匹配字符数 - 失配字符的上一位字符所对应的最大长度值
上文利用这个表和结论进行匹配时我们发现当匹配到一个字符失配时其实没必要考虑当前失配的字符更何况我们每次失配时都是看的失配字符的上一位字符对应的最大长度值。如此便引出了next 数组。 给定字符串“ABCDABD”可求得它的next 数组如下 把next 数组跟之前求得的最大长度表对比后不难发现next 数组相当于“最大长度值” 整体向右移动一位然后初始值赋为-1。意识到了这一点你会惊呼原来next 数组的求解竟然如此简单就是找最大对称长度的前缀后缀然后整体右移一位初值赋为-1 换言之对于给定的模式串ABCDABD它的最大长度表及next 数组分别如下 根据最大长度表求出了next 数组后从而有 失配时模式串向右移动的位数为失配字符所在位置 - 失配字符对应的next 值 而后你会发现无论是基于《最大长度表》的匹配还是基于next 数组的匹配两者得出来的向右移动的位数是一样的。为什么呢因为
根据《最大长度表》失配时模式串向右移动的位数 已经匹配的字符数 - 失配字符的上一位字符的最大长度值而根据《next 数组》失配时模式串向右移动的位数 失配字符的位置 - 失配字符对应的next 值 其中从0开始计数时失配字符的位置 已经匹配的字符数失配字符不计数而失配字符对应的next 值 失配字符的上一位字符的最大长度值两相比较结果必然完全一致。 所以你可以把《最大长度表》看做是next 数组的雏形甚至就把它当做next 数组也是可以的区别不过是怎么用的问题。更何况两者所要表达的信息一致都是为了传达出某个字符之前的模式串的子串有多大长度的相同前缀后缀。 接下来咱们来写代码求下next 数组。 基于之前的理解可知计算next 数组的方法可以采用递推
1. 如果对于值k已有p0 p1, ..., pk-1 pj-k pj-k1, ..., pj-1相当于next[j] k。 此意味着什么呢究其本质next[j] k 代表p[j] 之前的模式串子串中有长度为k 的相同前缀和后缀。有了这个next 数组在KMP匹配中当模式串后缀中j 处的字符失配时模式串向右移动j - next[j] 位。 举个例子如下图根据模式串“ABCDABD”的next 数组可知失配位置的字符D对应的next 值为2代表字符D前有长度为2的相同前缀和后缀这个相同的前缀后缀即为“AB”失配后模式串需要向右移动j - next [j] 6 - 2 4位。 向右移动4位后模式串中的字符C继续跟文本串匹配。 2. 下面的问题是已知next [0, ..., j]如何求出next [j 1]呢 对于pattern的前j1个序列字符
若pattern[k] pattern[j]则next[j 1 ] next [j] 1 k 1若pattern[k ] ≠ pattern[j]如果此时pattern[ next[k] ] pattern[j ]则next[ j 1 ] next[k] 1否则继续递归重复此过程。 相当于在字符p[j1]之前不存在长度为k1的前缀p0 p1, …, pk-1 pk跟后缀“pj-k pj-k1, …, pj-1 pj相等那么是否可能存在另一个值t1 k1使得长度更小的前缀 “p0 p1, …, pt-1 pt” 等于长度更小的后缀 “pj-t pj-t1, …, pj-1 pj” 呢如果存在那么这个t1 便是next[ j1]的值此相当于利用next 数组进行P串前缀跟P串后缀的匹配。
一般的文章或教材可能就此一笔带过但大部分的初学者可能还是不能很好的理解上述求解next 数组的原理故接下来我再来着重说明下。 如下图所示假定给定模式串ABCDABCE且已知next [j] k相当于“p0 pk-1” “pj-k pj-1” AB可以看出k为2现要求next [j 1]等于多少因为pk pj C所以next[j 1] next[j] 1 k 1可以看出next[j 1] 3。代表字符E前的模式串中有长度k1 的相同前缀后缀。 但 如果pk ! pj 呢说明“p0 pk-1 pk” ≠ “pj-k pj-1 pj”。换言之当pk ! pj后字符E前有多大长度的相同前缀后缀呢很明显因为C不同于D所以ABC 跟 ABD不相同即字符E前的模式串没有长度为k1的相同前缀后缀也就不能再简单的令next[j 1] next[j] 1 。所以咱们只能去寻找长度更短一点的相同前缀后缀。 结合上图来讲若能 在前缀 “ p0 pk-1 pk ” 中不断的递归k next [k]找到一个字符pk’ 也为D代表pk’ pj且满足p0 pk-1 pk pj-k pj-1 pj则最大相同的前缀后缀长度为k 1从而next [j 1] k’ 1 next [k ] 1。否则前缀中没有D则代表没有相同的前缀后缀next [j 1] 0。 所以因最终在前缀ABC中没有找到D故E的next 值为0 模式串的后缀AB DE 模式串的前缀AB C 前缀右移两位 ABC 此外咱们还可以换个角度理解这个问题 类似KMP的匹配思路当p0 p1, ..., pj 跟主串s0 s1, ..., si匹配时如果模式串在j处失配则j next [j]相当于模式串需要向右移动j - next[j] 位。现在前缀“p0 pk-1 pk” 去跟后缀 “pj-k pj-1 pj”匹配发现在pk处匹配失败那么前缀需要向右移动多少位呢根据已经求得的前缀各个字符的next 值可得前缀应该向右移动k - next[k]位相当于k next[k]。 若移动之后pk pj则代表字符E前存在长度为next[ k ] 1的相同前缀后缀否则继续递归k next [k]直到pk’’ 跟pj匹配成功或者不存在任何k0 k j满足pk pj 且 k next[k] -1停止递归。 综上可以通过递推求得next 数组代码如下所示 void GetNext(char* p,int next[]) { int pLen strlen(p); next[0] -1; int k -1; int j 0; while (j pLen - 1) { //p[k]表示前缀p[j]表示后缀 if (k -1 || p[j] p[k]) { j; k; next[j] k; } else { k next[k]; } } } 3.3.4 基于《next 数组》匹配 下面我们来基于next 数组进行匹配。 还是给定
文本串“BBC ABCDAB ABCDABCDABDE”和模式串“ABCDABD”现在要拿模式串去跟文本串匹配如下图所示 在正式匹配之前让我们来再次回顾下上文2.1节所述的KMP算法的匹配流程
“假设现在文本串S匹配到 i 位置模式串P匹配到 j 位置 如果j -1或者当前字符匹配成功即S[i] P[j]都令ij继续匹配下一个字符如果j ! -1且当前字符匹配失败即S[i] ! P[j]则令 i 不变j next[j]。此举意味着失配时模式串P相对于文本串S向右移动了j - next [j] 位。 换言之当匹配失败时模式串向右移动的位数为失配字符所在位置 - 失配字符对应的next 值即移动的实际位数为j - next[j]且此值大于等于1。”
1. 最开始匹配时 P[0]跟S[0]匹配失败 所以执行“如果j ! -1且当前字符匹配失败即S[i] ! P[j]则令 i 不变j next[j]”所以j -1故转而执行“如果j -1或者当前字符匹配成功即S[i] P[j]都令ij”得到i 1j 0即P[0]继续跟S[1]匹配。 P[0]跟S[1]又失配j再次等于-1i、j继续自增从而P[0]跟S[2]匹配。P[0]跟S[2]失配后P[0]又跟S[3]匹配。P[0]跟S[3]再失配直到P[0]跟S[4]匹配成功开始执行此条指令的后半段“如果j -1或者当前字符匹配成功即S[i] P[j]都令ij”。 2. P[1]跟S[5]匹配成功P[2]跟S[6]也匹配成功, ...直到当匹配到字符D时失配即S[10] ! P[6]由于 j 从0开始计数故数到失配的字符D时 j 为6且字符D对应的next 值为2所以向右移动的位数为j - next[j] 6 - 2 4 位 3. 向右移动4位后C再次失配向右移动j - next[j] 2 - 0 2 位 4. 移动两位之后A 跟空格不匹配再次后移1 位 5. D处失配向右移动 j - next[j] 6 - 2 4 位 6. 匹配成功过程结束。 匹配过程一模一样。也从侧面佐证了next 数组确实是只要将各个最大前缀后缀的公共元素的长度值右移一位且把初值赋为-1 即可。 3.3.5 基于《最大长度表》与基于《next 数组》等价 我们已经知道利用next 数组进行匹配失配时模式串向右移动 j - next [ j ] 位等价于已匹配字符数 - 失配字符的上一位字符所对应的最大长度值。原因是
j 从0开始计数那么当数到失配字符时j 的数值就是已匹配的字符数由于next 数组是由最大长度值表整体向右移动一位且初值赋为-1得到的那么失配字符的上一位字符所对应的最大长度值即为当前失配字符的next 值。 但为何本文不直接利用next 数组进行匹配呢因为next 数组不好求而一个字符串的前缀后缀的公共元素的最大长度值很容易求例如若给定模式串“ababa”要你求其next 数组则乍一看无从求起。而如果你求其前缀后缀公共元素的最大长度则很容易得出是0 0 1 2 3如下表格所示 然后这5个数字 全部整体右移一位且初值赋为-1即得到其next 数组-1 0 0 1 2。 3.3.6 Next 数组与有限状态自动机 next 负责把模式串向前移动且当第j位不匹配的时候用第next[j]位和主串匹配就像打了张“表”。此外next 也可以看作有限状态自动机的状态在已经读了多少字符的情况下失配后前面读的若干个字符是有用的。 3.3.7 Next 数组的优化 行文至此咱们全面了解了暴力匹配的思路、KMP算法的原理、流程、流程之间的内在逻辑联系以及next 数组的简单求解《最大长度表》整体右移一位然后初值赋为-1和代码求解最后基于《next 数组》的匹配看似洋洋洒洒清晰透彻但以上忽略了一个小问题。 比如如果用之前的next 数组方法求模式串“abab”的next 数组可得其next 数组为-1 0 0 10 0 1 2整体右移一位初值赋为-1当它跟下图中的文本串去匹配的时候发现b跟c失配于是模式串右移j - next[j] 3 - 1 2位。 右移2位后b又跟c失配。事实上因为在上一步的匹配中已经得知p[3] b与s[3] c失配而右移两位之后让p[ next[3] ] p[1] b 再跟s[3]匹配时必然失配。问题出在哪呢 问题出在不该出现p[j] p[ next[j] ]。为什么呢理由是
当p[j] ! s[i] 时下次匹配必然是p[ next [j]] 跟s[i]匹配如果p[j] p[ next[j] ]必然导致后一步匹配失败所以不能允许p[j] p[ next[j ]]。 因为p[j]已经跟s[i]失配然后你还用跟p[j]等同的值p[next[j]]去跟s[i]匹配很显然必然失配。 所以咱们得修改下求next 数组的代码。 //优化过后的next 数组求法 void GetNextval(char* p, int next[]) { int pLen strlen(p); next[0] -1; int k -1; int j 0; while (j pLen - 1) { //p[k]表示前缀p[j]表示后缀 if (k -1 || p[j] p[k]) { j; k; //较之前next数组求法改动在下面4行 if (p[j] ! p[k]) next[j] k; //之前只有这一行 else //因为不能出现p[j] p[ next[j ]]所以当出现时需要继续递归k next[k] next[next[k]] next[j] next[k]; } else { k next[k]; } } } 利用优化过后的next 数组求法可知模式串“abab”的新next数组为-1 0 -1 0读者可以在脑海里或纸上执行上述代码验证下如不会计算可看下文末的参考文献10。 可能有些读者会问原始next 数组是前缀后缀最长公共元素长度值右移一位 然后初值赋为-1而得那么优化后的next 数组如何快速心算出呢实际上只要求出了原始next 数组那么可根据原始next 数组快速求出优化后的next 数组。还是以abab为例如下表格所示 然后引用下之前3.1节的KMP代码 int KmpSearch(char* s, char* p) { int i 0; int j 0; int sLen strlen(s); int pLen strlen(p); while (i sLen j pLen) { //①如果j -1或者当前字符匹配成功即S[i] P[j]都令ij if (j -1 || s[i] p[j]) { i; j; } else { //②如果j ! -1且当前字符匹配失败即S[i] ! P[j]则令 i 不变j next[j] //next[j]即为j所对应的next值 j next[j]; } } if (j pLen) return i - j; else return -1; } 接下来咱们继续拿之前的例子说明整个匹配过程如下 1. S[3]与P[3]匹配失败。 2. S[3]保持不变P的下一个匹配位置是P[next[3]]而next[3]0所以P[next[3]]P[0]与S[3]匹配。 3. 由于上一步骤中P[0]与S[3]还是不匹配。此时i3jnext [0]-1由于满足条件j-1所以执行“i, j”即主串指针下移一个位置P[0]与S[4]开始匹配。最后jpLen跳出循环输出结果i - j 4即模式串第一次在文本串中出现的位置匹配成功算法结束。 3.4 KMP的时间复杂度分析 咱们先来回顾下KMP匹配算法的流程 “KMP的算法流程 假设现在文本串S匹配到 i 位置模式串P匹配到 j 位置 如果j -1或者当前字符匹配成功即S[i] P[j]都令ij继续匹配下一个字符如果j ! -1且当前字符匹配失败即S[i] ! P[j]则令 i 不变j next[j]。此举意味着失配时模式串P相对于文本串S向右移动了j - next [j] 位。” 我们发现如果某个字符匹配成功模式串首字符的位置保持不动仅仅是i、j如果匹配失配i 不变即 i 不回溯模式串会跳过匹配过的next [j]个字符。整个算法最坏的情况是当模式串首字符位于i - j的位置时才匹配成功算法结束。 所以如果文本串的长度为n模式串的长度为m那么匹配过程的时间复杂度为O(n)算上计算next的O(m)时间KMP的整体时间复杂度为O(m n)。 4. 扩展BM算法 KMP的匹配是从模式串的开头开始匹配的而1977年德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了一种新的字符串匹配算法Boyer-Moore算法该算法从模式串的尾部开始匹配且拥有在最坏情况下O(N)的时间复杂度。在实践中比KMP算法的实际效能高。 BM算法定义了两个规则
坏字符规则当文本串中的某个字符跟模式串的某个字符不匹配时我们称文本串中的这个失配字符为坏字符此时模式串需要向右移动移动的位数 坏字符在模式串中的位置 - 坏字符在模式串中最右出现的位置。此外如果坏字符不包含在模式串之中则最右出现位置为-1。好后缀规则当字符失配时后移位数 好后缀在模式串中的位置 - 好后缀在模式串上一次出现的位置且如果好后缀在模式串中没有再次出现则为-1。 下面举例说明BM算法。例如给定文本串“HERE IS A SIMPLE EXAMPLE”和模式串“EXAMPLE”现要查找模式串是否在文本串中如果存在返回模式串在文本串中的位置。 1. 首先文本串与模式串头部对齐从尾部开始比较。S与E不匹配。这时S就被称为坏字符bad character即不匹配的字符它对应着模式串的第6位。且S不包含在模式串EXAMPLE之中相当于最右出现位置是-1这意味着可以把模式串后移6-(-1)7位从而直接移到S的后一位。 2. 依然从尾部开始比较发现P与E不匹配所以P是坏字符。但是P包含在模式串EXAMPLE之中。因为“P”这个“坏字符”对应着模式串的第6位从0开始编号且在模式串中的最右出现位置为4所以将模式串后移6-42位两个P对齐。 3
. 依次比较得到 “MPLE”匹配称为好后缀good suffix即所有尾部匹配的字符串。注意MPLE、PLE、LE、E都是好后缀。 4
. 发现“I”与“A”不匹配“I”是坏字符。如果是根据坏字符规则此时模式串应该后移2-(-1)3位。问题是有没有更优的移法 5
. 更优的移法是利用好后缀规则当字符失配时后移位数 好后缀在模式串中的位置 - 好后缀在模式串中上一次出现的位置且如果好后缀在模式串中没有再次出现则为-1。 所有的“好后缀”MPLE、PLE、LE、E之中只有“E”在“EXAMPLE”的头部出现所以后移6-06位。 可以看出“坏字符规则”只能移3位“好后缀规则”可以移6位。每次后移这两个规则之中的较大值。这两个规则的移动位数只与模式串有关与原文本串无关。 6. 继续从尾部开始比较“P”与“E”不匹配因此“P”是“坏字符”根据“坏字符规则”后移 6 - 4 2位。因为是最后一位就失配尚未获得好后缀。 由上可知BM算法不仅效率高而且构思巧妙容易理解。完。 下面是hihocoder上的例子。 题目1 : KMP算法 时间限制: 1000ms 单点时限: 1000ms 内存限制: 256MB 描述 小Hi和小Ho是一对好朋友出生在信息化社会的他们对编程产生了莫大的兴趣他们约定好互相帮助在编程的学习道路上一同前进。 这一天他们遇到了一只河蟹于是河蟹就向小Hi和小Ho提出了那个经典的问题“小Hi和小Ho你们能不能够判断一段文字原串里面是不是存在那么一些……特殊……的文字模式串” 小Hi和小Ho仔细思考了一下觉得只能想到很简单的做法但是又觉得既然河蟹先生这么说了就肯定不会这么容易的让他们回答了于是他们只能说道“抱歉河蟹先生我们只能想到时间复杂度为文本长度 * 特殊文字总长度的方法即对于每个模式串分开判断然后依次枚举起始位置并检查是否能够匹配但是这不是您想要的方法是吧” 河蟹点了点头说道”看来你们的水平还有待提高这样吧如果我说只有一个特殊文字你能不能做到呢“ 小Ho这时候还有点晕晕乎乎的但是小Hi很快开口道”我知道这就是一个很经典的模式匹配问题可以使用KMP算法进行求解“ 河蟹满意的点了点头对小Hi说道”既然你知道就好办了你去把小Ho教会下周我有重要的任务交给你们“ ”保证完成任务”小Hi点头道。 输入 第一行一个整数N表示测试数据组数。 接下来的N*2行每两行表示一个测试数据。在每一个测试数据中第一行为模式串由不超过10^4个大写字母组成第二行为原串由不超过10^6个大写字母组成。 其中N20 输出 对于每一个测试数据按照它们在输入中出现的顺序输出一行Ans表示模式串在原串中出现的次数。 样例输入 5
HA
HAHAHA
WQN
WQN
ADA
ADADADA
BABABB
BABABABABABABABABB
DAD
ADDAADAADDAAADAAD 样例输出 3
1
3
1
0 实现代码 #includeiostream
#includestring
#includecstdlib
using namespace std;
void GetNext(string p, int *next) { int pLen p.size(); next[0] -1; int k -1; int j 0; while (j pLen-1) //因为j0已经初始化所以下面对于每个j求得是j1,所以终止条件是j pLen-1 { //p[k]表示前缀p[j]表示后缀 if (k -1 || p[j] p[k]) { j; k; if (p[j] ! p[k]) next[j] k; else next[j] next[k]; / /因为不能出现p[j] p[ next[j ]]所以当出现时需要继续递归k next[k] next[next[k]] } else { k next[k]; } } } int KmpSearch(string s, string p, int *next) { int i 0; int j 0; int sLen s.size(); int pLen p.size(); int ans 0; //成功匹配到的次数 GetNext(p,next); while (i sLen) { //①如果j -1或者当前字符匹配成功即S[i] P[j]都令ij if (j -1 || s[i] p[j]) { i; j; } else { //②如果j ! -1且当前字符匹配失败即S[i] ! P[j]则令 i 不变j next[j] //next[j]即为j所对应的next值 j next[j]; } if(jpLen) { ans ; i i-j1; //返回继续开始 j0; //这里由几种特殊情况的启示得到的 //第一种情形模式串为“ada”,文本串为“adadada” //第二种情形模式串为“aaa”,文本串为“abaaaaaaaaa” } } return ans; } int main(int argc,char **argv) { int n; //实验组数 string pattern; //模式串 string text; //文本串 cin n; while(n--) { pattern.clear(); //清除缓存 text.clear(); //清除缓存 cin pattern; cin text; int size pattern.size(); int *nextnew int[size]; int res KmpSearch(text,pattern,next); delete next; cout matched times: resendl; } system(pause); return 0; } 完