剑指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会报下标地址越界错误,各位小伙伴们如果你有谁知道,可以留言给我,多谢多谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Devin Dever

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值