先上题目。
实现 strStr() 函数。
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
示例 1: 输入: haystack = "hello", needle = "ll" 输出: 2
示例 2: 输入: haystack = "aaaaa", needle = "bba" 输出: -1
说明: 当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。 对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。
其实就是手写KMP
(但是已经大四的本人,早就把数据结构学的模式匹配抛之脑后,故而重拾)
我对于解题的习惯是能用暴力先暴力,主要是为了对题目有个初步的了解,再进阶。
先上代码。
class Solution {
public int strStr(String haystack, String needle) {
int index = -1;
if(needle == null){
return 0;
}
if(haystack == null){
return index;
}
if(haystack.length() < needle.length()){
return index;
}//防止数组越界的条件1
char[] text = haystack.toCharArray();
char[] mode = needle.toCharArray();
for (int i = 0; i < text.length; i++) {
int temp = i;
for (int j = 0; j < mode.length; j++,temp++) {
if(text[temp] != mode[j] || (temp == text.length - 1 && j != mode.length - 1)){
break;//后一个条件是为了防止数组越界的条件2
}
if(j == mode.length - 1){
return i;
}
}
}
return index;
}
}
暴力解法也有陷阱需要避免,比如防止数组越界,以及String为null的情况。
时间复杂度为O(m*n),LeetCode上能够给过,空间复杂度为O(1),(Java比较恶心的地方是不能直接向字符数组一样对String进行操作,我这里不想每次都用String的charAt方法,就转成了char[],空间复杂度就上去了,其实本质上只需要O(1))(其实谈不上恶心,只是因为OOP程度更高,且更安全,毕竟人家还有可扩展的StringBuffer和StringBuilder)。
打BOSS前先刷经验。
对于模式串为aabaaf,文本串为aabaabaaf,当文本串与模式串都匹配到第六个字母,出现了冲突,此时模式串指针直接回退index为2处(初始为0),即为b字母位置进行接着比对(文本串指针位置不变->可以先感性理解,其实严格证明并不那么想当然)节省了大量时间——相较于暴力解法,文本串不用回退了,模式串不用回退到一开始了
前缀表示包含首字母,且不包含尾字母的所有子串。aabaa的前缀有{a,aa,aab,aaba},没有aabaa
。所有前缀的集合,like,{a,aa,aab,aaba}称之为前缀表
后缀表示包含尾字母,且不包含首字母的所有子串。aabaa的后缀有{a,aa,baa,abaa},没有aabaa
。所有后缀的集合,like,{a,aa,baa,abaa}称之为后缀表
最长相等前后缀指的是->某个固定字符串的后缀表与前缀表的交集中元素的最长长度。e.g.aabaa的最长相等前后缀为{a,aa,aab,aaba}与{a,aa,baa,abaa}的交集{a,aa}的最长长度为aa的长度为2。
是回退位置的决定因素。
next数组是一个模式串等长数组,next[i]维护subString(0,i+1)的最长相等前后缀;
当模式匹配过程中当前字母charAt(i)出现冲突的时候,模式串回退到charAt(next[i-1])。对于next[0]单独写分支。注意next[0] = 0(这是前面3.2和3.3讲前后缀定义时,强调“且”的缘故)
(由于涉及前一位,所以不同人的具体实现与定义会有差异,但是本质都是找最长相等前后缀)
i作为后缀的尾指针(从1开始),j作为前缀的尾指针(从0开始)。求next数组的过程相当于前缀和后缀的匹配,文本串为后缀,模式串为前缀。j每轮的起始位置表示上一个i最长相等前后缀的长度。
冲突时,j会返回到next[j - 1]。有人会问为什么不回退到0,原因是,如果回退到0,就无法保证“”最长”。举个例子,“aabaaac”,i= 5,j = 2时,出现不等,如果j回退到0,next[5] = 1,但是实际上应该等于2
public int[] getNext(String s){
int[] next = new int[s.length()];
next[0] = 0;
int j = 0;//后缀末尾,以及前一位的最长相等前后缀长度
for (int i = 1; i < next.length; i++) {
while(s.charAt(i) != s.charAt(j) && j>0){
j = next[j - 1];//相当于模式匹配时的回退,只不过变成了前缀和后缀的匹配,此时模式串为前缀
}//注意是while,而不是if
if(s.charAt(i)==s.charAt(j)){
j++;
}
next[i] = j;
}
return next;
}
class Solution {
public int strStr(String haystack, String needle) {
// int index = -1;
// if(needle == null){
// return 0;
// }
// if(haystack == null){
// return index;
// }
// if(haystack.length() < needle.length()){
// return index;
// }
// char[] text = haystack.toCharArray();
// char[] mode = needle.toCharArray();
// for (int i = 0; i < text.length; i++) {
// int temp = i;
// for (int j = 0; j < mode.length; j++,temp++) {
// if(text[temp] != mode[j] || (temp == text.length - 1 && j != mode.length - 1)){
// break;
// }
// if(j == mode.length - 1){
// return i;
// }
// }
// }
// return index;
//暴力解法
//KMP算法
//前缀表示包含首字母不包含尾字母的所有子串
//后缀表示包含尾字母不包含首字母的所有子串
//最长相等前后缀指的是->某个固定字符串的后缀表与前缀表的最长相等者的长度
/**next数组是一个模式串等长数组,next[i]维护subString(0,i+1)的最长相等前后缀;
当模式匹配过程中当前字母charAt(i)出现冲突的时候
模式串回退到charAt(next[i-1])
**/
int index = -1;
int next[] = getNext(needle);
if(needle == null)return 0;
if(haystack == null || needle.length() > haystack.length())return index;
int j = 0;
for (int i = 0; i < haystack.length(); i++) {
while(j < needle.length()){
if(j == 0 && haystack.charAt(i) != needle.charAt(j)){
break;
}
if(haystack.charAt(i) == needle.charAt(j) && j == needle.length() - 1){
return i - j;
}//找到的情况
if(haystack.charAt(i) == needle.charAt(j) && i == haystack.length() - 1 && j < needle.length()){
return -1;
}//末尾部分匹配但是越界的情况
if(haystack.charAt(i) == needle.charAt(j)){
j++;
break;
}
if(haystack.charAt(i) != needle.charAt(j) && j > 0){
j = next[j - 1];
}
}
}
return index;
}
public int[] getNext(String s){
int[] next = new int[s.length()];
next[0] = 0;
int j = 0;//后缀末尾,以及前一位的最长相等前后缀长度
for (int i = 1; i < next.length; i++) {
while(s.charAt(i) != s.charAt(j) && j>0){
j = next[j - 1];//相当于模式匹配时的回退,只不过变成了前缀和后缀的匹配,此时模式串为前缀
//j = 0;错误
}//注意是while,而不是if
if(s.charAt(i)==s.charAt(j)){
j++;
}
next[i] = j;
}
return next;
}
}
时间复杂度会降为O(m+n)