Implement strStr().
Return the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.
Example 1:
Input: haystack = "hello", needle = "ll"
Output: 2
Example 2:
Input: haystack = "aaaaa", needle = "bba"
Output: -1
Clarification:
What should we return when needle
is an empty string? This is a great question to ask during an interview.
For the purpose of this problem, we will return 0 when needle
is an empty string. This is consistent to C’s strstr() and Java’s indexOf().
这道题本质上就是字符串的匹配问题。可以使用暴力破解的方式进行。首先先确立特殊情况。
以上是首先要确定的三种特殊情况。
接下来进行暴力破解,我们可以知道,需要每个文本串都可以产生n-m个长度与模版串长度相同的子串,其中n为文本串的长度,m为模板串的长度。之后将模板串和文本串的字串进行逐一匹配。若匹配成功,则直接返回该匹配成功的子串的首字母在文本串的位置。这种方式需要的时间复杂度为O((n-m+1)m)。
class Solution {
public int strStr(String haystack, String needle) {
if((isNullOrEmpty(haystack) && isNullOrEmpty(needle)) || isNullOrEmpty(needle)) {
return 0;
}
if(haystack.length() < needle.length()){
return -1;
}
int n = haystack.length();
int m = needle.length();
for(int i = 0; i < n-m +1 ;i++){
if(compareTwoString(haystack.substring(i,i+m),needle)){
return i;
}
}
return -1;
}
public boolean isNullOrEmpty(String s){
return s == null || s.length() == 0;
}
public boolean compareTwoString(String s1, String s2){
int n = s2.length() ;
for(int i = 0; i < n; i++){
if(s1.charAt(i) != s2.charAt(i)){
return false;
}
}
return true;
}
}
Rabin-Karp算法是暴力算法的改进。RK算法的思想是将模板串看成为一个数值,然后使用同样的规则计算出同等长度文本串的子串的数值。如果数值不等,那么就表示这两个字符串不可能是相等的字符串。如果相等,则说明这两个字符串有可能相等。对有可能相等的字符串的字符进行一一匹配。
这种方法对模板串进行了预先处理,能够减少了字符串匹配的次数。但是最糟糕的情况下就和暴力破解一样的。同时对模板进行处理需要O(m)的时间,而对于文本串值的计算需要O(m-n)的时间。
如何用数值来代表字符串,这里采用了秦九韶算法,其中在第13行传入的d表示进制,如256表示256进制。而这样的处理方法会出现数值过大的情况,进而导致不方便操作。因此通过mod q的方式使数值变小。这种处理方式和hash的处理方式相似。当然也可以通过其他方式来计算这个数值。
class Solution {
public int strStr(String haystack, String needle) {
if((isNullOrEmpty(haystack) && isNullOrEmpty(needle)) || isNullOrEmpty(needle)) {
return 0;
}
if(haystack.length() < needle.length()){
return -1;
}
return RKM(haystack,needle,256,23);
}
public int RKM(String haystack,String needle,int d,int q){
int n = haystack.length();
int m = needle.length();
int h = 1;
int p = 0;
int t = 0;
for(int i = 0; i < m -1;i++){
h = (h*d)%q;
}
for(int i = 0; i < m;i++){
p = (d*p + Integer.valueOf(needle.charAt(i))) % q;
t = (d*t + Integer.valueOf(haystack.charAt(i))) % q;
}
for(int s = 0; s < n -m + 1; s++){
if(p == t){
if(compareTwoString(haystack.substring(s,s+m),needle)){
return s;
}
}
if(s < n-m ){
t = (d * (t-haystack.charAt(s)*h) + haystack.charAt(s+m))% q;
if(t < 0){
t = t + q;
}
}
}
return -1;
}
public boolean isNullOrEmpty(String s){
return s == null || s.length() == 0;
}
public boolean compareTwoString(String s1, String s2){
int n = s2.length() ;
for(int i = 0; i < n; i++){
if(s1.charAt(i) != s2.charAt(i)){
return false;
}
}
return true;
}
}
KMP算法在更大程度上减少无效的匹配。举个例子如模板串为"abab",文本串为“abacabab“。匹配abac的时候,我们很容易知道是不能匹配的。但是对于第二个a和第一个b。我们也很容易知道同样是无效的。KMP就是通过一个next数组,通过改变偏移量来直接跳过已经知道是无效的匹配。
如abab的next数组为[0,0,1,2]。而next表示的是下一个匹配的字符的下标。同样是上面的那一个例子,我们匹配C失败的时候,但是我们知道上一次匹配a是成功的,因此不再回退到最原始的情况。而是回到上一次匹配a成功的状态,通过next数组我们回到了下标为1的状态,即匹配b。b是不能匹配c的,同理,我们通过next数组回到了0的状态。这个时候才回到了最原始的状态。所以KMP是不直接回到原始状态,而是回到上一次匹配成功的状态,尽可能减少回退。
怎么求next数组,这个也很简单。只要将模板串和其自己进行比较就好了。next[i]换一种角度理解就是以i结尾的字符串P1的真后缀P2的最长前缀长度。首个字母的必然为0。以abac为例。
计算next[1]:P1为 “ab”, 真后缀为"b",因此真后缀的最长前缀为0。(这里只是选取了最长前缀的一个后缀)
计算next[2]:P1为 “aba”, 真后缀为"a",因此真后缀的最长前缀为1。
计算next[3]:P1为 “abab”, 真后缀为"ab",因此真后缀的最长前缀为2。
KMP的本质思想和自动机的思想相似。根据当前的状态判断下一步的状态是怎么样子的。而next数组换一种理解就是。如果当前状态不匹配的话,就跳回之前匹配过的最好状态,正如上面所说的,当c不匹配b的时候,就回到上一次匹配的状态。
KMP算法在计算next数组的时候,需要O(m)的时间,匹配需要O(n)的时间,所以总的时间复杂度为O(m+n)。空间复杂度为O(m)
class Solution {
public int strStr(String haystack, String needle) {
if((isNullOrEmpty(haystack) && isNullOrEmpty(needle)) || isNullOrEmpty(needle)) {
return 0;
}
if(haystack.length() < needle.length()){
return -1;
}
int n = haystack.length();
int m = needle.length();
int[] next = getNext(needle);
int q = 0;
for(int i = 0; i < n ;i++){
while(q > 0 && needle.charAt(q) != haystack.charAt(i)){
q = next[q -1];
}
if(needle.charAt(q) == haystack.charAt(i)){
q++;
}
if( q == m){
return i - m +1;
}
// q = next[q];
}
return -1;
}
public int[] getNext(String needle){
int m = needle.length();
int[] next = new int[m];
next[0] = 0;
int k = 0;
for(int i = 1; i < m ; i++){
while(k > 0 && needle.charAt(k) != needle.charAt(i)){
k = next[k - 1];
}
if(needle.charAt(k)== needle.charAt(i)){
k ++;
}
next[i] = k;
}
return next;
}
public boolean isNullOrEmpty(String s){
return s == null || s.length() == 0;
}
}
BM算法在一定程度上是KMP算法的优化。KMP算法在每一次失败的时候都会回到上一次成功的状态。但是如果模板串当中不存在着要匹配的字符,那么KMP就会通过几次跳跃跳回到模板串的第一个字符进行重新进行匹配。而BM算法最大的特点就是获得更大的跳转,而不需要像KMP一样进行多次跳转。
Sunday算法和KMP算法一样,都是通过计算偏移来减少匹配的次数。不同的是如何进行偏移。我们可以将文本串位置固定,通过模板串的方式来理解这个算法。
Sunday算法主要关注的是参加匹配的最末位的下一位字符。通过这个字符来决定如何偏移。那么会有两种情况:
那么就需要使用一个next数组来计算匹配上模板串的字符的时候,需要移动的位数。
根据上面的两种情况,可以参考13~18行代码,不存在的模板串的字符就设置为模板串的长度 + 1,存在的就为 模板串的长度 - 该字符最右出现的位置。
Sunday 算法计算next数组的时间为O(m + 字符集的长度),最坏的情况下就变为了暴力破解,时间复杂度为O(mn),平均时间复杂度为O(n),空间复杂度为O(字符集的长度)
class Solution {
public int strStr(String haystack, String needle) {
if((isNullOrEmpty(haystack) && isNullOrEmpty(needle)) || isNullOrEmpty(needle)) {
return 0;
}
if(haystack.length() < needle.length()){
return -1;
}
int n = haystack.length();
int m = needle.length();
int[] next = new int[256];
// calculate next array
for(int i = 0; i < next.length - 1;i++){
next[i] = m + 1;
}
for(int i = 0; i < m; i++){
next[needle.charAt(i)] = m - i;
}
int s = 0;//haystack position
int j = 0;//needle position
while( s <= n -m){
j = 0;
while( s + j < n && j < m && haystack.charAt(s+j) == needle.charAt(j)){
j++;
if(j == m){
return s;
}
}
int max = s+m < n ? s+m : n - 1;
s += next[haystack.charAt(max)];
}
return -1;
}
public boolean isNullOrEmpty(String s){
return s == null || s.length() == 0;
}
}