代码随想录算法训练营第9天 | LeetCode28.找出字符串中第一个匹配项的下标、LeetCode459.重复的子字符串

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

给你两个字符串 haystackneedle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回  -1 。给你两个字符串 haystackneedle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回  -1 。 

思路:这道题有几种解题思路,暴力求解法 、辅助数组与双指针法,以及KMP方法

1、暴力求解法

暴力求解很容易让人理解,首先一层外循环遍历haystack字符串,并且当haystack[i]等于了needle的第一个元素,也就是说有匹配的可能性,于是就进入第二层循环,遍历needle字符串,这里我采用了while循环,这里注意一下的就是索引下标的问题,不管采用备份还是重赋值,都要保证其的可再用性,比如对于外面循环的i,如果第一次进入第二个循环出来后,没有备份直接使用i,已经与原始的i相差甚远了,所以我这里采用了备份的方式,定义了i_tmp变量,然后while循环结束,判断下标right的值是否为needle的最后一个元素,从而决定返回还是继续循环。

int strStr(string haystack, string needle) {
        if(haystack.size() < needle.size()){
            return -1;
        } //当haystack本身size小于needle的时候,直接返回-1
        for(int i = 0; i < haystack.size(); i ++){
            if(haystack[i] == needle[0]){
                //当haystack的元素与needle的第一个元素相等时
                //可能是我们要找的解,开始进入判断
                int i_tmp = i, right = 0;
                while(right < needle.size() && i_tmp < haystack.size()){
                    if(haystack[i_tmp] != needle[right]){
                        //出现不相等,直接跳出循环
                        break;
                    }
                    right ++;
                    i_tmp ++;
                }
                if(right == needle.size()){//当right等于了needle的size,说明找到了答案
                    return i;
                }
            }
        }
        return -1;//未找到
    }

时间复杂度:O(n*m)

空间复杂度:O(1)

 2、辅助数组与双指针法

这个方法使用了一个大小为26的record数组来首先存放needle中的字符串情况,然后进入left指针和right指针指向的时haystack字符串,进入while循环里面,预想是当record的所有元素都为0,并且right-left+1的长度等于needle的时候,此时有很大可能是包含的needle字串。

为什么是很大可能呢?因为上面求出来的是needle的异位词,也就是包含字符元素相同,但是位置顺序可能不同,所以此时还需要对顺序进行判断,检查每个元素是否和needle中元素顺序一样,如果一样,则返回结果;如果不一样,则重新进入循环,继续进行。

int strStr(string haystack, string needle) {
        int record[26] = {0}; //设置辅助数组
        for(int i = 0; i < needle.size(); i ++){
            record[needle[i] - 'a'] ++; //记录needle中的元素出现情况;
        }
        int left = 0, right = 0;  //设置双指针
        while(right < haystack.size()){
            record[haystack[right] - 'a'] --;  
            while(record[haystack[right]- 'a'] < 0){
                //当减后小于零,说明是haystack中的其他元素,
                //这时候为了保证record数组其他未出现的元素的位置为零,需要将其补齐
                record[haystack[left] - 'a'] ++;
                left ++;
            }
            if(right - left + 1 == needle.size()){
                //当来到这里,说明此时record数组中的元素全为零,
                //此时left和right区间里面的元素是needle中的元素,但是顺序不一定对
                int flag = 1, left_tmp = left;//这里设置标志位以及备份的left
                for(int i = 0; i < needle.size(); i ++, left_tmp ++){
                    if(needle[i] != haystack[left_tmp]){
                        //当haystack中的元素出现不等的情况时
                        //说明只是一个异位词,不满足题意,置flag为0
                        flag = 0;
                    }
                }
                if(flag) return left; //如果完全相同,则返回left的值即可
            }
            right ++;
        }
        return  -1; //没有满足题意的,返回-1
    }v

时间复杂度:O(n*m)

空间复杂度:O(1)

3、KMP方法

这是本题最关键的解法。

简单来说,KMP方法在匹配字符串类型的题目上面十分高效,有很多有关KMP的博客、资料等,这个方法其实刚开始理解起来很抽象,但是多理解理解就会发现其中的精妙之处。

