题目描述:实现 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。
算法过程
- 移动pn指针,找到pn指向的位置的字符和needle的第一个字符相同
- 通过固定住第一个匹配的字符,依次向后比较,计算出currlen,pl,通过pn计算出匹配长度
- 如果完全匹配currlen==l,返回匹配子串的起始坐标
- 如果不完全匹配,回溯,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} 就足够了。
算法详解
- 计算子字符串haystack(0,L),needle(0,L)的哈希值
- 从起始位置开始遍历,第一个字符遍历到第N-L个字符
- 根据前一个哈希值计算滚动哈希
- 如果子字符串哈希值和needle字符串哈希值相同,返回滑动窗口的起始位置
- 返回- 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会报下标地址越界错误,各位小伙伴们如果你有谁知道,可以留言给我,多谢多谢。