0%

KMP字符串模式匹配算法Java实现

本文灵感来自于July的博客从头到尾彻底理解KMP,并着重于Java实现 :)。 现有字符串匹配算法有不少,如简单暴力的朴素算法(暴力匹配算法)、KMP算法、BM算法以及Sunday算法等,在这里仅介绍前两种算法。

1. 朴素算法

朴素算法即暴力匹配算法,对于长度为n的文本串S和长度为m模式串P,在文本串S中是否存在一个有效偏移i,其中 0≤ i < n - m + 1,使得 S[i… i+m - 1] = P[0 … m-1](注:下标从0开始),如果存在则匹配成功,否则匹配失败。由于在匹配过程中一旦不匹配,就要让模式串P相对于文本串S右移1,即i需要进行回溯,其时间复杂度为O(n*m)。
Java实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
   // 定义接口
interface StringMatcher {
/**
* 从原字符串中查找模式字符串的位置,如果模式字符串存在,则返回模式字符串第一次出现的位置,否则返回-1
*
* @param source
* 原字符串
* @param pattern
* 模式字符串
* @return if substring exists, return the first occurrence of pattern
* substring, return -1 if not.
*/
int indexOf(String source, String pattern);
}
/**
* 暴力匹配
* <p>
* 时间复杂度: O(m*n), m = pattern.length, n = source.length
*/
class ViolentStringMatcher implements StringMatcher {

@Override
public int indexOf(String source, String pattern) {
int i = 0, j = 0;
int sLen = source.length(), pLen = pattern.length();
char[] src = source.toCharArray();
char[] ptn = pattern.toCharArray();
while (i < sLen && j < pLen) {
if (src[i] == ptn[j]) {
// 如果当前字符匹配成功,则将两者各自增1,继续比较后面的字符
i++;
j++;
} else {
// 如果当前字符匹配不成功,则i回溯到此次匹配最开始的位置+1处,也就是i = i - j + 1
// (因为i,j是同步增长的), j = 0;
i = i - j + 1;
j = 0;
}
}
// 匹配成功,则返回模式字符串在原字符串中首次出现的位置;否则返回-1
if (j == pLen)
return i - j;
else
return -1;
}
}

2. KMP算法

与朴素算法不同,朴素算法是当遇到不匹配字符时,向后移动一位继续匹配,而KMP算法是当遇到不匹配字符时,不是简单的向后移一位字符,而是根据前面已匹配的字符数和模式串前缀和后缀的最大相同字符串长度数组next的元素来确定向后移动的位数,所以KMP算法的时间复杂度比朴素算法的要少,并且是线性时间复杂度,即预处理时间复杂度是O(m),匹配时间复杂度是O(n)。

next数组含义:代表在模式串P中,当前下标对应的字符之前的字符串中,有多大长度的相同前缀后缀。例如如果next [j] = k,代表在模式串P中,下标为j的字符之前的字符串中有最大长度为k 的相同前缀后缀。

KMP算法的核心就是求next数组,在字符串匹配的过程中,一旦某个字符匹配不成功,next数组就会指导模式串P到底该相对于S右移多少位再进行下一次匹配,从而避免无效的匹配。

next数组求解方法:

  • next[0] = -1。
  • 如果已知next[j] = k,如何求出next[j+1]呢?具体算法如下:
    1. 如果p[j] = p[k], 则next[j+1] = next[k] + 1;
    2. 如果p[j] != p[k], 则令k=next[k],如果此时p[j]==p[k],则next[j+1]=k+1,如果不相等,则继续递归前缀索引,令 k=next[k],继续判断,直至k=-1(即k=next[0])或者p[j]=p[k]为止

