1. 字符串匹配问题
- 有一个字符串
str1="BBC ABCDAB ABCDABCDABDE",和子串str2="ABCDABD"- 判断
str1是否含有str2,如果存在,就返回第一次出现的位置,如果没有,则返回-1
2. 暴力匹配算法
2.1 思路分析
假设,
str1匹配到了 i 位置,子串str2匹配到了 j 位置,则有:① 如果当前字符匹配成功
(即:str1[i] = str2[j]),则 i++, j++, 继续匹配下一个字符② 如果失配
(即 str1[i] != str2[j]),令主串和子串回溯:i=i-(j-1),j=0;③ 用暴力匹配解决,会有大量的回溯,每次只能移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间
2.2 代码实现
public class ViolenceMatch {
public static void main(String[] args) {
String str1 = "爱爱你 我爱你他我爱 我爱你他我爱你他我爱他他了";
String str2 = "我爱你他我爱他~";
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();
int s1Len = s1.length;
int s2Len = s2.length;
int i = 0; //i索引指向s1
int j = 0; //j索引指向s2
while(i < s1Len && j < s2Len){ //保证匹配时,不越界
if(s1[i] == s2[j] ){ //匹配成功
i++;
j++;
}else{ //没有匹配成功
//如果失配(即 str1[i] != str2[j] ,令 i=i-(j-1),j=0)
i = i - (j - 1);
j = 0;
}
}
//判断是否匹配成功
if(j == s2Len) {
return i - j;
}else{
return -1;
}
}
}
3. KMP算法
- Knuth-Morris-Pratt 字符串查找算法,简称 “KMP算法” ,常用于在一个文本串 S 内查找一个模式串 P 的出现位置。
- KMP 算法利用之前就判断过的信息,通过一个 next 数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next 数组找到,前面匹配的过的位置,省去了大量的计算时间
3.1 关于KMP中部分匹配表
- 是在子串匹配主串时,发生不匹配时,但前面已经比较过的字符串已经大致知道,为了避免主串的过度回溯,所以需要部分匹配表,调整回溯的位置
3.1.1 关于前缀和后缀
3.1.2 部分匹配表
- 部分匹配表:就是“前缀”和“后缀”的最长的共有元素的长度。以 “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;
3.1.3 部分匹配的实质
- 有时候,字符串的头部和尾部会有重复,比如 “ABCDAB" 之中有两个 ”AB“ ,那么它的 “部分匹配值”就是2 .搜索词移动的时候,第一个 “AB” 向后移动4位
移动位数 = 已匹配的字符数 - 对应的部分的匹配值,就可以来到第二个 “AB” 的位置
3.2 思路分析
① 首先,用 Str1 的第一个字符和 Str2 的第一个字符去比较,不符合,Str1的指针向后移动一位
② 重复第一步,还是不符合,再后移
③ 一直重复,直到 Str1 有一个字符与 Str2 的第一个字符符合为止
④ 接着比较字符串和搜索词的下一个字符,若是符合,继续比较下一个字符,重复操作,若是遇到不符合的
⑤ 当发现不匹配时,因为BCD已经比较过了,没有必要再做重复的工作。而当空格与D不匹配时,其实已经直到了前面六个字符位“ABCDAB”。KMP的算法思想是,设法利用这个已知信息,不要回溯到已经比较过的位置。
⑥ 对 Str2 计算部分匹配值
⑦ 已知空格与 D 不匹配,前面六个字符“ABCDAB”是匹配的。查表可知,最后一个匹配字符串B对应的部分匹配值为2,因此:
移动位数 = 已匹配的字符数 - 对应的部分的匹配值,因为 6-2 等于 4,所以搜索词向后移动 4 位⑧ 因为空格和C不匹配,搜索词还要继续往后移,这是已匹配的字符数位 “AB”,对应的 “部分匹配值” 为0,所以
移动位数 = 2 - 0;于是将搜索词先把后移动 2 位
⑨ 因为 空格与A不匹配,继续向后移一位
⑩ 逐位比较,知道发现 C 与 D 不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动 4 位
- 逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成
3.3 代码实现
import java.util.Arrays;
/**
* @author feng
* @create 2022-03-21 8:03
*/
public class KMPAlgorithm {
public static void main(String[] args) {
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABD";
// String str2 = "BBC";
int[] next = kmpNext(str2); //[0,1]
System.out.println("next=" + Arrays.toString(next));
int index = kmpSearch(str1,str2,next);
System.out.println("index= "+index);
}
//写出我们的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),去调整j的大小
//KMP核心算法点
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()){ //找到了i=2 j = 3
return i - j + 1;
}
}
return -1;
}
//获取到一个字符串(子串)的部分匹配值表
// 如果子串中 ABCDABD
//A->0
// AB: 正A 逆B 0
//ABC: 正 A AB 逆C BC 0
//ABCD : 正 A AB ABC 逆 C BC BCD 0
//ABCDA: 正 A AB ABC ABCD 逆 A DA CDA BCDA 1(A)
//ABCDAB: 正 A AB ABC ABCD ABCDA 逆 B AB DAB CDAB BCDAB ->2(AB)
//ABCDABD: 0
public static int[] kmpNext(String dest){
//创建一个next数组,保存部分匹配值
int[] next = new int[dest.length()];
next[0] = 0; //如果字符串是长度为1 部分匹配值是0
for (int i = 1,j=0; i < dest.length(); i++) {
//当dest.charAt(i) != dest.charAt(j) 我们需要从 next[j-1] 获取新的j
//直到我们发现有dest.charAt(i) == dest.charAt(j) 成立时才退出
//这是kmp算法的核心点
while(j > 0 && dest.charAt(i) != dest.charAt(j) ){
j = next[j-1];
}
//当dest.charAt(i) == dest.charAt(j) 满足时,部分匹配值+1
if(dest.charAt(i) == dest.charAt(j)){
j++;
}
next[i] = j;
}
return next;
}
}