这里我不准备大讲特讲KMP相关的内容,因为讲得再多还是得自己花时间思考,才能有所领悟。如果有需要可以自行检索一些详细资料查看。

我这里讲解的是为什么要用KMP,以及使用的一些注意事项

首先KMP方法中使用了一个next数组(也叫前缀表),用来存放当前元素为最后元素时所能够得到的最长相等前后缀长度,这也是本题为什么推荐使用KMP方法,因为可以在匹配到不相等的字符时,减少从头开始重新遍历needle字符串的开销 ,这也是为什么上面两种方法的时间复杂度会是n*m,m就是因为要重复遍历所增加的时间开销。

所以这道题使用KMP时的总体思路就是,首先创建一个next数组存放needle中各元素的相等最长前后缀长度,然后在进入循环,开始遍历haystack字符串,当遇到不相匹的元素时,通过next[i]跳转到相应的位置,减少从头开始遍历的开销,开始尝试匹配后面的字符串,当匹配成功时,即返回索引下标。

当然,对于求next数组(前缀表)的方式也大体分为两种类型,一种是初始时设next[0]=-1类型;还有一种是初始时设next[i]=0类型。下面简称为-1型和0型。思路是一样的,不过实现形式有点不同罢了,得注意不同类型的下标的变换。

1、-1型

//获取next前缀数组
    void getNext(int* next, string& s){
        int j = - 1;
        next[0] = j; //初始化j的值以及next[0]
        for(int i = 1; i < s.size(); i ++){
            while(j >= 0 && s[i] != s[j + 1]){
                j = next[j];  
                //当出现不相等时,进行回退
                //回退的目的就是找到能够与其拥有相同前后缀的元素
                //得到其相对于开始位置的偏移量,并且记录在next里,
                //如果直到回退到下标为0的地方依然没有,那么就置为-1。
                //这样的目的是能够在之后的两个字符串比较中遇到不相等的元素时
                //能够通过next[i]找到一个合适地方返回,而不是重新从头开始遍历needle
            }
            if(s[i] == s[j + 1]){
                j ++;
            }
            next[i] = j;
        }
    }
    int strStr(string haystack, string needle) {
        if(needle.size() == 0){
            return 0; 
            //这里是对needle为空时的判断,
            //当然也可以不写,不写也能过,因为不同人理解可能不同
        }
        vector next(needle.size()); //初始化next前缀数组为needle的大小
        getNext(&next[0], needle);  
        //因为函数第一个参数为指针,所以传递了next的一个元素的地址
        int j = -1; //j的初始值为-1,根据getNext函数统一
        for(int i = 0; i < haystack.size(); i ++){
            while(j >= 0 && haystack[i] != needle[j + 1]){
                j = next[j];
            }
            if(haystack[i] == needle[j + 1]){
                j ++;//遇到了相同元素,j开始增加,尝试之后判断后面的元素
            }
            if(j == needle.size() - 1){
                //当j等于了needle.size()-1,说明已经成功匹配了,
                //返回在haystack中的needle字符串开始元素的下标,
                return i - needle.size() + 1;
            }
        }
        return -1;//没有找到匹配
    }

2、0型

 void getNext(int* next, string& s ){
        int j = 0;
        next[0] = 0; //这里初始化为了0
        for(int i = 1; i < s.size() - 1; i ++){
            while( j > 0 && s[i] != s[j]){
                j = next[j - 1];   //这里要保证s[j - 1]有意义
            }
            if(s[i] == s[j]){//这里有变化
                j ++;
            }
            next[i] = j;
        }
    }
    int strStr(string haystack, string needle) {
        vector next(needle.size());
        getNext(&next[0], needle);
        int j = 0; //这里初始化为了0
        for(int i = 0; i < haystack.size(); i ++){
            while(j > 0 && haystack[i] != needle[j]){
                j = next[j - 1];
            }
            if(haystack[i] == needle[j]){
                j ++;
            }
            if(j == needle.size()){ //注意这里的变化
                return i - needle.size() + 1;
            }
        }
        return -1;
    }

时间复杂度:O(n+m)

空间复杂度:O(m)

LeetCode459.重复的子字符串

给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。 

思路:这道题有两种解题方法,一种是移动匹配方法,还有一种是使用刚才讲的KMP方法。

