代码随想录Day 9 | 字符串Part 2


28. 找出字符串中第一个匹配项的下标

暴力解法

一上来的思路是遍历字符串,找到就返回下标,有匹配错误就重置index重新找。

class Solution {
public:
    int strStr(string haystack, string needle) {
        int index = 0;
        for (int i = 0; i < haystack.size(); i++){
            if (haystack [i] == needle[index]){
                index++;
            }
            else
                index = 0;
            if (index == needle.size()) return i + 1 - index;
        }
        return -1;
    }
};

在力扣上通过了66/79个案例,直到遇到了在“mississippi”中寻找“issip”,预期结果为4,但上面思路运行出来是-1。仔细一看,原因是在遍历到 haystack[6] 时匹配失败,index 置为0,而实际上 haystack[4] 和 haystack[5] 和 index 的前两个是匹配的,这时 index 应该置为2。

接下来的思路是考虑 else 部分的修改,即当发生匹配错误时,怎么将 i 和 index 放到一个合适的位置。一个简单的想法是记录字符串开始匹配的位置 start,当匹配失败时返回 start+1 的位置重新检查,并将 index 置为0。
初步尝试了一下,感觉没有办法把已经有正确匹配项并搜索到错误字符的情况和没有正确搜索的情况合并,所以最终分为匹配成功、匹配不成功但之前有成功的部分、匹配不成功三种情况讨论:

class Solution {
public:
    int strStr(string haystack, string needle) {
        int index = 0;
        int start;
        for (int i = 0; i < haystack.size(); i++){
            if (haystack [i] == needle[index]){
                if (index == 0) 
                    start = i;
                index++;
            }
            else if (index > 0){
               i = start; 
               index = 0;
            }
            else
                index = 0;
            if (index == needle.size()) return i + 1 - index;
        }
        return -1;
    }
};

这种解法实际上和双循环暴力解法是等价的,不知道为什么这段代码提交的耗时是0 ms,比力扣官方答案小很多,让人一度产生错觉。

KMP算法

“aaaaab” 中寻找 “aaaab”,怎么在遍历到 “aaaaa” 时让 index 直接置于4?
“mississippi” 中寻找 “issip”,怎么在遍历到 “issis” 时让 index 直接置于1?
首先重要的一个认识是:将 index 置于哪个位置和 haystack 是无关的,取决于 needle 本身的性质。这个性质就是匹配出错的那个元素之前的字符串序列的前后缀有多少是相同的。比如 abcabd 中,当d这个字符匹配出错时,它之前已经匹配成功的字符串序列为 abcab,匹配成功说明 haystack 数组中有这样一个序列,这时可以看出这个序列的尾部能直接作为新一轮匹配的头部,所以下一个字符的判断可以直接从c开始,即 index 置于2(注意此时原数组仍是从匹配出错位置的元素开始)。
而对于每一个出错的字符,根据其前面的字符串前后缀情况,都有一个对应的 index 位置,因此我们可以制作一个和每个位置对应的索引表,称为 next 表。构造 next 表的大体思路就是遍历 needle,在每个位置去判断前面(包括或不包括当前元素)的字符串的前后缀情况。
具体来看,如果是判断包含自身的字符串,则在主函数里当 haystack 和 needle 字符不匹配时,index 应置于 next[i - 1] 的位置。这种情况下的代码实现如下所示,理解其中的 while 循环很关键:在当下的后缀尾部( i 位置)与前缀尾部( j 位置)不匹配时,不是一口气回退到 needle 的最开头,而是回退到 next[j - 1] 的位置。这和主函数的思路是相同的:当整个字符串匹配不成功时,退而求其次地比较其后缀字串与 needle 的前缀子串。

void getNext(int* next, const string& s){
    int j = 0;
    next[0] = j;
    for(int i = 1; i < s.size(); i++) { 
        while (j >= 0 && s[i] != s[j]) {
            j = next[j - 1];
        }
        if (s[i] == s[j]) { 
            j++;
        }
        next[i] = j;
    }
}

C++语法及特性

  1. 定义一个子函数时,有两种方法,一种是在主函数声明变量,再将其指针传入子函数,而子函数的返回值为空,如 void getNext(int* next, const string& s); 另一种是直接在子函数里定义一个变量并以 return 的形式返回给主函数,如 vector<int> getNext(const string& s) 。 第一种方式省去了在子函数体里创建和销毁临时变量的开销。

总结

理解KMP算法花了不少功夫,next函数的构建也还没思考透彻,459. 重复的子字符串 留待周末再看吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值