KMP 匹配算法

 

在开发中,经常会遇到在一个字节数组中,查找一个子数组的问题。如果不是字节数组,而是字符串的话,直接通过 string.IndexOf 就可以解决,对于字节数组还是需要做一点功课。

 

因为字符串比较容易观察,所以,我们首先通过字符串来分析,然后,再在字节数组上实现。

 

问题:

对于一个源字符串 source = "abababaababacb" 来说,查找其中包含子串 pattern = "ababacb" 出现的位置下标。

 

首先,我们通过最基本的方法来进行查找。

i 表示当前用来匹配的 source 中字符的下标,j 表示当前用来匹配的模板的下标。

i

0

1

2

3

4

5

6

7

8

9

10

11

12

13

source

a

b

a

b

a

b

a

a

b

a

b

a

c

b

j

0

1

2

3

4

5

 

 

 

 

 

 

 

 

pattern

a

b

a

b

a

c

 

 

 

 

 

 

 

 

 

当 j = 5 的时候,我们可以发现,source[ 5 ] != pattern[ 5 ] , 问题是下一次使用 pattern 的哪个位置来重新开始匹配?

最简单的处理就是让 j = 0 , i 增加一个位置 i = 1,  然后,重新开始,下一次,i = 2,再重新开始,直到完成。

 

i

0

1

2

3

4

5

6

7

8

9

10

11

12

13

source

a

b

a

b

a

b

a

a

b

a

b

a

c

b

j

 

0

 

 

 

 

 

 

 

 

 

 

 

 

pattern

 

a

 

 

 

 

 

 

 

 

 

 

 

 

 

但是,我们对于我们的 pattern 来说,比较容易看到,pattern 下标为 2,3,4 的三个字符正好与 pattern 开始的三个字符相同,都是 "aba"

 

0

1

2

3

4

5

6

7

8

pattern

a

b

a

b

a

c

b

 

 

 

 

 

a

b

a

b

a

c

b

 

 

 

0

1

2

3

4

5

6

 

因此,并不需要让 j=0, 可以直接让 j=3, 从 3 开始比较,而 i 不变,这样可以减少 3 次以上的比较。

i

0

1

2

3

4

5

6

7

8

9

10

11

12

13

source

a

b

a

b

a

b

a

a

b

a

b

a

c

b

j

 

 

0

1

2

3

 

 

 

 

 

 

 

 

pattern

 

 

a

b

a

b

 

 

 

 

 

 

 

 

 

继续进行比较,直到 i=7,  j= 5 的时候,又不匹配了。

i

0

1

2

3

4

5

6

7

8

9

10

11

12

13

source

a

b

a

b

a

b

a

a

b

a

b

a

c

b

j

 

 

0

1

2

3

4

5

 

 

 

 

 

 

pattern

 

 

a

b

a

b

a

c

 

 

 

 

 

 

 

根据上一次的经验,我们又可以让 j =3, i 不变,然后进行匹配。但是还是不能匹配。

i

0

1

2

3

4

5

6

7

8

9

10

11

12

13

source

a

b

a

b

a

b

a

a

b

a

b

a

c

B

j

 

 

 

 

0

1

2

3

 

 

 

 

 

 

pattern

 

 

 

 

a

b

a

b

 

 

 

 

 

 

 

由于,我们还可以再调整一次 j = 1, 然而,还是不能匹配。

i

0

1

2

3

4

5

6

7

8

9

10

11

12

13

source

a

b

a

b

a

b

a

a

b

a

b

a

c

B

j

 

 

 

 

 

 

0

1

 

 

 

 

 

 

pattern

 

 

 

 

 

 

a

b

 

 

 

 

 

 

 

没有办法,只能让 j = 0 再次从头开始了。这次完全匹配。

I

0

1

2

3

4

5

6

7

8

9

10

11

12

13

Source

a

b

a

b

a

b

a

a

b

a

b

a

c

b

J

 

 

 

 

 

 

 

0

1

2

3

4

5

6

 

 

 

 

 

 

 

 

a

b

a

b

a

c

b

 

通过上面的分析,我们可以看到,由于模式串 pattern 内在的规律性,使得我们不必在每次匹配失败的时候,回溯到起始位置重新开始,而是可以利用已经匹配的部分来减少匹配的次数。如果我们提前通过预处理,构建出一个回溯的函数,就可以通过这个函数获取回溯的位置。

 

