一、基本概念
字符串匹配是计算机科学中最古老、研究最广泛的问题之一。一个字符串是一个定义在有限字母表∑上的字符序列。例如,ATCTAGAGA是字母表∑ = {A,C,G,T}上的一个字符串。字符串匹配问题就是在一个大的字符串T中搜索某个字符串P的所有出现位置。其中,T称为文本,P称为模式,T和P都定义在同一个字母表∑上。
对于字符串匹配问题,我们最直接能想到的就是暴力匹配,由于暴力匹配算法时需要大量的回溯,所以虽然可以解决问题,但其效率是非常低的。而为了解决这一问题,我们可以使用KMP算法来解决字符串匹配问题。
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。
二、思路分析
现假设有字符串str1="BBC ABCDAB ABCDABCDABDE"和一个字串str2=“ABCDABD”,需要判断str1中是否存在str2,若存在则返回第一次出现的位置,否则返回-1
1.暴力匹配算法(不推荐):
如果使用暴力匹配算法,并假设现在str1匹配到i位置,子串str2匹配到j位置,则有:
- 如果当前字符串匹配成功,即str1[i] = str2[j],则i++,j++,继续匹配下一个字符。
- 如果匹配失败,即str1[i] != str2[j],令i = i - (j - 1),j = 0,相当于每次匹配失败,i回溯,j置为0。
- 直到str2完全进行匹配并返回 i = i - j。
2.KMP算法(推荐)
KMP算法的详细实现原理可以参考:
链接: KMP算法原理详解
- 算法步骤:
- 根据待匹配字符串得到部分匹配表
- 对字符串进行匹配
- 如果两个字符串的第一个字符不符合就i++,j++
- 重复1,直到str1的某个字符与str2的某个字符符合,即str1[i] = str2[0]
- 当后面再次出现str1[i] != str2[j]的情况时,不再像暴力匹配算法一样进行回溯到已经比较过的位置,而是根据部分匹配表来进行继续向后移动,移动位数 = 已匹配的字符数 - 对应的部分匹配值。
- 直到直到str2完全进行匹配并返回 i = i - j + 1。
KMP算法的几本匹配步骤和暴力算法是类似的,只是在匹配失败时,KMP算法不再是回溯到已经匹配过的位置,而是根据部分匹配值继续后移来提高效率。
- 部分匹配表
先简单介绍一下字符串的前缀和后缀:
关于这个部分匹配表的计算,实际上就是计算字符串的前缀和后缀的最长公共元素的长度。这里我们以ABCDABD为例,来进行分析。
- "A"的前缀和后缀都为空,所以共有元素长度为0
- "AB"的前缀为[A],后缀为[B],所以共有元素长度为0
- "ABC"的前缀为[A,AB],后缀为[C,BC],所以共有元素长度为0
- "ABCD"的前缀为[A,AB,ABC],后缀为[D,CD,BCD],所以共有元素长度为0
- “ABCDA"的前缀为[A,AB,ABC,ABCD],后缀为[A,DA,CDA,BCDA],共有元素为“A”,所以共有元素长度为1
- “ABCDAB"的前缀为[A,AB,ABC,ABCD,ABCDA],后缀为[B,AB,DAB,CDAB,BCDAB],共有元素为“AB”,所以共有元素长度为2
- "ABCDABD"的前缀为[A,AB,ABC,ABCD,ABCDA,ABCDAB],后缀为[D,BD,ABD,DABD,CDABD,BCDABD],所以共有元素长度为0
所以我们可以得到以下一个部分匹配表
搜索词 | A | B | C | D | A | B | D |
---|---|---|---|---|---|---|---|
部分匹配值 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
部分匹配的实质是,有时候,字符串头部和尾部会有重复。比如,”ABCDAB“之中有两个”AB“,那么它的部分匹配值就是2("AB"的长度),搜索词移动的时候,第一个”AB“向后移动4位(字符串长度-部分匹配值)就可以来到第二个"AB"的位置。
三、代码实现
1.暴力匹配算法代码实现
/**
* @author dankejun
* @create 2020/9/1811:25
*/
public class ViolenceMatch {
public static void main(String[] args) {
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABD";
int index = violenceMatch(str1, str2);
System.out.println("str2第一次出现的位置为:" + index);
}
//暴力匹配算法
public static int violenceMatch(String str1, String str2) {
char[] s1 = str1.toCharArray();
char[] s2 = str2.toCharArray();
int s1Len = s1.length;
int s2Len = s2.length;
int i = 0;
int j = 0;
while (i < s1Len && j<s2Len) {
if (s1[i] == s2[j]) {
i++;
j++;
} else {
i = i - (j - 1);
j = 0;
}
}
if (j == s2Len) {
return i - j;
} else return -1;
}
}
测试结果:
2.KMP算法代码实现
/**
* @author dankejun
* @create 2020/9/1815:58
*/
public class KMPAlgorithm {
public static void main(String[] args) {
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABD";
int[] next = kmpNext(str2);
System.out.println("next=" + Arrays.toString(next));
int index = kmpSearch(str1, str2, next);
System.out.println("str2第一次出现的位置为:" + index);
}
public static int kmpSearch(String str1, String str2, int[] next) {
for (int i = 0, j = 0; i < str1.length(); i++) {
//匹配未完成且匹配失败,j根据最后一个匹配字符的部分匹配值继续后移
while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
j = next[j - 1];
}
//匹配成功,继续后移
if (str1.charAt(i) == str2.charAt(j)) {
j++;
}
//待匹配字符串已经匹配完毕
if (j == str2.length()) {
return i - j + 1;
}
}
return -1;
}
//获取字符串的部分匹配值
public static int[] kmpNext(String dest) {
//保持部分匹配值
int[] next = new int[dest.length()];
next[0] = 0;//如果字符串长度为1,部分匹配值为0
for (int i = 1, j = 0; i < dest.length(); i++) {
while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
j = next[j - 1];
}
if (dest.charAt(i) == dest.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
}
测试结果: