代码随想录算法训练营day09 | 28. 实现 strStr() 459.重复的子字符串

97 阅读9分钟

28. 实现 strStr()

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

28. 找出字符串中第一个匹配项的下标 - 力扣(Leetcode)

思路

思路1:Sunday算法

  • 给定目标字符串haystack,模式字符串needle
  • 遍历haystack,使用index标记当前位置
  • 从 目标字符串中 提取 待匹配字符串与 模式串 进行匹配,待匹配字符串haystack中下标为[ index, index + needle.length() )的子串,
    • 若匹配成功,则返回index
    • 否则,判断 待匹配字符串 的下一个字符k是否包含在needle
      • 若包含,则移动到匹配字符串 的下一个字符kneedle的最后一个k相重合的位置,此时移动的步数最小,移动的步数为index = index + 偏移表[k]
      • 否则,则index移动到k的后面。
      • 原因:当前不匹配,index需要向后移动,移动[ 1, needle.length() )步,待匹配字符串中必然都包含k,若k不在needle中,则必然会匹配失败,因此此时可以直接移动needle.length()步,令匹配字符串中不包含k这个不包含在模式串中的字符

实现 strStr()-Sunday算法.jpg

思路2:KMP算法

KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。

当不匹配部分出现时,模式串可以分为4部分,不匹配部分作为第 4 部分存在,前面已匹配部分分成最长前缀、前缀后缀之间部分和最长后缀,见图。
同样地,待匹配字符串也可以划分为这 4 部分,待匹配字符串中的前两部分(图中红色的AB)失去成为目标字符串的可能性,由于

  • 最长前缀和最长后缀相同,待匹配字符串的最长后缀(图中蓝色的C)和模式串的最长前缀(图中红色的A)相同,
  • 模式串的不匹配部分(图中红色的D)和模式串的前缀后缀之间部分(图中红色的B)不相同,
  • 待匹配字符串的不匹配部分(图中蓝色的D)和模式串的不匹配部分(图中红色的D)不相同,
    因此,待匹配字符串的不匹配部分(图中蓝色的D)与模式串的前缀后缀之间部分(图中红色的B)可能相同,可以从这里向后继续进行匹配

实现 strStr()-KMP算法.jpg

使用前缀表next存储当模式串某个字符不匹配时,回退到模式串哪个位置继续进行匹配的信息。

  • needle[0,i]最长公共前后缀:字符串needle下标区间为[0,i]的子串中,由其所有前缀和所有后缀构成的交集中,集合中的字符串既是前缀,也是后缀,相当于前缀和后缀的公共部分,所以称其为公共前后缀。而公共前后缀中长度最长的字符串,称为最长公共前后缀。
  • 前缀:不包含最后一个字符的所有以第一个字符开头的连续子串
  • 后缀:不包含第一个字符的所有以最后一个字符结尾的连续子串
  • next[i]:needle[0,i]的最长公共前后缀的长度。
    • 由上图可知,当发生字符串不匹配时,文本串的指针处于不匹配部分的位置(图中蓝色的D),我们要将模式串的指针移到前缀后缀之间的位置(图中红色的B),并从此位置进行匹配。
    • 模式串前缀后缀之间的位置(图中红色的B)位于最长前缀的后面,由于数组下标从 0 算起,所以红色的 B 的第一个字符的下标为最长前缀(图中红色的A)的长度。
    • next[i]计算时,使用 pre来标识当前最长前缀的末尾位置,而i相当于后缀的标识。
      • pre的作用:从needle下标为 0 时遍历,前缀为 [0 , pre]
      • next[i] = next[pre]时,意味着i的位置可以形成一个与[0, pre]相同的字符串,即与[0, pre]前缀相对应的后缀,当前位置的最长公共前后缀的长度即为 字符串 [0, pre] 的长度,即为 pre+1pre + 1
      • next[i] != next[pre]时,意味着当前字符串的最长前缀不会是 [0, pre],而且只会比[0, pre]短, pre要回退到字符串 [0, pre-1]的最长前缀的后一个位置,即 next[pre]的位置,并重复该匹配,直到找到与 next[i]相等的 next[pre],或者匹配位置回退到 下标 0 的位置。
    • next前缀表的计算步骤如下:
      1. 初始化:next[0]00needle的匹配从下标 11开始,设置pre = 0;
      2. 在每一次循环中,一旦 needle.charAt(pre)!=needle.charAt(i) 成立,就将 pre 回退到字符串 [0 , pre-1]的最长前缀的后面,即 next[pre-1] 的位置 ,直到条件不成立, 或者pre == 0
      3. 在回退的循环结束后,判断回退循环的条件needle.charAt(pre)!=needle.charAt(i) 是否成立,若成立,则 当前所求的 next[i]为 当前字符串 [0, pre]的长度,即 pre+1pre + 1。这也意味着当前字符已匹配,在下一轮循环中要匹配的是下一个字符,因此令pre++
      4. 若回退循环的条件不成立,则当前 pre == 0,也就是该字符与模式串needle的第一个字符也不匹配,则最长公共前后缀的长度为 0 ,设置next[i]=0 即可,在下一轮的匹配要匹配的字符也是 needle的第一个字符。

代码

Sunday算法,代码如下:

class Solution {
    public int strStr(String haystack, String needle) {
        int[] map=new int[26];

        // 初始化map
        for(int i=0;i<map.length;i++){
            map[i]=needle.length()+1;
        }

        // 偏移表
        for(int i=0;i<needle.length();i++){
            map[needle.charAt(i)-'a']=needle.length()-i;
        }

        for(int i=0;i<=haystack.length()-needle.length();){
            if(haystack.subSequence(i,i+needle.length()).equals(needle)){
                return i;
            }

            // 加判断,避免越界
            if(i+needle.length()<haystack.length()){
                i+=map[haystack.charAt(i+needle.length())-'a'];
            }else{
                break;
            }
        }

        return -1;
    }
}

KMP算法,代码如下:

class Solution {
    public int strStr(String haystack, String needle) {
        int[] next=new int[needle.length()];

        getNext(needle,next);

        // 指向 needle
        int index=0;

        for(int i=0;i<haystack.length();i++){
            while(index>0 && haystack.charAt(i)!=needle.charAt(index)){
                // 让needle回退 , 产生两种结果
                // 1. index !=0 , 下标为 index 的字符 与 当前字符相等
                // 2. index == 0 ,是否相等需要后续判断
                index=next[index-1];
            }

            if(needle.charAt(index)==haystack.charAt(i)){
                // needle.charAt(index) 与 haystack.charAt(i)匹配
                // 设置 haystack串的 i+1 与 needle的 index+1 进行匹配
                index++;
            }

            if(index==needle.length()){
                // 当 index == needle.length() 时
                // needle已完全匹配
                return i+1-needle.length();
            }
            
        }

        return -1;
    }

    private void getNext(String needle,int[] next){

        next[0]=0;
        int pre=0;

        for(int i=1;i<needle.length();i++){

            while(pre>0&&needle.charAt(pre)!=needle.charAt(i)){
                pre=next[pre-1];
            }

            if(needle.charAt(i)==needle.charAt(pre)){
                next[i]=++pre;
                continue;
            }

            next[i]=pre;
        }
    }
}

459.重复的子字符串

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

459. 重复的子字符串 - 力扣(Leetcode)

思路

思路1
如果一个字符串由重复的子字符串构成,那么当两个相同的字符串拼接在一起,第一个字符串的后半部分和第二个字符串的前半部分也会构成1个相同的字符串。
因此,将两个字符串拼接在一起,然后去除新的字符串的首尾两个字符(避免对之后的判断造成干扰),然后再判断该字符串中是否仍含有此字符串。
若结果为真,则该字符串由重复的子字符串构成;否则,该字符串不是由重复的子字符串构成的。

这个方法的时间复杂度取决于判断新字符串中是否还含有原字符串,可以在此处使用 java 提供的api,也可以使用 kmp算法。

注:

  • StringBuilderindexOf() 方法调用了 StringindexOf() 方法。
  • StringindexOf()的底层逻辑:
    • 先做一系列的参数的判断,确保参数有效;
    • 遍历源字符串,
    • 在源字符串中找到模式串第一个字符的位置,设为m
    • 逐一匹配模式串与源字符串m起始后面对应位置的字符,
    • 若各字符一一匹配,则找到了结果m,返回结果;
    • 否则,这次匹配失败,继续循环。

思路2 对于字符串 s: “S1 S2 S3 …… Sk-1 Sk S1 …… Sk”,由 n 个最小重复子串 "S1 S2 S3 …… Sk-1" 组成,那么对于s来说,其最长公共前后缀为 n-1 个 “S1 S2 S3 …… Sk-1 Sk”。

  • s的前缀需要去除 最后一个字符 S1 , 必须以 S1 开头
  • s的后缀需要去除 第一个字符 Sk , 必须以 Sk 结尾
  • 当其前缀和后缀相等时,前缀中不能以第一个S1开头 ,后缀不能以最后一个Sk结尾,而前缀必须以 S1 开头,后缀必须以 Sk 结尾;因此,在s中,从末尾从 Sk 一直删除到 S1 ,得到 由 n-1 个 “S1 S2 S3 …… Sk-1 Sk”组成的字符串,该字符串满足s后缀的条件,即这个字符串就是 s 的最长公共(相同)前后缀。
  • s 的最长公共前后缀 比 s1 个最小重复子串,而s 的最长公共前后缀的长度就是s 字符串最后一个字符的前缀表的值,即 next[s.length()-1],,因此 最小重复子串的长度为s.length()-next[s.length-1]
  • 如果最长公共前后缀的长度为 0 ,这意味着不存在最小重复子串,即无法通过重复子串构成字符串,因此,由多个重复子串构成的字符串满足条件:next[s.length()-1] != 0
  • 如果 s 是由多个最小重复子串构成的,则 s的长度是最小重复子串的长度的整数倍,即 s.length() % (s.length()-next[s.length-1]) ==0
  • 通过判断上述两个条件是否成立,即可判断字符串 s 是否由重复子串构成,若是,则该子串为 s[0, s.length()-next[s.length-1] -1 ]

代码

思路1,使用java提供的api 的方法,代码如下:

class Solution {
    public boolean repeatedSubstringPattern(String s) {

        StringBuilder sb=new StringBuilder();

        sb.append(s);
        sb.append(s);

        sb.deleteCharAt(0);
        sb.deleteCharAt(sb.length()-1);

        return sb.indexOf(s)!=-1?true:false;
    }
}

思路1,使用 kmp 算法的方法:

class Solution {
    public boolean repeatedSubstringPattern(String s) {

        StringBuilder sb=new StringBuilder();

        sb.append(s);
        sb.append(s);

        sb.deleteCharAt(0);
        sb.deleteCharAt(sb.length()-1);

        return strStr(sb.toString(),s)!=-1?true:false;
    }

    private int strStr(String haystack, String needle) {
        int[] next=new int[needle.length()];

        getNext(needle,next);

        // 指向 needle
        int index=0;

        for(int i=0;i<haystack.length();i++){
            while(index>0 && haystack.charAt(i)!=needle.charAt(index)){
                // 让needle回退 , 产生两种结果
                // 1. index !=0 , 下标为 index 的字符 与 当前字符相等
                // 2. index == 0 ,是否相等需要后续判断
                index=next[index-1];
            }

            if(needle.charAt(index)==haystack.charAt(i)){
                // needle.charAt(index) 与 haystack.charAt(i)匹配
                // 设置 haystack串的 i+1 与 needle的 index+1 进行匹配
                index++;
            }

            if(index==needle.length()){
                // 当 index == needle.length() 时
                // needle已完全匹配
                return i+1-needle.length();
            }
            
        }

        return -1;
    }

    private void getNext(String needle,int[] next){

        next[0]=0;
        int pre=0;

        for(int i=1;i<needle.length();i++){

            while(pre>0&&needle.charAt(pre)!=needle.charAt(i)){
                pre=next[pre-1];
            }

            if(needle.charAt(i)==needle.charAt(pre)){
                next[i]=++pre;
                continue;
            }

            next[i]=pre;
        }
    }
}

思路2的代码如下:

class Solution {
    public boolean repeatedSubstringPattern(String s) {

        int[] next=new int[s.length()];

        getNext(s,next);

        int lenRepeatedStr = s.length() - next[s.length() - 1];

        return next[s.length() - 1]!=0 && s.length() % lenRepeatedStr ==0 ? true : false;

    }

    private void getNext(String needle,int[] next){

        int pre=0;
        next[0]=0;

        for(int i=1;i<needle.length();i++){
            
            // 回退到 pre == 0 或 needle.charAt(i) == needle.charAt(pre)
            while(pre > 0 && needle.charAt(i)!=needle.charAt(pre)){
                pre=next[pre-1];
            }

            if(needle.charAt(i)==needle.charAt(pre)){
                next[i]=++pre;
                continue;
            }

            next[i]=pre;
        }
    }
}