【字符串算法题记录】28. 找出字符串中第一个匹配项的下标,459.重复的子字符串——KMP算法

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

题目链接

暴力求解

这一题蛮简单,但是逻辑上容易忽略一些小细节。我一开始想的是从haystack数组的头开始找与needle匹配的第一个字符,然后再顺着看下面连续的字符是否匹配,然后直接返回结果,但是忽略了很重要的一点:haystack中可能有多个字符和needle的第一个字符是一样的,那么我们不能一次匹配就返回判断结果,而是要遍历整个haystack进行判断

class Solution {
    public:
    int strStr(string haystack, string needle) {
        int j = 0;
        int index = -1;
        for(int i = 0; i < haystack.size(); i++) { 
            if(haystack[i] == needle[j]) {  // 首先在haystack中找到needle的第一个字符
                index = i;  // 并记录第一个匹配字符的下标index
                // 进入循环检查是否接下来每一个字符都是与needle匹配的
                while(haystack[i] == needle[j] && j < needle.size() && i < haystack.size()) {
                    i++;
                    j++;
                }
                if(j == needle.size()) return index; // 如果needle中的字符全检查完了,则是匹配的,返回对应的index
                else return -1; // 如果needle中的字符没有检查完,即不匹配,则返回-1
            }
        }
        return index; // for循环已经结束都没有返回的话,说明haystack中没有匹配字符,则返回-1
    }
};

第二次修改后的如下,但是还是错了,因为我直接用外层的for循环的变量i去做字符串匹配检查,这其中包含了i++指令,更改了i的值,那么会影响到最外层的for循环

class Solution {
    public:
    int strStr(string haystack, string needle) {
        int index = -1; // index默认值是-1
        for(int i = 0; i < haystack.size(); i++) { 
            int j = 0;
            if(haystack[i] == needle[j]) {  // 首先在haystack中找到与needle匹配的第一个字符
                index = i;  // 并记录第一个匹配字符的下标index
                // 进入循环检查是否接下来每一个字符都是与needle匹配的
                while(haystack[i] == needle[j] && j < needle.size() && i < haystack.size()) {
                    i++;
                    j++; 
                }
                if(j == needle.size()) return index; // 如果needle中的字符全检查完了,则是匹配的,返回对应的index
                else index = -1; // 如果needle中的字符没有检查完,即不匹配,index要恢复默认值
            }
        }
        return index; // for循环已经结束都没有返回的话,说明haystack中没有匹配字符,则返回-1
    }
};

最终正确代码如下:

class Solution {
    public:
    int strStr(string haystack, string needle) {
        int index = -1; // index默认值是-1
        for(int i = 0; i < haystack.size(); i++) { 
            int j = 0;
            if(haystack[i] == needle[j]) {  // 首先在haystack中找到与needle匹配的第一个字符
                index = i;  // 并记录第一个匹配字符的下标index
                int k = i;  // 用k来代替i进行接下来的检查(不然i值被改变会影响最外层的for循环)
                
                // 进入循环检查是否接下来每一个字符都是与needle匹配的
                while(haystack[k] == needle[j] && j < needle.size() && k < haystack.size()) {
                    k++;
                    j++; 
                }
                if(j == needle.size()) return index; // 如果needle中的字符全检查完了,则是匹配的,返回对应的index
                else index = -1; // 如果needle中的字符没有检查完,即不匹配,index要恢复默认值
            }
        }
        return index; // for循环已经结束都没有返回的话,说明haystack中没有匹配字符,则返回-1
    }
};

KMP算法

上面的暴力求解算法不难看出时间复杂度是O(n × m),而KMP算法可以将效率提高至O(n + m)。那么什么是KMP算法呢?
他的核心思想在于定义一个模式串的前缀表,然后在目标串中依次比对,碰到不匹配的地方,则回退到模式串此处的相同前缀处继续比对,就不必再退回到模式串的开始而从头再来。文字可能比较难以理解,大家可以去看代码随想录中的示例图,非常清晰明了。

首先我们构造模式串的前缀表,注意这里构造的形式不是固定的,有的前缀表初始值为0,有的为-1(整体向右移),看需求与习惯来决定如何构造,但是核心方法是一样的:

  • 初始值为0
