本文已参与[新人创作礼]活动,一起开启掘金创作之路。
JS实现strStr()-俩种解法
BF算法
即暴力(Brute Force)算法,是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果。BF算法是一种蛮力算法。
/**
* @param {string} haystack
* @param {string} needle
* @return {number}
*/
var strStr = function(haystack, needle) {
var haystackLengh =haystack.length;
var needleLengh =needle.length;
for(let i =0;i<=haystackLengh-needleLengh;i++){
let j
for( j=0;j<needleLengh;j++){
if(haystack.charAt(i+j) !== needle.charAt(j)){
break
}
}
if(j == needleLengh){
return i
}
}
return -1
};
Sunday算法
Daniel M.Sunday于1990年提出的一种字符串模式匹配算法。 其核心思想是:在匹配过程中,模式串并不被要求一定要按从左向右进行比较还是从右向左进行比较,它在发现不匹配时,算法能跳过尽可能多的字符以进行下一步的匹配,从而提高了匹配效率。 记模式串为S,子串为T,长度分别为N,M。
/**
* @param {string} haystack
* @param {string} needle
* @return {number}
*/
var strStr = function(haystack, needle) {
const haystackLength = haystack.length;
const needleLength = needle.length;
const list = listmenu(needle)
let i;
for( i =0;i <= haystackLength-needleLength;){
let j;
for(j=0;j<needleLength;j++){
if(haystack[i+j] !== needle[j]){
var x = haystack[i+needleLength]
i += list[x] ? list[x] : list['other']//在偏移表中查询
break
}
}
if(j==needleLength){
return i
}
}
return -1
};
//生成一个模式串对应的偏移表
function listmenu(need){
var res={}
var len = need.length;
for(let i = 0; i < len;i++){
res[need[i]] = len -i
}
res['other'] = len+1
return res
}
Knuth-Morris-Pratt 算法
Knuth- Morris-pratt算法,简称KMP算法,由 Donald Knuth、 James H. Morris 2和 Vaughan Pratt三人1977年联合发表。 Kmth- Morris-Prat算法的核心为前缓数,记作r(i),其定义如下: 对于长度为m的字符串8,其前缓数r(i0≤i<m)表示s的子80:的最长的相等的真前缓与真后缓的长度。特別地,如果不存在符合条件的前后缓,那么(i)=0。其中真前缓与真后缓的定义为不等于身的的前缀与后缀。 我们举个例子说明:字符甲 abacab的前缓数值依次为0,1,0,1,2,2,3
-
π(0)=0,因为 aa 没有真前缀和真后缀,根据规定为 00(可以发现对于任意字符串 π(0)=0 必定成立);
-
π(1)=1,因为 aaaa 最长的一对相等的真前后缀为 aa,长度为 11;
-
π(2)=0,因为 aabaab 没有对应真前缀和真后缀,根据规定为 00;
-
π(3)=1,因为 aabaaaba 最长的一对相等的真前后缀为 aa,长度为 11;
-
π(4)=2,因为 aabaaaabaa 最长的一对相等的真前后缀为 aaaa,长度为 22;
-
π(5)=2,因为 aabaaaaabaaa 最长的一对相等的真前后缀为 aaaa,长度为 22;
-
π(6)=3,因为 aabaaabaabaaab 最长的一对相等的真前后缀为 aabaab,长度为 33。
有了前缀函数,我们就可以快速地计算出模式串在主串中的每一次出现。
复杂度证明
时间复杂度部分,注意到 \pi(i)\leq \pi(i-1)+1π(i)≤π(i−1)+1,即每次当前位的前缀函数至多比前一位增加一,每当我们迭代一次,当前位的前缀函数的最大值都会减少。可以发现前缀函数的总减少次数不会超过总增加次数,而总增加次数不会超过 mm 次,因此总减少次数也不会超过 mm 次,即总迭代次数不会超过 mm 次。
空间复杂度部分,我们只用到了长度为 mm 的数组保存前缀函数,以及使用了常数的空间保存了若干变量。
如何解决本题
记字符串 haystack 的长度为 n,字符串 needle 的长度为 mm。
我们记字符串str= needle+#+ haystack,即将字符串neee和 haystack进行的特殊字符#将两串然后我们对字符串st求前缀为特殊字符#符串str中 haystack部分对应的真前缀必定落在字符串 needle分,真后爱必走落在字符串 haystack部分。当 haystack部分的前爱函数值为m时,我needle在字符串 haystack现(因为此时真前缀恰为字符串 needle
实现时,我们可以进行一定的优化,包括:
我们无需显式地创建字符串 str。 为了节约空间,我们只需要顺次遍历字符串 needle、特殊字符 # 和字符串 haystack 即可。 也无需显式地保存所有前缀函数的结果,而只需要保存字符串needle 部分的前缀函数即可。 特殊字符 # 的前缀函数必定为 0,且易知 π(i)≤m(真前缀不可能包含特殊字符 #)。 这样我们计算 π(i) 时,j=π(π(π(…)−1)−1) 的所有的取值中仅有 π(i−1) 的下标可能大于等于 m。我们只需要保存前一个位置的前缀函数,其它的 jj 的取值将全部为字符串 needle 部分的前缀函数。 我们也无需特别处理特殊字符 #,只需要注意处理字符串haystack 的第一个位置对应的前缀函数时,直接设定 j 的初值为 00 即可。 这样我们可以将代码实现分为两部分:
第一部分是求needle 部分的前缀函数,我们需要保留这部分的前缀函数值。 第二部分是求 haystack 部分的前缀函数,我们无需保留这部分的前缀函数值,只需要用一个变量记录上一个位置的前缀函数值即可。当某个位置的前缀函数值等于 mm 时,说明我们就找到了一次字符串 needle 在字符串 haystack 中的出现(因为此时真前缀恰为字符串 needle,真后缀为以当前位置为结束位置的字符串haystack 的子串),我们计算出起始位置,将其返回即可。
var strStr = function(haystack, needle) {
const n = haystack.length, m = needle.length;
if (m === 0) {
return 0;
}
const pi = new Array(m).fill(0);
for (let i = 1, j = 0; i < m; i++) {
while (j > 0 && needle[i] !== needle[j]) {
j = pi[j - 1];
}
if (needle[i] == needle[j]) {
j++;
}
pi[i] = j;
}
for (let i = 0, j = 0; i < n; i++) {
while (j > 0 && haystack[i] != needle[j]) {
j = pi[j - 1];
}
if (haystack[i] == needle[j]) {
j++;
}
if (j === m) {
return i - m + 1;
}
}
return -1;
};