详细的介绍及分析还请移步从头到尾彻底理解KMP,在下语拙 :(
Java实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
   /**
* KMP模式匹配
* @author Tianma
*
*/
class KMPStringMatcher implements StringMatcher {

/**
* 获取KMP算法中pattern字符串对应的next数组
*
* @param p
* 模式字符串对应的字符数组
* @return
*/
protected int[] getNext(char[] p) {
// 已知next[j] = k,利用递归的思想求出next[j+1]的值
// 如果已知next[j] = k,如何求出next[j+1]呢?具体算法如下:
// 1. 如果p[j] = p[k], 则next[j+1] = next[k] + 1;
// 2. 如果p[j] != p[k], 则令k=next[k],如果此时p[j]==p[k],则next[j+1]=k+1,
// 如果不相等,则继续递归前缀索引,令 k=next[k],继续判断,直至k=-1(即k=next[0])或者p[j]=p[k]为止
int pLen = p.length;
int[] next = new int[pLen];
int k = -1;
int j = 0;
next[0] = -1; // next数组中next[0]为-1
while (j < pLen - 1) {
if (k == -1 || p[j] == p[k]) {
k++;
j++;
next[j] = k;
} else {
k = next[k];
}
}
return next;
}

@Override
public int indexOf(String source, String pattern) {
int i = 0, j = 0;
char[] src = source.toCharArray();
char[] ptn = pattern.toCharArray();
int sLen = src.length;
int pLen = ptn.length;
int[] next = getNext(ptn);
while (i < sLen && j < pLen) {
// 如果j = -1,或者当前字符匹配成功(src[i] = ptn[j]),都让i++,j++
if (j == -1 || src[i] == ptn[j]) {
i++;
j++;
} else {
// 如果j!=-1且当前字符匹配失败,则令i不变,j=next[j],即让pattern模式串右移j-next[j]个单位
j = next[j];
}
}
if (j == pLen)
return i - j;
return -1;
}
}

3. 优化的KMP算法(改进next数组)

具体过程移步从头到尾彻底理解KMP3.3.8 Next 数组的优化
在这里给出Java实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
   /**
* 优化的KMP算法(对next数组的获取进行优化)
*
* @author Tianma
*
*/
class OptimizedKMPStringMatcher extends KMPStringMatcher {

@Override
protected int[] getNext(char[] p) {
// 已知next[j] = k,利用递归的思想求出next[j+1]的值
// 如果已知next[j] = k,如何求出next[j+1]呢?具体算法如下:
// 1. 如果p[j] = p[k], 则next[j+1] = next[k] + 1;
// 2. 如果p[j] != p[k], 则令k=next[k],如果此时p[j]==p[k],则next[j+1]=k+1,
// 如果不相等,则继续递归前缀索引,令 k=next[k],继续判断,直至k=-1(即k=next[0])或者p[j]=p[k]为止
int pLen = p.length;
int[] next = new int[pLen];
int k = -1;
int j = 0;
next[0] = -1; // next数组中next[0]为-1
while (j < pLen - 1) {
if (k == -1 || p[j] == p[k]) {
k++;
j++;
// 修改next数组求法
if (p[j] != p[k]) {
next[j] = k;// KMPStringMatcher中只有这一行
} else {
// 不能出现p[j] = p[next[j]],所以如果出现这种情况则继续递归,如 k = next[k],
// k = next[[next[k]]
next[j] = next[k];
}
} else {
k = next[k];
}
}
return next;
}

}

4. 花絮

提到字符串匹配,或者说字符串查找,我们会想到Java中的String类就有一个String.indexOf(String str);方法,那它使用的是什么算法呢?在这里截取JavaSE-1.8的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// String.indexOf(String str); 最终会调用该方法
/**
* Code shared by String and StringBuffer to do searches. The
* source is the character array being searched, and the target
* is the string being searched for.
*
* @param source the characters being searched.(源字符数组)
* @param sourceOffset offset of the source string.(源字符数组偏移量)
* @param sourceCount count of the source string.(源字符数组长度)
* @param target the characters being searched for.(待搜索的模式字符数组)
* @param targetOffset offset of the target string.(模式字符数组偏移量)
* @param targetCount count of the target string.(模式数组长度)
* @param fromIndex the index to begin searching from.(从原字符数组的哪个下标开始查询)
*/
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
}

char first = target[targetOffset];
int max = sourceOffset + (sourceCount - targetCount);

for (int i = sourceOffset + fromIndex; i <= max; i++) {
/* Look for first character. */
// 找到第一个匹配的字符的位置
if (source[i] != first) {
while (++i <= max && source[i] != first);
}

/* Found first character, now look at the rest of v2 *
if (i <= max) {
// 找到了第一个匹配的字符,看余下的是否完全匹配
int j = i + 1;
int end = j + targetCount - 1;
for (int k = targetOffset + 1; j < end && source[j]
== target[k]; j++, k++);

if (j == end) {
/* Found whole string. */
return i - sourceOffset;
}
// 如果不完全匹配,因为外层for循环中有i++,即i+1继续匹配
// 故而该方法本质上就是字符串匹配的朴素算法
}
}
return -1;
}

通过对代码片段的注释和分析可以看出,Java源码中的String.indexOf(String str); 内部所使用的算法其实就是字符串匹配的朴素算法…

源码github地址:
StringMatchSample

重要参考:
从头到尾彻底理解KMP