1、移动匹配法

这道题思路是这样的,如果说一个字符串满足题意,由某个字串循环n次得来,那么当将两个这样的字符传拼接在一起,比如需要验证的字符串为s,现在把它拼为s+s,那么假设s是循环两次得来,那么可以知道,在s+s中间,必然有一个字符串和s一摸一样,也就是前一个s的后半段,后一个s的前半段,如果满足这个条件,那么该字符串就是满足题意的。

但是需要注意的是,得将拼接成的s+s字符串的首和尾元素去掉,否则在判断的时候会混淆其为原来的s(前面或后面原本的s),使得判断失误。

   bool repeatedSubstringPattern(string s) {
        string t = s + s;
        t.erase(t.begin());
        t.erase(t.end() - 1); //移除首尾元素
        if(t.find(s) != std::string::npos){ 
            //std::string::npos是string里的一个静态常量
            //表示“未找到”
            return true;
        }else{
            return false;
        }
    }

时间复杂度:O(n+m)(这是因为使用了库函数造成的一般时间开销)

空间复杂度:O(1)

2、KMP方法

这道题同样还是可以使用KMP。需要使用KMP中的next数组。

简单来说,就是当一个字符串满足题意的时候,可以画图得到,这个字符串的总长度与该字符串的最长的前缀或者最长的后缀的长度的差值为循环子串的长度(画图很容易理解,只要这个字符串是通过某个子串往复几次得到的)。

在KMP中的next数组本身就是存放以某个元素为最后元素时的最长的相等前后缀长度,于是只要实现求next的方法即可。显然,要验证的字符串的最后一个元素的next值不能为-1(或0,取决于选择哪种next方法的实现方式),否则根本就不存在子串的往复,因为根本没有相等的前后缀。这是判断的第一步,当next不为-1(或者0)时,只是能够确定其确实存在相等的最长前后缀,但是还需要一个判断,那就是总长度与单个子串的长度是否可以整除,具体实现也就是看总长度%单个子串的长度是否为0,因为存在最后一个元素的next所对应的值不为-1(或者0),于是当计算其差值从而将此作为某个子串时,会出现长度取模后并不为0的情况(比如字符串abcdab)。

因此当求得待验证的字符串的next数组值后,进行next最后一个元素值得判断以及长度是否匹配,即可得到该字符串是否是由某个子串往复得来的。

1、-1型

//获取next数组
    void getNext(int* next, string& s){
        int j = -1;
        next[0] = j;
        for(int i = 1; i < s.size(); i ++){
            while(j >= 0 && s[i] != s[j + 1]){
                j = next[j];
            }
            if(s[i] == s[j + 1]){
                j ++;
            }
            next[i] = j;
        }
    }
    bool repeatedSubstringPattern(string s) {
        int next[s.size()];
        getNext(&next[0], s); //获取next数组
        int len = next[s.size() - 1] + 1;//获取字符串最大相同前后缀长度
        int strlen = s.size(); //获取字符串长度
        if(next[s.size() - 1] != -1 && strlen % (strlen - len) == 0) return true;
        //这里next[s.size()-1]如果等于了-1,
        //那么说明这个字符串不存在最长的相等前后缀,因此可以直到该字符串不是某子串循环得到
        //strlen % (strlen - len)==0的判断是因为它的最长相等前后缀存在
        //想要验证其总长度是否能整除其应该为循环子串的长度
        return false;
    }

2、0型

    //获取next数组
    void getNext(int* next, 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;
        }
    }
    bool repeatedSubstringPattern(string s) {
        if(s.size() == 0) return false; //当s字符串为空时,返回false
        int next[s.size()];
        getNext(next, s);
        int strlen = s.size();
        int len = next[s.size() - 1]; //此时不需要加1,因为初始为0
        if(next[s.size() - 1] != 0 && strlen % (strlen - len) == 0) return true;
        return false;
    }

时间复杂度:O(n)(主要开销是构建next数组)

空间复杂度:O(n)(主要开销是构建next数组)

 希望我的文章能够帮助到你,如果有帮助,希望能够点赞加收藏,再点点关注,非常感谢!

你可能感兴趣的:(算法,数据结构)