对于 pattern = "ababacb" 来说,我们可以构建一个回溯函数表 next ,列出在匹配失败的时候,应该回溯的位置。

对于 next[0] 来说,显然为 0.

对于 next[1] 来说,也只能从 0 再开始匹配。

以后的匹配值,需要参考上一次的匹配,如果继续匹配了,那么这个值应该增加 1,否则,应为最大匹配值。

对于  pattern = "ababacb"  的匹配函数如下:

pattern

a

b

a

b

a

c

b

j

0

1

2

3

4

5

6

next

0

0

 

但是,在这个匹配函数,对于 j 为 0,1,2 的情况来说,0, 1 是没有匹配,2 实际上匹配了第一次,这样的话,对于不匹配或者第一次匹配,next 函数都是 0, 不方便计算。

我们可以将 next 函数定义为当前是第几个匹配,这样的话,如果匹配了,就是大于 0 , 如果没有匹配就是 0 ,比较方便计算。

在进行匹配的过程中,匹配失败则取得上一个 next 函数的值,就可以取得当前位置之前的匹配。

pattern

a

b

a

b

a

c

b

j

0

1

2

3

4

5

6

next

0

0

 0

0

 

下面是我自己写的求next[]值的函数

void get_next( char * str )
{
     int i, j, k, l, len, flag;
     char ch1[1000], ch2[1000];
     len = strlen( str );
     for ( i = 0; i < len; i ++ )
     {
          flag = 0;
          for ( j = i - 1; j > 0; j -- )
          {
               for ( l = 0, k = 0; k < j; k ++ )
               {
                    ch1[l ++] = str[l];
               }
               ch1[l] = '\0';
               for ( l = 0, k = i - k; k < i; k ++ )
               {
                    ch2[l ++] = str[k];
               }
               ch2[l] = '\0';
               if ( strcmp( ch1 , ch2 ) == 0 )
               {
                    flag = 1;
                    next[i] = j;
                    break;
               }
          }
          if ( flag = 0 )

        {
               next[i] = 0;

        }
     }
}

 

后来在网上搜了一下,发现自己的代码好繁琐

方法1:

 1 public int[] BuildKMP( byte[] pattern )
 2 {
     int[] next = new int[pattern.Length];
 4 
 5     next[0] = 0;        // 第一个位置一定为 0
 
     int j = 0;          // 匹配的起始位置
     for (int i = 1; i < pattern.Length; i++)
 9     {
10         // 如果已经匹配上,但是现在不能匹配,回溯寻找
11         while( j>0 && pattern[j] != pattern[i] )
12         {
13             j = next[j-1];
14         }
15
16         // 如果能够匹配上,向下推进一个位置
17         // 注意 i 在 for 循环中自动推进
18         if (pattern[j] == pattern[i])
19             j++;
20 
21         // 保存
22         next[i] = j;
23     }
24     return next;
25 }

方法2:该方法最为简洁

 假设 next= k, 则有 S[0, k-1]== S[i-k, i-1], 如何求 next[i+1]?
1). 如果 S[k]== S[i+1], 则有 S[0,k]== S[i-k, i],  可以肯定,不可能存在 x> k, 使得 S[0, x]== S[i-x, i] 故 next[i+1]= next+ 1= k+ 1;
2). 如果 S[k]!= S[i+1], 可看成是一个模式匹配问题,相当于 S[len(S)-K, len(S)]为主串, S[0, len(S)]  为模式串的模式匹配,这时应将模式串向右滑动 next       [k] 个字符, 再比较 S[ next[k] ]与 S[i+1], 如果相等, 则 next[i+1]= next[ next[k] ]+ 1, 如此继续。

  • void get_next(char * str)
  • {
  •       next[0]= 0;
  •       int i= 0, j= 0;
  •     int len = strlen(str);
  •       while ( i < len )
  •    {
  •             if ( j == 0 || str[i] == str[j] )
  •        {
  •                   i ++;
  •           j ++;
  •           next[i] = j;
  •             }
  •             else
  •        {
  •           j = next[j];
  •        }
  •       }
  • }
  •  

     

    posted on 2010-11-24 12:37  00Prince || Lai00  阅读(163)  评论(0)    收藏  举报