字符串匹配算法BF,BM,KMP

字符串匹配bf算法:(暴力穷举算法)

在一个字符串中寻找另一字符串,最容易想到的,也是最简单的办法是:取主串和模式串/搜索串中的每一位依次比较,如果匹配则同时后移一位继续比较,直至匹配到模式串的最后一位;如果出现不匹配的字符,则模式串向后移动一位,继续比较。这种解决问题的思路简单暴力,也是这个算法被叫做BF(Brute Force)的原因。整个匹配的过程可以参考下图,我们假设主串为“abdea”,搜索串为“dea”:

    字符串匹配算法BF,BM,KMP_第1张图片

 字符串匹配算法BF,BM,KMP_第2张图片

字符串匹配算法BF,BM,KMP_第3张图片这个算法的复杂度还是比较好分析的,我们假设主串的长度是 m,模式串的长度是 n,在最好的情况下,在第一个字符处的匹配就能够成功,例如主串是 a b c d ,模式串是a b c,这时只遍历了模式串的长度,因为时间复杂度是 O(n);

 在最坏的情况下,每次都需要遍历整个模式串,但是又未能匹配成功,例如主串是 a a a a a ...a,模式串是 a a a a b,所以需要遍历 m - n + 1 次,时间复杂度是 O(m * n) 。

Java的indexOf 方法解析

在 Java 语言中,常用的字符串匹配的方法是 String 类中的 indexOf() 方法,它的设计思路又是怎么样的呢?其实 indexOf 方法的逻辑非常简单,看看源代码就知道它其实就是 BF 算法的实现。    

字符串匹配算法BF,BM,KMP_第4张图片

主要的匹配逻辑在代码中的 for 循环这一段,可以看到匹配的过程主要分为了两步,第一步是找出第一个匹配的字符,

字符串匹配算法BF,BM,KMP_第5张图片

 

然后再依次遍历模式串看是否匹配。这和 BF 算法的思路是一致的。

 

BM算法:

各种文本编辑器的"查找"功能(Ctrl+F),大多采用Boyer[ˈbɔɪə]-Moore[mʊə]算法。

Boyer-Moore算法不仅效率高,而且构思巧妙,容易理解。1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了这种算法。BM算法里的总体思路是:对于每一次失败的匹配尝试,算法都能够使用这些信息来排除尽可能多的无法匹配的位置。

下面根据Moore教授自己的例子来解释这种算法。

1.

假定字符串为"HERE IS A SIMPLE EXAMPLE",搜索词为"EXAMPLE"。这里的EXAMPLE就是模式串。

字符串匹配算法BF,BM,KMP_第6张图片

首先,"字符串"与"搜索词"头部对齐,从尾部开始比较。

这是一个很聪明的想法,因为如果尾部字符不匹配,那么只要一次比较,就可以知道前7个字符(整体上)肯定不是要找的结果。

我们看到,"S"与"E"不匹配。这时,"S"就被称为"坏字符"(bad character),即不匹配的字符。我们还发现,"S"不包含在搜索词"EXAMPLE"之中,这意味着可以把搜索词直接移到"S"的后一位。

字符串匹配算法BF,BM,KMP_第7张图片

依然从尾部开始比较,发现"P"与"E"不匹配,所以"P"是"坏字符"。但是,"P"包含在搜索词"EXAMPLE"之中。所以,将搜索词后移两位,两个"P"对齐。

这里移动两位的依据:

后移位数 = 坏字符的位置 – 模式串中的上一次出现位置,如果"坏字符"不包含在搜索词之中,则上一次出现位置为 -1。也就是P这次在上面的主字符串是第六个位置,在模式串中是第四个位置 ,所以6-4 等于2 ,所以移动两位,通过这个也可以说明第一次的S和E不行等那就是6(S这个坏字符在字符串中的位置是6,以0下表开始计算)减去-1(S在模式串中不存在就是负1)(再以前面第二步的"S"为例,它出现在第6位,上一次出现位置是 -1(即未出现),则整个模式串后移 6 - (-1) = 7)位。

以"P"为例,它作为"坏字符",出现在主串的第6位(从0开始编号),也就是“空格,A,空格,S,I,M,P”,在模式串中的上一次出现位置为4,所以后移 6 - 4 = 2位。

如下图所示:

 

字符串匹配算法BF,BM,KMP_第8张图片

接着移动到这里:

字符串匹配算法BF,BM,KMP_第9张图片

依然从尾部开始比较,"E"与"E"匹配。 

字符串匹配算法BF,BM,KMP_第10张图片

 比较前面一位,"LE"与"LE"匹配。

字符串匹配算法BF,BM,KMP_第11张图片

 比较前面一位,"PLE"与"PLE"匹配。

