字符串匹配算法——KMP、BM、Sunday
KMP算法
KMP算法主要包括两个过程,一个是针对子串生成相应的“索引表”,用来保存部分匹配值,第二个步骤是子串匹配。
部分匹配值是指字符串的“前缀”和“后缀”的最长的共有元素的长度。以“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},共有元素长度为1;
- “ABCDAB”的前缀为{A, AB, ABC, ABCD, ABCDA},后缀为{BCDAB, CDAB, DAB, AB, B},共有元素长度为2;
- “ABCDABD”的前缀为{A, AB, ABC, ABCD, ABCDA, ABCDAB},后缀为{BCDABD, CDABD, DABD, ABD, BD, D},共有元素长度为0;
所以,生成了如下的部分匹配值表:
搜索词 | A | B | C | D | A | B | D |
部分匹配值 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
生成部分匹配值表的意义在于匹配时,父串的搜索位置可以向后跳动,而不是每次都只向后移动一位。可以用下面的公式计算向后跳动的位数:
移动位数=已匹配的字符数-对应的部分匹配值
以上是KMP的主要思想,下面是KMP的Java实现:
//生成next数组,表示部分匹配值 public static int[] next (String sub) { int[] a = new int [sub.length()]; char[] c = sub.toCharArray(); int i = 0, j = 1; a[0] = -1; for(; j<sub.length(); j++) { i = a[j-1]; while (i>=0 && c[j]!=c[i+1]) { i = a[i]; } if (c[j] == c[i+1]) { a[j] = i+1; } else { a[j] = -1; } } return a; } //匹配过程 public static void pattern (String str, String sub, int[] next) { char[] ch1 = str.toCharArray(); char[] ch2 = sub.toCharArray(); int i = 0, j = 0; while (i<ch1.length) { if (ch1[i] == ch2[j]) { i ++; j ++; } else if (j==0) { i ++; } else { j = next[j-1] + 1; } } }
BM算法
BM算法的基本流程: 设文本串T,模式串为P。首先将T与P进行左对齐,然后进行从右向左比较 ,若是某趟比较不匹配时,BM算法就采用坏字符规则和好后缀规则来计算模式串向右移动的距离,直到整个匹配过程的结束。
首先介绍坏字符规则和好后缀规则。
坏字符规则:
在从右向左扫描的过程中,若发现某个字符x不匹配,则按下面两种情况讨论:
- 如果字符x在模式P中没有出现,那么从字符x开始的m个文本显然不可能与P匹配成功,直接跳过该区域即可
- 如果x在模式P中出现,则以该字符进行对齐。
可以用下面公式表示:(skip(x)是P右移的距离,m为模式串P的长度,max(x)为字符x在P中最有位置)
skip(x) = m; x在P中未出现
= m-max(x); x在P中出现
好后缀规则:
若发现某个字符串不匹配的同时,已有部分字符匹配成功,同样分情况讨论:
- 模式串中有子串匹配上好后缀,此时移动模式串,让该子串和好后缀对齐即可,如果超过一个子串匹配上好后缀,则选择最靠靠近好后缀的子串对齐
- 模式串中没有子串匹配上后后缀,此时需要寻找模式串的一个最长前缀,并让该前缀等于好后缀的后缀,寻找到该前缀后,让该前缀和好后缀对齐即可
- 模式串中没有子串匹配上后后缀,并且在模式串中找不到最长前缀,让该前缀等于好后缀的后缀。此时,直接移动模式到好后缀的下一个字符。
用数学公式表示:
shift(j) = min{s|(P[j+1..m]=P[j-s+1..m-s]) && (P[j]!=P[j-s])(j>s), P[s+1..m]=P[1..m](j<=s)}
在BM算法匹配过程中,取skip和shift中较大者作为跳跃的距离。
下面是BM算法的C实现:
//根据坏字符规则做预处理,建立坏字符表 int* MakeSkip (char* ptrn, int plen) { int i; //申请256个int的空间,一个字符8位,总共有256中不同的字符 int *skip = (int*) malloc(256 * sizeof(int)); assert (skip != NULL); //初始化坏字符表 for (i=0; i<256; i++) { *(skip + i) = plen; } //给表中需要赋值的单元赋值 while (plen != 0) { *(skip + (unsigned char)*ptrn++) = plen--; } return skip; } //根据好后缀规则做预处理,建立好后缀表 int *MakeShift (char* ptrn, int plen) { //为好后缀表申请空间 int *shift = (int*) malloc(plen * sizeof(int)); //给好后缀表进行赋值的指针 int *sptr = shift + plen - 1; //记录字符串边界的指针 int *pptr = ptrn + plen - 1; assert (shift != NULL); char c = *pptr; *sptr = 1; while (sptr-- != shift) { char *p1 = ptrn + plen -2, *p2, *p3; do { while (p1 >= ptrn && *p1-- != c); p2 = ptrn + plen -2; p3 = p1; while (p3 = ptrn && *p3-- == *p2-- && p2 >= pptr); } while (p3 >= ptrn && p2 >= pptr); *sptr = shift _ plen - sptr + p2 - p3; pptr--; } return shift; } int BMSearch ( char* buf, int blen, char* ptrn, int plen, int* skip, int* shift) { int b_idx = plen; if (plen == 0) return 1; //计算字符串是否匹配到了尽头 while (b_idx <= blen) { int p_idx = plen, skip_stride, shift_stride; while (buf[--b_idx] == ptrn[--p_idx]) { if (b_idx < 0) { return 0; } if (p_idx == 0) { return 1; } } skip_stride = skip[(unsigned char)buf[b_idx]]; shift_stride = shift[p_idx]; b_idx += (skip_stride > shift_stride) ? skip_stride : shift_stride; } return 0; }
Sunday算法
从头开始匹配,当发现失配的时候就判断子串的后一位在父串的字符是否在子串中存在。如果存在则将该位置和子串中的该字符对齐,在从头开始匹配。如果不存在就将子串向后移动,和父串k+1处的字符对齐,再进行匹配。重复上面的操作直到找到,或父串被找完。
下面是Sunday算法的C实现:
int SundayMatch(byte* pSrc, int nSrcSize, byte* pSubSrc, int nSubSrcSize) 2 { 3 int skip[256]; 4 for (int i = 0; i < 256; i++) 5 { 6 skip[i] = nSubSrcSize + 1; 7 } 8 9 for (int i = 0; i < nSubSrcSize; i++) 10 { 11 skip[pSubSrc[i]] = nSubSrcSize - i; 12 } 13 14 int nPos = 0; 15 while(nPos <= nSrcSize - nSubSrcSize) 16 { 17 int j = nSubSrcSize - 1; 18 while(j >= 0 && pSrc[nPos + j] == pSubSrc[j]) 19 { 20 j--; 21 } 22 if (j < 0) 23 { 24 break; 25 } 26 nPos = nPos + skip[pSrc[nPos + nSubSrcSize]]; 27 } 28 return nPos; 29 }