前置知识
-
前缀 是指从串首开始到某个位置 结束的一个特殊子串。字符串S的以i为结尾的前缀表示为 Prefix(S,i),也就是 Prefix(S,i)。
举例:
-
真前缀 指除了S本身的S的前缀。
举例来说,字符串
abcabcd
的所有前缀为{a, ab, abc, abca, abcab, abcabc, abcabcd}
, 而它的真前缀为{a, ab, abc, abca, abcab, abcabc}
。 -
子串
字符串S的 子串S[i...j]i<j表示 串中从i到j这一段,也就是顺次排列S[i],S[i+1],...S[j]形成的字符串。
有时也会用 S[i...j],i>j来表示空串。
-
前缀函数的定义
给定一个长度为 的字符串 ,其 前缀函数 被定义为一个长度为n的数组 Xn。 其中 的定义是:
- 如果子串s[0...i]有一对相等的真前缀与真后缀:s[0...k-1]和s[i-k+1...i],那么 就是这个相等的真前缀(或者真后缀,因为它们相等)的长度,也就是Xn=[k];
- 如果不止有一对相等的,那么 就是其中最长的那一对的长度;
- 如果没有相等的,那么 。
简单来说 就是,子串 最长的相等的真前缀与真后缀的长度。
用数学语言描述如下:
特别地,规定 Xn[0] = 0(此处忽略了空串,在上述的字串定义中将空串规避掉了)。
举例来说,对于字符串
abcabcd
,因为
a
没有真前缀和真后缀,根据规定为 0因为
ab
无相等的真前缀和真后缀因为
abc
无相等的真前缀和真后缀因为
abca
只有一对相等的真前缀和真后缀:a
,长度为 1因为
abcab
相等的真前缀和真后缀只有ab
,长度为 2因为
abcabc
相等的真前缀和真后缀只有abc
,长度为 3因为
abcabcd
无相等的真前缀和真后缀,同理可以计算字符串
aabaaab
的前缀函数为[0,1,0,1,2,2,3]。
计算字符串的前缀函数
- 求取前缀函数的朴素算法
package com.algorithms.other;
import java.util.Arrays;
//求取前缀函数
//复杂度为O(n^3)
public class PrefixFun {
public static void main(String[] args) {
int[] res = PrefixFun.prefixFun("aabaaab");
Arrays.stream(res).forEach(System.out::println);
// 0 1 0 1 2 2 3
}
/**
* 求取前缀函数
* @param str
*/
static int[] prefixFun(String str){
int len = str.length();
int[] result = new int[len];
//最外层循环用于遍历字串
//字串s[1]
//字串s[1,2]
//字串s[1,2,3]
//...
result[0] = 0;
for(int i = 1;i<len;i++){
//内层循环用于寻找字串的最长公共字串
for(int j = i;j>=0;j--){
if(str.substring(0,j).equals(str.substring(i-j+1,i+1))){
result[i] = j;
break;
}
}
}
return result;
}
}
这个算法的复杂度为O(n^3)
-
优化方向1
减枝:相邻两个前缀函数的值要么加1要么不变要么减少
- for(int j = i;j>=0;j--){
+ for(int j = result[i-1]+1;j>=0;j--){
KMP算法
求取前缀函数的方法为
package com.algorithms.string;
import java.util.Arrays;
//求取前缀函数
//复杂度为O(n^3)
public class PrefixFun {
public static void main(String[] args) {
int[] res = PrefixFun.prefixFunOn("ababcabcacbab");
Arrays.stream(res).forEach(System.out::println);
// 0 1 0 1 2 2 3
}
/**
* 暴力求取前缀函数(复杂度为On3,后期改进后复杂度为On2)
* @param str
*/
static int[] prefixFunOn3(String str){
int len = str.length();
int[] result = new int[len];
//最外层循环用于遍历字串
//字串s[1]
//字串s[1,2]
//字串s[1,2,3]
//...
result[0] = 0;
for(int i = 1;i<len;i++){
//内层循环用于寻找字串的最长公共字串
// for(int j = i;j>=0;j--){
for(int j = result[i-1]+1;j>=0;j--){
if(str.substring(0,j).equals(str.substring(i-j+1,i+1))){
result[i] = j;
break;
}
}
}
return result;
}
/**
* 复杂度为o(n)的前缀函数算法//
* @param str
* @return
*/
static int[] prefixFunOn(String str){
int len = str.length();
//pr
int[] prefixArr = new int[len];
//按照惯例让result[0] = 0
prefixArr[0] = 0;
for(int i = 1;i<len;i++){
//得到转移方程为第
int j = prefixArr[i-1];
//如果prefix[i-1]与新加入相邻的字符串相匹配的话,则prefix[i]的值将直接为prefix[i]+1
//否则的话我们通过前缀数组做二次匹配
//我们不断去寻找这个条件
while(j>0&&str.charAt(j)!=str.charAt(i)){
j = prefixArr[j-1];
}
//如果最终没有找到的话则j必为0
//找到了的话则将结果加1
if(str.charAt(i)==str.charAt(j)){
j++;
}
prefixArr[i] = j;
}
return prefixArr;
}
}
采用双指针方法去对字符串进行搜索,
整个流程为:
具体代码为
package com.algorithms.string;
public class Kmp {
public static void main(String[] args) {
System.out.println( Kmp.KmpSolution("adsdbbabb", "dbba"));
}
/**
* 字符串匹配问题
* @param str 字符串
* @param pattern 匹配串
*/
static int KmpSolution(String str,String pattern){
int[] prefix = PrefixFun.prefixFunOn(str);
int i=0,j=0;
while (i<str.length()&&j<pattern.length()){
//串和匹配串字符相同两者指针都向前移动
if(str.charAt(i)==pattern.charAt(j)){
//如果j到达最后一个下标的话直接返回
if(j==pattern.length()-1){
return i-j;
}
i++;
j++;
}
//让j进行回跳,查看前一个位置的最小前缀函数,回跳至这个位置的下一个位置继续进行比较
else if(j==0||prefix[j-1]==0){
i++;
}
else {
j = prefix[j-1];
}
}
return -1;
}
}