字符串匹配算法

实现代码链接:https://github.com/Rey123456/stringMatch

字符串匹配之暴力匹配:

如果用暴力匹配的思路,并假设现在文本串 S 匹配到 i 位置,模式串 P 匹配到 j 位置,则有:

  • 如果当前字符匹配成功(即 S[i] == P[j]),则 i++,j++,继续匹配下一个字符;
  • 如果失配(即 S[i]! = P[j]),令 i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。

字符串匹配之KMP算法:

kmp算法流程:

假设现在文本串 S 匹配到 i 位置,模式串 P 匹配到 j 位置

  • 如果 j = -1,或者当前字符匹配成功(即 S[i] == P[j]),都令 i++,j++,继续匹配下一个字符;
  • 如果 j != -1,且当前字符匹配失败(即 S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串 P 相对于文本串 S 向右移动了 j - next [j] 位。

     kmp算法的关键在理解next[]数组在算法中的含义,next值代表当前字符之前的字符串中,有多长的相同前缀后缀。next数组相当于最大长度值整体右移一位,初始值设为-1。

 

如何求解next数组:通过代码递推计算

对于 P 的前 j+1 个序列字符:

若p[k] == p[j],则 next[j + 1 ] = next [j] + 1 = k + 1;

若p[k ] ≠ p[j],如果此时 p[ next[k] ] == p[j ],则 next[ j + 1 ] = next[k] + 1,否则继续递归前缀索引 k = next[k],而后重复此过程。 

如下图所示,假定给定模式串 ABCDABCE,且已知 k=next [j]=2,现要求 next [j + 1] 等于多少?因为 pk = pj = C,所以 next[j + 1] = next[j] + 1 = k + 1=3。代表字符 E 前的模式串中,有长度为 3 的相同前缀后缀。

 

但如果 pk != pj 呢?说明“p0 pk-1 pk” ≠ “pj-k pj-1 pj”。换言之,当 pk != pj 后,字符 E 前有多大长度的相同前缀后缀呢?很明显,因为 C 不同于 D,所以 ABC 跟 ABD 不相同,即字符 E 前的模式串没有长度为 k+1 的相同前缀后缀,也就不能再简单的令: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。

下面,我们来基于 next 数组进行匹配。

 

给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,现在要拿模式串去跟文本串匹配,如下图所示:

 

P[1] 跟 S[4] 匹配成功,P[2] 跟 S[5] 也匹配成功, ...,直到当匹配到 P[6] 处的字符 D 时失配(即 S[10] != P[6]),由于 P[6] 处的 D 对应的 next 值为 2,所以下一步用 P[2] 处的字符 C 继续跟 S[10] 匹配,相当于向右移动:j - next[j] = 6 - 2 =4 位。

 

向右移动 4 位后,P[2] 处的 C 再次失配,由于 C 对应的 next 值为 0,所以下一步用 P[0] 处的字符继续跟 S[10] 匹配,相当于向右移动:j - next[j] = 2 - 0 = 2 位。

 

移动两位之后,A 跟空格不匹配,模式串后移 1 位。

 

P[6] 处的 D 再次失配,因为 P[6] 对应的 next 值为 2,故下一步用 P[2] 继续跟文本串匹配,相当于模式串向右移动 j - next[j] = 6 - 2 = 4 位。

 

匹配成功,过程结束。

 

Next 数组的优化

如果用之前的 next 数组方法求模式串“abab”的 next 数组,可得其 next 数组为-1 0 0 1(0 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] 已经跟 s[i] 失配,然后你还用跟 p[j] 等同的值 p[next[j]] 去跟 s[i] 匹配,很显然,必然失配),所以不能允许 p[j] = p[ next[j ]]。如果出现了 p[j] = p[ next[j] ] 咋办呢?如果出现了,则需要再次递归,即令 next[j] = next[ next[j] ]。

字符串匹配之Boyer-Moore算法:

Boyer-Moore 算法,简称 BM 算法。该算法从模式串的尾部开始匹配,且拥有在最坏情况下 O(N) 的时间复杂度。在实践中,比 KMP 算法的实际效能高。

BM 算法定义了两个规则:

  • 坏字符规则:当文本串中的某个字符跟模式串的某个字符不匹配时,我们称文本串中的这个失配字符为坏字符,此时模式串需要向右移动,移动的位数 = 坏字符在模式串中的位置 - 坏字符在模式串中最右出现的位置。此外,如果"坏字符"不包含在模式串之中,则最右出现位置为 -1。
  • 好后缀规则:当字符失配时,后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串上一次出现的位置,且如果好后缀在模式串中没有再次出现,则为 -1。

下面举例说明 BM 算法。

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-4=2 位,两个"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-0=6 位。

可以看出,“坏字符规则”只能移3位,“好后缀规则”可以移 6 位。每次后移这两个规则之中的较大值。这两个规则的移动位数,只与模式串有关,与原文本串无关。

 

6.继续从尾部开始比较,“P”与“E”不匹配,因此“P”是“坏字符”,根据“坏字符规则”,后移 6 - 4 = 2 位。因为是最后一位就失配,尚未获得好后缀。

 

由上可知,BM 算法不仅效率高,而且构思巧妙,容易理解。

字符串匹配之Sunday 算法

它的思想跟 BM 算法很相似:

  • 只不过 Sunday 算法是从前往后匹配,在匹配失败时关注的是文本串中参加匹配的最末位字符的下一位字符。
    • 如果该字符没有在模式串中出现则直接跳过,即移动位数 = 匹配串长度 + 1;
    • 否则,其移动位数 = 模式串中最右端的该字符到末尾的距离 +1。

下面举个例子说明下 Sunday 算法。假定现在要在文本串"substring searching algorithm"中查找模式串"search"。

1.刚开始时,把模式串与文本串左边对齐:

 

2.结果发现在第 2 个字符处发现不匹配,不匹配时关注文本串中参加匹配的最末位字符的下一位字符,即标粗的字符 i,因为模式串 search 中并不存在 i,所以模式串直接跳过一大片,向右移动位数 = 匹配串长度 + 1 = 6 + 1 = 7,从 i 之后的那个字符(即字符 n)开始下一步的匹配,如下图:

 

3.结果第一个字符就不匹配,再看文本串中参加匹配的最末位字符的下一位字符,是'r',它出现在模式串中的倒数第3位,于是把模式串向右移动 3 位(r 到模式串末尾的距离 + 1 = 2 + 1 =3),使两个'r'对齐,如下:

 

4.匹配成功。

回顾整个过程,我们只移动了两次模式串就找到了匹配位置,缘于 Sunday 算法每一步的移动量都比较大,效率很高。

参考:

http://wiki.jikexueyuan.com/project/kmp-algorithm/define.html

http://blog.csdn.net/likebamboo/article/details/12778701

http://www.ruanyifeng.com/blog/2013/05/boyer-moore_string_search_algorithm.html

 

posted on 2016-07-26 15:07  羽溪夜  阅读(1329)  评论(0编辑  收藏  举报

导航