字符串匹配问题
字符串匹配问题:
- 有一个字符串
str1 = "BBC ABCDAB ABCDABCDABDE";
和一个子串str2 = "ABCDABD";
- 现在判断str1是否含有str2,存在返回匹配的第一次位置,不存在返回-1
暴力匹配法
思路
首先想到的就是暴力匹配法:
- 设计两个指针i,j,分别指向两个字符串,i向右移与
str2[j]
比较
- 一直比较到值相同的位置,i、j后移比较
str1[i]
与str2[j]
的值
- 最终比较
str1[10]
与str2[6]
不同,不匹配
- 再重新退回
str1[5]
和str2[0]
开始比较
- 最终匹配到str1[15]才匹配成功
Java代码实现
package com.company.十种算法.kmp;
/**
* Author : zfk
* Data : 17:14
* 字符串匹配问题 :暴力匹配法
*/
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("index = " +index);
}
//暴力匹配算法
public static int violenceMatch(String str1,String str2){
char[] s1 = str1.toCharArray();
char[] s2 = str2.toCharArray();
//i,j为s1,s2的索引
int i = 0;
int j = 0;
//保证不越界
while (i < s1.length && j < s2.length){
//匹配成功
if (s1[i] == s2[j]){
i++;
j++;
}
else {//没有匹配成功
//i回到最开始匹配的后一位,j置0
i = i - (j - 1);
j = 0;
}
}
//判断是否匹配
if (j == s2.length){
return i - j;
}
else {
return -1;
}
}
}
暴力匹配法的缺陷
再第3步到第4步时,我们想既然匹配失败,就退回到最开始匹配的地方,继续往后移动i指针
实际上是重复计算了,我们肉眼可以看出,str1[4]~str1[7]
这一段是可以不用管的,直接到下一个A处
有什么办法能够实现这个想法呢?就是KMP算法
KMP算法
KMP算法是D.E.Knuth,J.H.Morris和V.R.Pratt三位大佬提出的,看懂之后才能明白这个算法的优美、干练之处
KMP算法的核心是利用匹配失败后的信息,尽量减少模式串str2与主串str1的匹配次数以达到快速匹配的目的
在最后匹配失败后:
利用匹配失败后的信息可以减少了3次匹配
这个信息是通过部分匹配表得到的
部分匹配表
- 前缀:指除了最后一个字符以外,一个字符串的全部头部组合的集合
- 后缀:指除了第一个字符以外,一个字符串的全部尾部组合的集合
- 部分匹配值:"前缀"和"后缀"的最长的共有元素的长度
如上面的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,即部分匹配值为1 - “ABCDAB”的前缀为
{"A","AB","ABC","ABCD","ABCDA"}
,后缀为{"B","AB","DAB","CDAB","BCDAB"}
,最长的共有元素为AB,长度为2,即部分匹配值为2 - “ABCDABD”的前缀为
{"A","AB","ABC","ABCD","ABCDA","ABCDAB"}
,后缀为{"D","BD","ABD","DABD","CDABD","BCDABD"}
,部分匹配值为0
即可以作部分匹配表:
部分匹配表的用处
实际上部分匹配表就类型于字符串中重复长度
当某一位没有匹配到时,查看前一位str2[j-1]
的部分匹配值,决定str2移动位数
移动位数 = 已匹配的字符数 - 对应的部分匹配值
在这个时候,已匹配6个字符,最后一个匹配字符B对应的"部分匹配值"为2,所以应该位移6-2=4
位
Java代码实现
实现时关注两个方法:
kmpNext(String str2)
:得到子串str2的部分匹配表kmpSearch(String str1,String str2,int[] next)
:str1匹配字符串str2
其中每次比较时,不等就查看部分匹配表来决定位移位数
相等自然就比较下一位
package com.company.十种算法.kmp;
import java.util.Arrays;
/**
* Author : zfk
* Data : 15:47
*/
public class KMP {
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));
System.out.println("index = "+ kmpSearch(str1,str2,next));
}
/**
* kmp搜索
* @param str1 源字符串
* @param str2 子串
* @param next 部分匹配表
* @return 返回第一个匹配的位置,为-1没有匹配到
*/
public static int kmpSearch(String str1,String str2,int[] next){
//遍历
for (int i = 0,j = 0; i < str1.length();i++){
//需要处理str1.charAt(i) != str2.charAt(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 str2){
//创建next数组保存部分匹配值
int[] next = new int[str2.length()];
//长度为1的字符串部分匹配值为0
for (int i = 1, j = 0; i < str2.length();i++){
//当不等时,需要从next[j-1]获得j
while (j > 0 && str2.charAt(i) != str2.charAt(j)){
j = next[j-1];
}
if (str2.charAt(i) == str2.charAt(j)){
j++;
}
next[i] = j;
}
return next;
}
}
结果: