串的模式匹配算法
查找子串(模式串)在主串中的位置的操作通常称为串的模式匹配。
1.朴素算法(Brute-Force(BF)暴力算法)
如果两个指针(i,j)指向的元素相同则指针后移,不相同则需要回退指针(主串指针回退到上次匹配首位的下一个位置,子串指针回退到开头位置),重复进行上述操作直到主串指针指向主串末尾,即如下所示:
在2、3、4步骤中,主串的首字符与子串的首字符均不等。在步骤1中主串与子串的前三个字符相等,这就意味着子串的首字符"g"不可能与主串的二、三位相等,故上图中步骤2、3完全是多余的。
也就是说,如果我们知道子串的首字符"g"与后面两个字符不相等(此处需要进行一些预处理),我们就不需要再进行2、3步操作,只保留1、4、5步即可。
在使用朴素算法进行匹配时,主串指针需要进行一些回退。而在知道了子串的一些性质后,主串指针不需要再进行回退,只需一直往前走就行,这样就节省了一些时间开销。
BF算法分析
1.算法在字符比较不相等,需要回溯即i = i - j + 1:即回退到s中的下一个字符开始进行字符匹配。
2.最好情况下的时间复杂度为O(m+n)
3.最坏情况下的时间复杂度为O(m*n)
2.KMP算法
Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP 算法”,常用于在一个文本串 S 内查找一个模式串 T 的出现位置,为了避免朴素算法的低效,由Donald Knuth、Vaughan Pratt、James H. Morris 三人于 1977 年联合发表,故取这 3 人的姓氏命名此算法。
KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。
字符串的最长公共前后缀
字符串 “ABABA” :
前缀的概念:指的是字符串的子串中从原串最前面开始的子串
前缀有:A,AB,ABA,ABAB
后缀的概念:指的是字符串的子串中在原串结尾处结尾的子串
后缀有:BABA,ABA,BA,A
公共前后缀:一个字符串的所有前缀连续子串和所有后缀连续子串中相等的子串
共前后缀有:A ,ABA
最长公共前后缀:所有公共前后缀的长度最长的那个子串
最长公共前后缀是: ABA
在得知了子串中有相等的前后缀之后,匹配失败时子串指针不需要回退到开头处,而是回退到相等前缀的后一个位置。
部分匹配表(前缀表)Next
从第一个字符开始的每个子串的最后一个字符与该子串的最长公共前后缀的长度的对应关系表格
其实就是:每个子串的最大相等前后缀的长度
字符串T=“aabaaf "
子串 “a”:最后一个字符是 a,该子串没有前缀也没有后缀,最长公共前后缀长度是 0,因此对应关系就是 a - 0
子串 “aa”:最后一个字符是 a,该子串的最长公共前后缀长度是 1,因此对应关系就是 a- 1
子串 “aab”:最后一个字符是 b,该子串的最长公共前后缀长度是 0,因此对应关系就是 b- 0
子串 “aaba”:最后一个字符是 a,该子串的最长公共前后缀长度是 1,因此对应关系就是 a- 1
子串 “aabaa”:最后一个字符是 a,该子串的最长公共前后缀长度是 2,因此对应关系就是 a- 2
子串 “aabaaf”:最后一个字符是 f,该子串的最长公共前后缀长度是 0,因此对应关系就是 f- 0
所以我们能得到字符串T的前缀表为:
j | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
T | a | a | b | a | a | f |
next | 0 | 1 | 0 | 1 | 2 | 0 |
作用:决定了子串指针j在匹配失败时回溯到的位置。
KMP算法就可以整体上分为两步:
1.计算前缀表
import java.util.Arrays;
/**
* 计算前缀表next
*/
public class Next {
/**
* 获取一个字符串 pattern 的部分匹配表
*
* @param patternStr 用于模式匹配字符串
* @return 存储部分匹配表的每个子串的最长公共前后缀的 next数组
*/
public int[] kmpNext(String patternStr) {
//将 patternStr 转为 字符数组形式
char[] patternArr = patternStr.toCharArray();
//预先创建一个next数组,用于存储部分匹配表的每个子串的最长公共前后缀
int[] next = new int[patternStr.length()];
/*
从第一个字符(对应索引为0)开始的子串,如果子串的长度为1,那么肯定最长公共前后缀为0
因为这唯一的一个字符既是第一个字符,又是最后一个字符,所以前后缀都不存在 -> 最长公共前后缀为0
*/
next[0] = 0;
/*
len有两个作用:
1. 用于记录当前子串的最长公共前后缀长度
2. 同时知道当前子串的最长公共前后缀的前缀字符串对应索引==>子串指针j在匹配失败时回溯到的位置
*/
int len = 0;
//从第二个字符开始遍历,求索引在 [0,i] 的子串的最长公共前后缀长度
int i = 1;
while (i < patternArr.length) {
//比较一下 patternArr[len] 与 patternArr[i] 是否相等
if (patternArr[len] == patternArr[i]) {
/*
1.如果相等即 patternArr[len]==patternArr[i],那么就可以确定存在当前子串的最长公共前后缀
2.由于是拼接操作,那么当前子串的最长公共前后缀长度只需要在上一个子串的最长公共前后缀长度的基础上 +1 即可
即 next[i] = next[i-1] + 1
3.由于 len 是记录的子串的最长公共前后缀长度, len 还是记录的上一个子串的最长公共前后缀长度,
因此:next[i] = next[i-1] + 1 等价于 next[i] = ++len
*/
next[i] = ++len;
//判断以下一个字符结尾的子串的最长公共前后缀长度
i++;
// A B C D A B D
// [0, 0, 0, 0, 1, 2, 0]
}else {
/*
1.如果不相等 patternArr[len]!=patternArr[i]
2.可以先修改len的值:len = next[len-1],再去判断下一个字符是否相等,即 判断 patternArr[len] 是否等于 patternArr[i]
3.但实际上我们在这里改为 len = next[len-1] 表示上一个子串的最长公共前后缀字符串的长度
*/
if(len==0) {
/*
len为 0说明上一个子串已经没有了公共前后缀字符串
则我们没有继续寻找的必要,当前子串的最长公共前后缀字符串长度就是0
*/
next[i] = len;
//继续寻找下一个字符串的最长公共前后缀字符串长度
i++;
}else{
len = next[len - 1];
}
}
}
return next;
}
}
//测试
public static void main(String[] args) {
Next next = new Next();
String patternStr = "ABCDABD";
System.out.println(Arrays.toString(next.kmpNext(patternStr)));
//输出结果:[0, 0, 0, 0, 1, 2, 0]
}
2.根据前缀表移动两个指针进行匹配
/** 匹配成功一个就退出匹配
* @param matchStr 原字符串
* @param patternStr 子串
* @param next 子串对应的部分匹配表
* @return 如果是-1,就是没有匹配到,否则就返回第一个匹配的位置
*/
public int search(String matchStr, String patternStr, int[] next) {
int i = 0, j = 0;
while (i < matchStr.length() && j < patternStr.length()) {
// matchStr = "AABABADDABAC";
// patternStr = "BAB";
if (matchStr.charAt(i) == patternStr.charAt(j)) {
//相等就继续进行匹配
i++;
j++;
} else {
//如果 patternStr[i] 和 matchStr[j] 不相等
if (j == 0) {
/*
表示 matchStr 没有匹配到 patternStr的第一个字符
那直接将 matchStr 的指针 i 向后移动一位即可
*/
i++;
} else {
j = next[j - 1];
}
}
}
return j == patternStr.length() ? i - j : -1;
}
public static void main(String[] args) {
KmpSearch kmpSearch = new KmpSearch();
Next next = new Next();
String matchStr = "AABABADDABAC";
String patternStr = "BAB";
int index = kmpSearch.search(matchStr, patternStr, next.kmpNext(patternStr));
System.out.println("index = " + index);
// 输出:index = 2
}
允许匹配多个,可重复索引字符的代码:
import java.util.ArrayList;
/**
* kmp搜索算法
* 允许匹配多个,可重复索引字符的代码
*/
public class Search {
/**
* @param matchStr 原字符串
* @param patternStr 子串
* @param next 子串对应的部分匹配表
* @return 每次匹配成功的字符串的开始索引位置的集合
*/
public ArrayList<Integer> kmpSearch(String matchStr, String patternStr, int[] next) {
int i = 0, j = 0;
ArrayList<Integer> firstIndexList = new ArrayList<>();
while (i < matchStr.length()) {
if (matchStr.charAt(i) == patternStr.charAt(j)) {
//相等就继续进行匹配
i++;
j++;
} else {
//如果 patternStr[i] 和 matchStr[j] 不相等
if (j == 0) {
/*
表示 matchStr 没有匹配到 patternStr的第一个字符
那直接将 matchStr 的指针 i 向后移动一位即可
*/
i++;
} else {
j = next[j - 1];
}
}
if (j == patternStr.length()) {
//超出了最大索引值
firstIndexList.add(i - j);
j = next[j - 1];
}
}
return firstIndexList;
}
public static void main(String[] args) {
Next next = new Next();
Search search = new Search();
//原字符串
String matchStr = "AABABADDABAC";
//子串
String patternStr = "ABA";
ArrayList<Integer> arrayList = search.kmpSearch(matchStr, patternStr, next.kmpNext(patternStr));
System.out.println(arrayList);
// 输出:[1, 3, 8]
}
}
算法动画页面效果可免费给哦!