情景导入
给你两个字符串
haystack
和needle
,请你在haystack
字符串中找出needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果needle
不是haystack
的一部分,则返回-1
。示例 1:
输入:haystack = "sadbutsad", needle = "sad" 输出:0 解释:"sad" 在下标 0 和 6 处匹配。 第一个匹配项的下标是 0 ,所以返回 0 。示例 2:
输入:haystack = "leetcode", needle = "leeto" 输出:-1 解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。提示:
1 <= haystack.length, needle.length <= 104
haystack
和needle
仅由小写英文字符组成
很轻松的想到使用暴力的方式去解决问题,两个for循环从头开始遍历文本串与模式串,一匹配失败就从头开始遍历。
class Solution {
public int strStr(String haystack, String needle) {
int index=-1;
if(needle.length()>haystack.length()) return -1;
for(int i=0;i<haystack.length();i++){
if(haystack.charAt(i)==needle.charAt(0)){
boolean flag=true;
for(int j=1;j<needle.length();j++){
if(i+j>=haystack.length()){
flag=false;
break;
}
if(needle.charAt(j)!=haystack.charAt(i+j)){
flag=false;
break;
}
}
if(flag) return i;
}
}
return -1;
}
}
明显这样子的时间复杂度O(n*m),那能不能降低时间复杂度去解决问题呢?
这时匹配失败,明显我们可以不从头开始匹配,可以从下图位置开始匹配。
但要怎么才能实现不从头开始匹配呢?
前缀表
前缀/后缀
前缀 是指从串首开始到某个位置
结束的一个特殊子串。
举例来说,字符串
abcabcd
的所有前缀为{a, ab, abc, abca, abcab, abcabc, abcabcd}
, 而它的真前缀为{a, ab, abc, abca, abcab, abcabc}
。后缀 是指从某个位置
开始到整个串末尾结束的一个特殊子串。
举例来说,字符串
abcabcd
的所有后缀为{d, cd, bcd, abcd, cabcd, bcabcd, abcabcd}
,而它的真后缀为{d, cd, bcd, abcd, cabcd, bcabcd}
。
前缀函数
给定一个长度为n的字符串s,前缀表定义为pi[n]。
pi[0...n-1]表示s的从0开始到n-1的子串,最长相等真前缀与真后缀的长度。
现在举个例子方便理解:
s="aabaaf"
s1="a", 无真前缀与真后缀(不包括本身的) pi[0]=0;
s2="aa",真前缀={a} 真后缀={a} pi[1]=1;
s3="aab",真前缀={a,aa} 真后缀={b,ab} pi[2]=0;
s4="aaba",真前缀={a,aa,aab} 真后缀={a,ba,aba} pi[3]=1;
s5="aabaa",真前缀={a,aa,aab,aaba} 真后缀={a,,aa,baa,abaa} pi[4]=2;
s6="aabaaf",真前缀={a,aa,aab,aaba,aabaa} 真后缀={f,af,aaf,baaf,abaaf} pi[5]=0;
利用前缀表就可以知道该从哪重新开始匹配。
求前缀表pi的函数称为前缀函数
朴素算法求前缀函数
这里j表示真前缀与真后缀最大长度,i表示前缀表的下标。这个函数简单来说外层的for就是i从1开始(不理解为什么i从1开始就重新看看真前缀和真后缀是什么),遍历到n;内层的for的作用是判断长度为[0,i]的子串的最大相等真前缀与真后缀是多少,j是这个子串的长度也是现在可能的最大真前缀与真后缀的值。理解不了可以代“aabaaf”去实验一下。
public int[] prefix_function(String s){
int n=s.length();
int[] pi = new int[n];
for(int i=1;i<n;i++){
for(int j=i;j>=0;j--){
if(s.substring(0,j).equals(s.substring(i-j+1,i+1))){
pi[i]=j;
break;
}
}
}
return pi;
}
时间复杂度:O(n^3)
现在可以求到前缀表了,但是能不能再优化一下时间?
过程优化1
重要的优化1:相邻的前缀函数值至多增加1。
换种说法就是相邻的最大相等真前缀与真后缀最多相差1,转化到函数就是j不用从i开始,可以从前缀表pi[i-1]+1开始,因为最多就差1。
public int[] prefix_function(String s){
int n=s.length();
int[] pi = new int[n];
for(int i=1;i<n;i++){
for(int j=pi[i-1]+1;j>=0;j--){
if(s.substring(0,j).equals(s.substring(i-j+1,i+1))){
pi[i]=j;
break;
}
}
}
return pi;
}
时间复杂度:O(n^2)
还能不能继续进行时间上的优化?
过程优化2
在优化1中我们知道我们匹配的时候可以从pi[i-1]+1开始遍历,因为邻近的最多增加1。匹配失败之后我们每次都要去str1.equals(str2),那我们能不能减少回溯产生的多余时间呢?
i 代表的是当前pi的下标也代表字符串 s 的下标为 i 的字符,j表示的是现在 s[0...i ]子串 时的可能最长相等真前缀的最后一个字符。如果s[i] == s[j]那么 j++ ,这个是没有问题的。当s[i] != s[j] 时,那我们就让 j=pi[j-1] ,pi[j-1]表示下标为j-1的最长相等真前缀的最后一个字符,还记得前缀表有什么用吗,前缀表可以知道跳转到什么位置(因为前缀表的值就表示该下标下的最长相等真前缀和后缀),如果这时 s[j]==s[i] 那么此时就可以继续匹配了,一直到 j=0为止。
public int[] prefix_function(String s){
int n = s.length();
int[] pi = new int[n];
for (int i = 1; i < n; i++) {
int j = pi[i-1];
while (j>0&&s.charAt(i)!=s.charAt(j)){
j=pi[j-1];
}
if(s.charAt(i)==s.charAt(j)) j++;
pi[i]=j;
}
return pi;
}
时间复杂度O(n)
学会了前缀表的构建我觉得可以自己试着去匹配文本串了,两个原理都是一样的。
匹配子串
现在获得了前缀表,再重复一次前缀表作用是可以知道不匹配的时候应该跳转到哪里。两个指针,i指向haystack(文本串),j指向needle(模式串),当haystack[i] !=needle[j] 就跳转到 j=pi[j-1]上。相等就 j++,直到 j==n的时候证明指向末尾了就返回此时的 i-j+1表示模式串的起始。
public int strStr(String haystack, String needle) {
int[] arr = new int[needle.length()];
prefix_function(arr,needle);
int m=haystack.length();
int n=needle.length();
int j=0;
for(int i=0;i<m;i++){
while(j>0&&haystack.charAt(i)!=needle.charAt(j)){
j=arr[j-1];
}
if(haystack.charAt(i)==needle.charAt(j)){
j++;
if(j==n) return i-j+1;
}
}
return -1;
}
参考网站: