劍指leetcode—實現字符串匹配函數strStr()

題目描述:實現 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() 定義相符

這類問題屬於字符串匹配問題

解法一: 暴力求解法

最直接的方法—沿着字符換逐步移動滑動窗口,將窗口內的子串與needle字符串比較
時間複雜度爲O((N−L)L)

class Solution
{
public int strStr(String haystack,String needle)
{
int l=needle.length();
int n=haystack.length();
for(int start=0;start<n-l+1;start++)
{
if(haystack.substring(start,start+l).equals(needle){
return start;
}
}
return -1}
}

解法二: 雙指針方法

暴力法會將haystack所有長度爲l的子串都和needle字符串比較。
實際上,只要有一個子串的第一個字符和needle字符串的第一個字符相同時,才進行比較。
在這裏插入圖片描述
然後,可以一個字符一個字符的比較,不匹配了就立即終止。
在這裏插入圖片描述
如下圖所示,比較到最後一位發現不匹配,然後回溯
,pn指針移動到pn=pn-currlen(已成功匹配的長度)+1.
在這裏插入圖片描述
然後再一次比較,重複上面的步驟,直到找到完整匹配的子串,直接返回子串的開始位置pn-l。
在這裏插入圖片描述

算法過程

  1. 移動pn指針,找到pn指向的位置的字符和needle的第一個字符相同
  2. 通過固定住第一個匹配的字符,依次向後比較,計算出currlen,pl,通過pn計算出匹配長度
  3. 如果完全匹配currlen==l,返回匹配子串的起始座標
  4. 如果不完全匹配,回溯,pn=pn-currlen+2,pl=0,currlen=0;

java語言實現

class Solution {
  public int strStr(String haystack, String needle) {
  //獲取文本字符串和需要匹配的字符串的長度
    int L = needle.length(), n = haystack.length();
    if (L == 0) return 0;
    //如果文本字符串爲空,則匹配不出,直接返回0
    int pn = 0;
    while (pn < n - L + 1) {
    //pn表示needle的首字符的匹配地址,最大到n-l,因爲needle的長度爲l。
      while (pn < n - L + 1 && haystack.charAt(pn) != needle.charAt(0)) ++pn;
      //移動pn指針,找到pn指向的位置的字符和needle的第一個字符相同
      int currLen = 0, pL = 0;
      while (pL < L && pn < n && haystack.charAt(pn) == needle.charAt(pL)) {
      //pl,pn要保持地址的不越界,同時如果一個一個字符比較相同,那麼所有字符串的下標加一
        ++pn;//比較下一個
        ++pL;//比較下一個
        ++currLen;//成功匹配字符的個數
      }
      if (currLen == L) return pn - L;
      //如果成功匹配字符的個數等於needle的長度,返回needle首字符在haystack的地址
      pn = pn - currLen + 1;  //如果匹配失敗,回溯pn
    }
    return -1;
  }
}

解法三: Rabin Karp - 常數複雜度

思路如下
先生成窗口內子串的哈希碼,然後在跟needle字符串的哈希碼作比較

那麼如果在常數時間內生成子串的哈希碼呢?
生成一個長度爲l數組的哈希碼,要O(L)時間

利用滑動窗口的特性,每次滑動都有一個元素進,一個出。

只會出現小寫的英文字母,因此可以將字符串轉化成值爲 0 到 25 的整數數組: arr[i] = (int)S.charAt(i) - (int)‘a’。按照這種規則,abcd 整數數組形式就是 [0, 1, 2, 3],轉換公式如下所示
在這裏插入圖片描述
將上面的公式寫成通式,ci爲整數數組的元素,a=26,表示字符集的個數
在這裏插入圖片描述
下面來考慮窗口從 abcd 滑動到 bcde 的情況。這時候整數形式數組從 [0, 1, 2, 3] 變成了 [1, 2, 3, 4],數組最左邊的 0 被移除,同時最右邊新添了 4。滑動後數組的哈希值可以根據滑動前數組的哈希值來計算,計算公式如下所示。
在這裏插入圖片描述

如何避免溢出

a^L 可能是一個很大的數字,因此需要設置數值上限來避免溢出。設置數值上限可以用取模的方式,即用 h % modulus 來代替原本的哈希值。
理論上,modules 應該取一個很大數,但具體應該取多大的數呢? ,對於這個問題來說 2^{31} 就足夠了。

算法詳解

  1. 計算子字符串haystack(0,L),needle(0,L)的哈希值
  2. 從起始位置開始遍歷,第一個字符遍歷到第N-L個字符
    • 根據前一個哈希值計算滾動哈希
    • 如果子字符串哈希值和needle字符串哈希值相同,返回滑動窗口的起始位置
  3. 返回- 1,這時候haystack字符串中不存在needle字符串

簡而言之,就是needle(長度爲L)字符串有屬於自己的哈希值爲RES,在haystack依次利用滑動窗口計算自己的長度爲L的子字符串的哈希值的時候,如果計算出的值是等於RES的那麼這個滑動窗口的起始位置就是完全匹配的起始地址。

class Solution {
 //轉換機制
  public int charToInt(int idx, String s) {
    return (int)s.charAt(idx) - (int)'a';
  }
  public int strStr(String haystack, String needle) {
    int L = needle.length(), n = haystack.length();
    if (L > n) return -1;
    //字符集的個數,用於哈希建立
    int a = 26;
    // 用於取模,防止整數溢出
    long modulus = (long)Math.pow(2, 31);
    // 各自計算各自的哈希值
    long h = 0, ref_h = 0;
    for (int i = 0; i < L; ++i) {
      h = (h * a + charToInt(i, haystack)) % modulus;
      ref_h = (ref_h * a + charToInt(i, needle)) % modulus;
    }
    if (h == ref_h) return 0;//如果相同,說明下標爲0就完全匹配
    // const value to be used often : a**L % modulus
    long aL = 1;
    for (int i = 1; i <= L; ++i) aL = (aL * a) % modulus;
    for (int start = 1; start < n - L + 1; ++start) {
      // compute rolling hash in O(1) time
      h = (h * a - charToInt(start - 1, haystack) * aL
              + charToInt(start + L - 1, haystack)) % modulus;
      if (h == ref_h) return start;
    }
    return -1;
  }
}

字符串匹配還有很多不同的算法,可以參考下面(算法詳解)KMP算法

leetcode實現

c語言實現

int strStr(char * haystack,char * needle)
{
 int i=0,j=0;
if(needle[0]=='\0')
 return 0;
 int len1=strlen(haystack);
 int len2=strlen(needle);
 int *next;
int ans=-1;
 next=(int*)malloc(sizeof(int) * len2);
 getnext(needle,next);
 while(i<len1&&j<len2)
 {
  if(j==-1||haystack[i]==needle[j])
  {
   i++;
   j++;
  }
  else
  j=next[j];
 }
 if(j>=len2)
{
ans=i-len2;
}
 return ans;
}
void getnext(char * needle,int next[])
{
 int j,k;
 int len2=strlen(needle);
 j=0;k=-1;next[0]=-1;
 while(j<len2-1)
 {
  if(k==-1||needle[j]==needle[k])
  {
   j++;k++;
   next[j]=k;
  }
  else
  k=next[k];
 } 
}

java語言實現

class Solution {
    public int strStr(String haystack, String needle) {
    if(needle==null)
    return 0;
    int len1=haystack.length();
    int len2=needle.length();
    char [] s=haystack.toCharArray();
    char [] t=needle.toCharArray();
    int [] next=new int[len2+1];
    int i=0,j=0;
    int ans=-1;
    getnext(needle,next);
    while(i<len1&&j<len2)
    {
    if(j==-1||s[i]==t[j]){
     i++;
     j++;
    }
         else
        j=next[j];
    }
    if(j>=len2)
    {
     ans=i-len2;
    }  
        return ans;
}
    void getnext(String needle,int next[])
    {
    int j,k;
    int len2=needle.length();
    j=0;k=-1;next[0]=-1;
    while(j<len2-1)
    {      
        if(k==-1||needle.charAt(j)==needle.charAt(k))
    {
     j++;k++;
    next[j]=k;
  }
        else
         k=next[k];
    } 
        }
}

這裏有一個疑問,爲什麼c語言實現的next數組的時候,可以數組長度設置爲len2,而java在創建next數組時,長度必須是len2+1,設置爲len2會報下標地址越界錯誤,各位小夥伴們如果你有誰知道,可以留言給我,多謝多謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章