void getNext(int* next, string& s) {
        int j = 0;
        next[0] = j; // next初始值为0
        for(int i = 1; i < s.size(); i++) {
            while(j >= 1 && s[i]!=s[j]) // 如果当前前后缀不同,j就要退回到与当前s[i]相等的前缀上
                j = next[j-1];
            if(s[i]==s[j]) j++; // 如果当前前后缀相同,则j向前一位,即指到j+1上
            next[i] = j;    // 将当前前缀的长度赋予next[i]
        }
    }
  • 初始值为-1
void getNext(int* next, string& s) {
        int j = -1;
        next[0] = j; // next初始值为-1
        for(int i = 1; i < s.size(); i++) {
            while(j >= 0 && s[i]!=s[j+1]) // 如果当前前后缀不同,j就要退回到与当前s[i]相等的前缀上
                j = next[j];
            if(s[i]==s[j+1]) j++; // 如果当前前后缀相同,则j向前一位,即指到j+1上
            next[i] = j;    // 将当前前缀的长度赋予next[i]
        }
    }

然后再利用next数组来匹配。对于初始值为0的前缀表,我们碰到不匹配的地方是跳到next[j-1]处匹配,而初始值为-1的前缀表,我们是跳到next[j]处。

  • 初始值为0
int strStr(string haystack, string needle) {
        int* next = new int[needle.size()]; // 初始化next数组
        getNext(next, needle);
        int j = 0; // next数组起始位置是0
        for(int i = 0; i < haystack.size(); i++) {
            while(j >= 1 && haystack[i] != needle[j]) {   // 如果匹配到不相等的,j就要跳回到相等前缀处,即next[j-1]
                j = next[j-1];
            }
            if(haystack[i] == needle[j]) {
                j++;
            }
            if(j == (needle.size())) {
                delete[] next;
                return (i-needle.size()+1);
            }
        }
        delete[] next;
        return -1;
  • 初始值为-1
int strStr(string haystack, string needle) {
        int* next = new int[needle.size()]; // 初始化next数组
        getNext(next, needle);
        int j = 0; // next数组起始位置是-1
        for(int i = 0; i < haystack.size(); i++) {
            while(j >= 1 && haystack[i] != needle[j+1]) {   // 如果匹配到不相等的,j就要跳回到相等前缀处,即next[j]
                j = next[j];
            }
            if(haystack[i] == needle[j+1]) {
                j++;
            }
            if(j == (needle.size() - 1)) {
                delete[] next;
                return (i-needle.size()+1);
            }
        }
        delete[] next;
        return -1;

459.重复的子字符串

题目链接

暴力求解

class Solution {
public:
    bool repeatedSubstringPattern(string s) {
        int fast = 1;
        while(fast < s.size()) {    // 遍历快指针
            if(s[fast] == s[0]) {    // 找到与s[0]相同的字符
                int i = 0;
                int j = fast;
                while(s[i] == s[j] && j < s.size()) {   // 检查他们对应位置的字符是否一样
                    i++;
                    j++;
                }
                if(j == s.size() && i % fast == 0) return true;  // 如果检查到字符串末尾都是一样的,则是重复子串,返回true
            }
            fast++; // // 如果没有检查到末尾,则还没有找到子串,fast继续向后找
        }
        return false;
    }
};

KMP算法

假设重复子串的长度为 x x x,那么整个字符串的长度为 n × x n\times x n×x,最长相等前后缀长度则为 m × x m\times x m×x,而字符串减去最长相等前后缀即是重复子串: m x − n x = x mx-nx=x mxnx=x。所以有: m x % ( m x − n x ) = 0 mx \% (mx-nx) = 0 mx%(mxnx)=0

class Solution {
public:
    void getNext(int* next, string& s) {
        int j = -1;
        next[0] = j;   // 首先对next数组赋初值:s的第一个字符必定是没有相等前后缀的
        for(int i = 1; i < s.size(); i++) { // 从s的第二个字符开始看
            while(j >= 0 && s[i]!=s[j+1]) { // 利用while循环找到与s[i]相等的前缀
                j = next[j];
            }
            if(s[i]==s[j+1]) j++; // 如果找到了相等前缀,就要让j补1(因为此处的前缀表是统一减1的)
            next[i] = j;
        }
    }

    bool repeatedSubstringPattern(string s) {
        int len = s.size();
        if (!len) return false;  // 如果s本身就是空串,则直接返回false
        
        int next[len];  // 构造前缀表
        getNext(next, s);
        
        if (next[len-1]!=-1 && len % (len - (next[len-1] + 1)) == 0) {
            return true;
        }
        return false;
    }
};
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值