字符串匹配算法BF,BM,KMP_第12张图片

 比较前面一位,"MPLE"与"MPLE"匹配。我们把这种情况称为"好后缀"(good suffix),即所有尾部匹配的字符串。注意,"MPLE"、"PLE"、"LE"、"E"都是好后缀。

 字符串匹配算法BF,BM,KMP_第13张图片

比较前一位,发现"I"与"A"不匹配。所以,"I"是"坏字符"。 

 字符串匹配算法BF,BM,KMP_第14张图片

 根据"坏字符规则",此时模式串应该后移 2 - (-1)= 3 位。问题是,此时有没有更好的移法?

我们知道,此时存在"好后缀"。所以,可以采用"好后缀规则":

后移位数 = 好后缀的位置 – 模式串中的上一次出现位置 

举例来说,如果字符串"ABCDAB"的后一个"AB"是"好后缀"。那么它的位置是5(从0开始计算,取最后的"B"的值),在模式串中的上一次出现位置是1(第一个"B"的位置),所以后移 5 - 1 = 4位,前一个"AB"移到后一个"AB"的位置。

再举一个例子,如果字符串"ABCDEF"的"EF"是好后缀,则"EF"的位置是5 ,上一次出现的位置是 -1(即未出现),所以后移 5 - (-1) = 6位,即整个模式串移到"F"的后一位。

 (1)"好后缀"的位置以最后一个字符为准。假定"ABCDEF"的"EF"是好后缀,则它的位置以"F"为准,即5(从0开始计算)。

  (2)如果"好后缀"在模式串中只出现一次,则它的上一次出现位置为 -1。比如,"EF"在"ABCDEF"之中只出现一次,则它的上一次出现位置为-1(即未出现)。

  (3)如果"好后缀"有多个,如果最长的那个"好后缀"在模式串中只出现一次,其他"好后缀"的上一次出现位置必须在头部。比如我们的例子中所有的"好后缀"(MPLE、PLE、LE、E)之中,只有"E"在"EXAMPLE"还出现在头部,所以后移 6 - 0 = 6位。

字符串匹配算法BF,BM,KMP_第15张图片

 可以看到,"坏字符规则"只能移3位,"好后缀规则"可以移6位。所以,Boyer-Moore算法的基本思想是,每次后移这两个规则之中的较大值。

更巧妙的是,这两个规则的移动位数,只与模式串有关,与原字符串无关。因此,可以预先计算生成《坏字符规则表》和《好后缀规则表》。使用时,只要查表比较一下就可以了。

继续从尾部开始比较,"P"与"E"不匹配,因此"P"是"坏字符"。根据"坏字符规则",后移 6 - 4 = 2位。

字符串匹配算法BF,BM,KMP_第16张图片

从尾部开始逐位比较,发现全部匹配,于是搜索结束。如果还要继续查找(即找出全部匹配),则根据"好后缀规则",后移 6 - 0 = 6位,即头部的"E"移到尾部的"E"的位置。 这个也就是平时文本编辑器ctrl+f移动搜索的原理

最坏情况下找到模式所有出现的时间复杂度为O(mn),在最好情况下执行匹配找到模式所有出现的时间复杂度为O(m/n)。

所以,BM算法里的总体思路就是:对于每一次失败的匹配尝试,算法都能够使用这些信息来排除尽可能多的无法匹配的位置。同时可以预先计算生成《坏字符规则表》和《好后缀规则表》,大大加快查找匹配的速度。 

字符串KMP算法:

KMP算法是三位学者在 Brute-Force算法的基础上同时提出的模式匹配的改进算法。它也是字符串匹配中的经典算法。它以三个发明者命名。

举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"?。

首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。 

 因为B与A不匹配,搜索词再往后移。

 就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。

接着比较字符串和搜索词的下一个字符,还是相同。

 直到字符串有一个字符,与搜索词对应的字符不相同为止。

 这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。

一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。

怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。 

已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数: 

已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:

  移动位数 = 已匹配的字符数 - 对应的部分匹配值

因为 6 - 2 等于4,所以将搜索词向后移动4位。

 

因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。

 因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。

 因为空格与A不匹配,继续后移一位。

 

逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。

逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。 

接着介绍部分匹配表的概念:

字符串匹配算法BF,BM,KMP_第17张图片

 前缀:除了最后一个字符以外,剩下所有字符组合 br bre  brea

后缀:除了第一个字符意外,剩下所有字符,read,ead,ad,d

首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

 

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,

  - "A"的前缀和后缀都为空集,共有元素的长度为0;

"AB"的前缀为[A],后缀为[B],共有元素的长度为0;

  - "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

  - "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

  - "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;

  - "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;

"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。

KMP算法的时间复杂度O(m+n)。

BM算法和KMP算法两者比较来说,BM算法的执行效率要比KMP算法快3-5倍左右,并且十分容易理解。所以各种文本编辑器的“查找”功能(CTRL + F)一般都是采用的此算法。

 

你可能感兴趣的:(p2p,网络协议,网络)