代码随想录——字符串

目录

一、反转字符串

1、题目:344. 反转字符串 - 力扣(LeetCode)

2、思路

3、代码

1)用temp来交换数值

2)通过位运算来交换数值

4、复杂度分析

二、反转字符串2

1、题目:541. 反转字符串 II - 力扣(LeetCode)

2、思路

3、代码

4、复杂度分析

三、替换数字

1、题目:54. 替换数字(第八期模拟笔试) (kamacoder.com)

2、思路

3、代码

4、复杂度分析

四、翻转字符串里的单词

1、题目:151. 反转字符串中的单词 - 力扣(LeetCode)

2、思路

3、代码

4、复杂度分析

五、右旋转字符串

1、题目:55. 右旋字符串(第八期模拟笔试) (kamacoder.com)

2、思路

3、代码

4、复杂度分析

六、实现strStr()、Java中是indexOf()

1、题目:28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)

2、思想(KMP)

1)KMP

2)借助KMP做匹配

3、代码

1)next数组右移一位

2)next数组就是原始前缀表

4、复杂度分析

七、重复的子字符串

1、题目:459. 重复的子字符串 - 力扣(LeetCode)

2、思路

1)移动匹配

2)KMP

3、代码

4、复杂度分析


在思路上,其实字符串和数组是差不多的。但是具体是线上不同的编程语言实现起来字符串还是不太一样。

一、反转字符串

1、题目:344. 反转字符串 - 力扣(LeetCode)

输入一个字符串数组,返回反转后的字符串。要求不能额外用一个数组,要原地修改。

输入:["h","e","l","l","o"];输出:["o","l","l","e","h"]

2、思路

视频课:字符串基础操作! | LeetCode:344.反转字符串_哔哩哔哩_bilibili

类似前面的反转链表,用双指针的方法。但是这里字符串是存在数组里面的,所以地址空间连续,这点和链表不同。(链表是prev和cur指针为前一个和当前的,然后依次修改指向)

只用像下图一样,定义两个指针,一个在头一个在尾,然后依次交换,两个指针都向中间移动。

3、代码

1)用temp来交换数值

class Solution {
    public void reverseString(char[] s) {
        int l = 0;  // l指针从初始位置下下标为0开始
        int r = s.length - 1;  // r指针从末尾开始
        while(l < r){   // 直到r和l相遇的时候就结束
            char temp = s[l];  // 定义一个temp来临时存放字符串
            s[l] = s[r];  // 交换字符串
            s[r] = temp;
            l++;   // l右移,r左移
            r--;
        }
    }
}

2)通过位运算来交换数值

使用异或运算符^来交换s[l]s[r]的值。异或运算的一个特性是,如果两个比特位相同则结果为0,不同则结果为1。比如说对一个数进行两次异或运算,就会得回原来的值。

  1. s[l] ^= s[r]是将结果赋给s[l],此时s[l]s[r]的值互换了。
  2. s[r] ^= s[l];:再次使用异或运算,这次是将s[l]的值(现在它已经是s[r]原来的值)与s[r]进行异或运算,并将结果赋给s[r]。由于s[l]现在是s[r]原来的值,这一步实际上是将s[r]的值清零。

  3. s[l] ^= s[r];:最后,将s[l]s[r](现在为0)进行异或运算,将s[l]恢复为s[r]原来的值,完成交换。

比如A = 1010(二进制)、B = 1100(二进制)想要交换a和b的值,而不使用任何额外的存储空间

A=  A ^ B=0110;  B= B ^ A = 1010; A= A ^B = 1100   这样就实现了交换

class Solution {
    public void reverseString(char[] s) {
        int l = 0;
        int r = s.length - 1;
        while (l < r) {
            s[l] ^= s[r];  //s[l] = s[l]^ s[r]  构造 a ^ b 的结果,并放在 a 中  即
            s[r] ^= s[l];  //将 a ^ b 这一结果再 ^ b ,存入b中,此时 b = a, a = a ^ b
            s[l] ^= s[r];  //a ^ b 的结果再 ^ a ,存入 a 中,此时 b = a, a = b 完成交换
            l++;
            r--;
        }
    }
}

4、复杂度分析

  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

二、反转字符串2

1、题目:541. 反转字符串 II - 力扣(LeetCode)

比如输入: s = "abcdefg", k = 2,那就每往后计数2k个,就交换前面的字符串。

所以输出: "bacdfeg"  尾巴要是不足k个,那么也单独进行交换

2、思路

视频课:字符串操作进阶! | LeetCode:541. 反转字符串II_哔哩哔哩_bilibili

遍历字符串的时候每次移动2k步,即 i += (2 * k)。即每次要交换的的字符串是多少个,即每次要执行几次交换。

关注到每一步下。目标是确定每一步是交换起始start和结尾end起始点就跟着 i 走,结尾点是start+k-1的位置。然后每个起始和结尾之间的交换方式就还是普通的形式。

然后注意结尾的部分,所以end不能总是为strat+k-1,还要判断剩余的是否比总长度短。尾巴可能就要单独进行交换,即不再是k个进行交换。

3、代码

一般做算法题的时候还是不使用库函数reverse。

比如说长度为7,k=2,则end初始为1,

  1. 然后下面即第一步是反转下标为0-1的数。
  2. 然后到第二步。start=2,end=min(6,3),反转1次,即反转下标为2-3的数。
  3. 第三步。start=4,end=min(6,5),反转1次,反转下标为4,5的数。
  4. 第四部,start=6,end=min(6,6),start=end。就不执行反转了。

加入是每隔4个反转。那么到最后可能是start=4,end=7,那最后就是4-7这一段进行反转,所以并不是末尾长度不够k就不反转了,末尾剩多少个就单独进行反转。

class Solution {
    public String reverseStr(String s, int k) {
        char[] ch = s.toCharArray();
        for(int i = 0; i < ch.length; i += 2 * k){  //每次移动2k步
            int start = i;  // i表示交换的起始位置
            //这里是判断尾数够不够k个来取决end指针的位置
            int end = Math.min(ch.length - 1, start + k - 1);  
            //用异或运算反转 
            while(start < end){  //
                ch[start] ^= ch[end];
                ch[end] ^= ch[start];
                ch[start] ^= ch[end];
                start++;
                end--;
            }
        }
        return new String(ch);
    }
}

当然也可以用temp来进行元素的交换。

4、复杂度分析

使用库函数reverse和不使用的复杂度一样。

  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

三、替换数字

1、题目:54. 替换数字(第八期模拟笔试) (kamacoder.com)

给定一个字符串里面包含数字和小写字母。要实现把所有数字替换为number

输入:a1b2c3;输出:anumberbnumbercnumber

2、思路

java里面的string不能修改,所以必须要用辅助空间。

步骤1:确定新数组的长度,为原本是替换成number之后的大小

步骤2:用双指针法。初始i指向新数组的末尾,j指向旧数组的末尾。从后向前遍历依次赋值。

步骤3:从旧数组从后向前遍历,开始赋值到新数组,遇到数字,就从后向前填充number。

为什么要从后向前?因为如果不是用的java,就会直接在原数组上面扩充长度,尾部都是空的,前面是原数组元素。从后向前填充依次覆盖。所以如果从前向后填充的话,每次填充就都要移动原本的元素,这样复杂度就是O(n^2)了。就像下面这个图一样:

3、代码

这里是java的代码,因为java中不能修改String,所以就要重新定义一个数组newS

但是在下面实际实现的时候,是仍然把旧的s放到newS的头部了,其实没必要这样,这么做只是为了贴合前面思路介绍的时候所介绍的方法。

import java.util.Scanner;

public class Main {
    
    public static String replaceNumber(String s) {
        int count = 0; // 统计数字的个数
        int sOldSize = s.length();
        for (int i = 0; i < s.length(); i++) {
            if(Character.isDigit(s.charAt(i))){
                count++;   // count用来统计数字的个数
            }
        }
        // 定义一个新数组,长度为原本length+数字长度*5
        char[] newS = new char[s.length() + count * 5];  // java要重新定义一个数组
        int sNewSize = newS.length;
        // 将旧字符串的内容填入新数组(其实是可以没有这个步骤的,因为java是用了新数组)
        // 这里只是为了更贴合原本的思路解法
        System.arraycopy(s.toCharArray(), 0, newS, 0, sOldSize);
        // 从后向前遍历,i初始在新数组末尾,j初始在旧数组末尾
        for (int i = sNewSize - 1, j = sOldSize - 1; j < i; j--, i--) {
            if (!Character.isDigit(newS[j])) {  //如果不是数字就直接赋值
                newS[i] = newS[j];
            } else {  // 如果是数字,就把新数组对应位置从后往前进行number的填充
                newS[i] = 'r';
                newS[i - 1] = 'e';
                newS[i - 2] = 'b';
                newS[i - 3] = 'm';
                newS[i - 4] = 'u';
                newS[i - 5] = 'n';
                i -= 5;   // i就要向前移动5格
            }
        }
        return new String(newS);
    };
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String s = scanner.next();
        System.out.println(replaceNumber(s));
        scanner.close();
    }
}

4、复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

四、翻转字符串里的单词

1、题目:151. 反转字符串中的单词 - 力扣(LeetCode)

输入一个字符串,将其中非空格的单词,反转过来,相当于把一句话反着说

输入: "the sky is blue"    ;输出: "blue is sky the"

将反转后的字符串规范化,开头没有空格,每个单词之间只能有1个空格。

2、思路

如果用库函数的话,可以直接用库函数分割单词,然后定义一个新的string放进去,但是做算法题一般不会直接这么用库函数,况且这样空间复杂度也不会O(1)。

第一步:将整个字符串都翻转过来

第二步:再对每个单词进行翻转。

因为在java中不能对String进行修改。所以一开始就要新建一个数组,把String放入这个数组

然后用快慢指针指向数组起始位置。fast指针用于主要遍历原始数组,如果找到了一个字母开头,就给slow指针后面加一个空格再同时后移fast和slow把这个单词放进去,直到fast又到了空格的位置单词结束,slow就停止移动。然后就fast再接着往后寻找单词。

3、代码

视频课:字符串复杂操作拿捏了! | LeetCode:151.翻转字符串里的单词_哔哩哔哩_bilibili

去空格的方法:

  1. 步骤1:初始slow和fast都从位置0开始。
  2. 步骤2:主要是令fast指针遍历字符数组。当fast指向一个非空字符的时候,说明遇到了单词
  3. 步骤3:此时,如果slow不在最开始,就给slow的位置加一个空格,并且slow右移一位。
  4. 步骤4:然后同时右移fast和slow,直到fast遇到了空格,即fast到了单词的结尾,就停止移动slow
  5. 步骤5:进入下一个循环,继续向右移动fast,寻找下一个非空的字符,即下一个单词的起始。如果遇到了,就才会再给slow后面加一个空格,然后slow右移到这个单词结尾。
class Solution {
    //用 char[] 来实现 String 的 removeExtraSpaces,reverse 操作
    public String reverseWords(String s) {
        char[] chars = s.toCharArray();  // 基于以下三个函数来实现
        //1.去除首尾以及中间多余空格
        chars = removeExtraSpaces(chars);
        //2.整个字符串反转
        reverse(chars, 0, chars.length - 1);  // 因为字符串是地址引用,所以形参改变影响实参
        //3.单词反转
        reverseEachWord(chars);
        return new String(chars);
    }

    //1.用 快慢指针 去除首尾以及中间多余空格,可参考数组元素移除的题解
    public char[] removeExtraSpaces(char[] chars) {
        int slow = 0;
        for (int fast = 0; fast < chars.length; fast++) {  // 用fast指针来向后遍历
            //先用 fast 移除所有空格
            if (chars[fast] != ' ') {  // fast一直向后移动,遇到非空格的,就执行
                //在用 slow 加空格。 除第一个单词外,单词末尾要加空格
                if (slow != 0)
                    chars[slow++] = ' ';
                //fast 遇到空格或遍历到字符串末尾,就证明遍历完一个单词了
                while (fast < chars.length && chars[fast] != ' ')
                    chars[slow++] = chars[fast++];  // 让slow和fast同步后移,直到不是字母,遇上空格位置
            }
        }
        //相当于 c++ 里的 resize()
        char[] newChars = new char[slow];
        System.arraycopy(chars, 0, newChars, 0, slow);   //class Solution {
    //用 char[] 来实现 String 的 removeExtraSpaces,reverse 操作
    public String reverseWords(String s) {
        char[] chars = s.toCharArray();  // 基于以下三个函数来实现
        //1.去除首尾以及中间多余空格
        chars = removeExtraSpaces(chars);
        //2.整个字符串反转
        reverse(chars, 0, chars.length - 1);  // 因为字符串是地址引用,所以形参改变影响实参
        //3.单词反转
        reverseEachWord(chars);
        return new String(chars);
    }

    //1.用 快慢指针 去除首尾以及中间多余空格,可参考数组元素移除的题解
    public char[] removeExtraSpaces(char[] chars) {
        int slow = 0;
        for (int fast = 0; fast < chars.length; fast++) {
            //先用 fast 移除所有空格
            if (chars[fast] != ' ') {
                //在用 slow 加空格。 除第一个单词外,单词末尾要加空格
                if (slow != 0)
                    chars[slow++] = ' ';
                //fast 遇到空格或遍历到字符串末尾,就证明遍历完一个单词了
                while (fast < chars.length && chars[fast] != ' ')
                    chars[slow++] = chars[fast++];
            }
        }
        //相当于 c++ 里的 resize()
        char[] newChars = new char[slow];
        System.arraycopy(chars, 0, newChars, 0, slow); 
        return newChars;
    }

    // 2、双指针实现指定范围内字符串反转,可参考字符串反转题解
    public void reverse(char[] chars, int left, int right) {  
        if (right >= chars.length) {  // 初始right在末尾,left在位置0
            System.out.println("set a wrong right");
            return;
        }
        while (left < right) {   // 从头尾开始对每个元素进行交换
            chars[left] ^= chars[right];
            chars[right] ^= chars[left];
            chars[left] ^= chars[right];
            left++;
            right--;
        }
    }

    //3.单词反转
    public void reverseEachWord(char[] chars) {
        int start = 0;
        for (int end = 0; end <= chars.length; end++) {
            // 每次都把start指向单词结尾后的空格,start指向单词开始
            if (end == chars.length || chars[end] == ' ') {  //(当然最后一步end是指向单次结尾的,因为没有空格了)
                reverse(chars, start, end - 1);  // 然后就调用reverse函数来反转
                start = end + 1;
            }
        }
    }
}
        return newChars;
    }

    // 2、双指针实现指定范围内字符串反转,可参考字符串反转题解
    public void reverse(char[] chars, int left, int right) {  
        if (right >= chars.length) {  // 初始right在末尾,left在位置0
            System.out.println("set a wrong right");
            return;
        }
        while (left < right) {   // 从头尾开始对每个元素进行交换
            chars[left] ^= chars[right];
            chars[right] ^= chars[left];
            chars[left] ^= chars[right];
            left++;
            right--;
        }
    }

    //3.单词反转
    public void reverseEachWord(char[] chars) {
        int start = 0;
        for (int end = 0; end <= chars.length; end++) {
            // 每次都把start指向单词结尾后的空格,start指向单词开始
            if (end == chars.length || chars[end] == ' ') {  //(当然最后一步end是指向单次结尾的,因为没有空格了)
                reverse(chars, start, end - 1);  // 然后就调用reverse函数来反转
                start = end + 1;
            }
        }
    }
}

4、复杂度分析

  • 时间复杂度: O(n)
  • 空间复杂度: O(1) 或 O(n),取决于语言中字符串是否可变,java中的字符串的不能变的

五、右旋转字符串

1、题目:55. 右旋字符串(第八期模拟笔试) (kamacoder.com)

即把字符串尾部的若干个字符移到字符串的前面。会给一个字符串s和正整数k,要将字符串的后k个字符,一起移到前面。输入s=“abcdefg”,k=2  ;  输出  “fgabcde”

2、思路

仿照上面一个题,反转单词的操作,也是先把整个字符串都翻转过来,这样末尾的肯定就在前面了。然后再看看前面一部分是多长,后面一部分是多长,给两个部分分别进行内部反转即可

下图的2个步骤为:整个到倒转+子串翻转

当然也可以先局部翻转,再整体翻转。总之都是要在局部翻转的时候定位到k的位置。

这道题是右翻转,其实左翻转也是一样的思路。

3、代码

1)先整个翻转,再子串翻转

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = Integer.parseInt(in.nextLine());
        String s = in.nextLine();   // 接受用户输入的s和n

        int len = s.length();  //获取字符串长度
        char[] chars = s.toCharArray();  // java不能在原字符串上操作,要重新定义一个数组来存放s
        reverseString(chars, 0, len - 1);  //1、反转整个字符串
        reverseString(chars, 0, n - 1);  //2、反转前一段字符串,此时的字符串首尾尾是0,n - 1
        reverseString(chars, n, len - 1);  //3、反转后一段字符串,此时的字符串首尾尾是n,len - 1
        
        System.out.println(chars);  // 返回经过3次翻转后的字符数组

    }

    public static void reverseString(char[] ch, int start, int end) {
        //异或法反转字符串,也可以用temp来反转
        while (start < end) {
            ch[start] ^= ch[end];
            ch[end] ^= ch[start];
            ch[start] ^= ch[end];
            start++;
            end--;
        }
    }
}

2)改成先子串翻转,再整个翻转。(只需要改一点点)

reverseString(chars, 0, len - n - 1);  //反转前一段字符串,此时的字符串首尾是0,len - n - 1
reverseString(chars, len - n, len - 1);  //反转后一段字符串,此时的字符串首尾是len - n,len - 1
reverseString(chars, 0, len - 1);  //反转整个字符串

4、复杂度分析

  • 时间复杂度:O(n) 其实是O(len*n)
  • 空间复杂度:O(n)

六、实现strStr()、Java中是indexOf()

1、题目:28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)

给两个字符串,如果B是A的子串,则返回A中B开始的下标,如果不是子串,就返回-1.

输入: haystack = "hello", needle = "ll" 输出: 2;输入: haystack = "aaaaa", needle = "bba" 输出: -1

如果B串是空的话,按照函数的定义,应该返回0.

2、思想(KMP)

1)KMP

视频课:帮你把KMP算法学个通透!(理论篇)_哔哩哔哩_bilibili;帮你把KMP算法学个通透!(求next数组代码篇)_哔哩哔哩_bilibili帮你把KMP算法学个通透!(理论篇)_哔哩哔哩_bilibili;

KMP的思想:(三位学者的名字命名)当出现字符串不匹配的时候,可以记录一部分之前已经匹配的文本内容,利用这些信息,避免再从头去做匹配。其实就是解决字符串匹配问题。

!!重点就是next数组:用于记录已经匹配的文本内容

next数组——是一个前缀表:用于回退。

比如要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。

这个例子中,两个指针都从初始位置遍历比较。当比到b-f的时候,发现不匹配了。如果是传统暴力方法,就要再从头开始重新匹配。

但是用前缀表的话就不用从头匹配。而是从上次已经匹配的内容开始,会从模式串中第三个字符b继续开始匹配。(前缀表记录了当前下标i及之前的字符串中,有多大长度的相同前缀后缀

》》 找最长相等前后缀:所以对于刚刚的匹配,模式串遍历到f的时候不匹配了,这时候就要找模式串有没有长度相等的前后缀(从前往后一个个遍历,看每个子串的最长相等前后缀的长度为多少,比如首先是a,长度为0,然后aa长度为1,aab长度为0,直到aabaa长度为2.最后看匹配不上的时候,其前一位的前缀表数值,定位到数值对应下标),下面是找到了,

到f处就匹配不上了,然后看他前一位的a前缀表是2,所以下一次遍历从下表为2的b开始

所谓next数组,就是存放的前缀表 or 前缀表数值-1。核心就是要构造next数组。

  1. 初始化:定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置。定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置。所以初始化next[0] = j 。因为j初始化为-1,那么i就从1开始,进行s[i] 与 s[j+1]的比较
  2. 处理前后缀不相同的情况:如果 s[i] 与 s[j+1]不相同,j就要回退。即回到next[j]数值对应的下标处
  3. 处理前后缀相同的情况:如果 s[i] 与 s[j + 1] 相同,那么就同时向后移动i 和j。同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度

即i走的快一点,j走得慢一点,i用于真正记录前缀表数值, j用于辅助定位前缀子串。

原始next数组:010120  (不匹配就找前一位的next数组值2,回到下标2)

-1后的next数组:-10-1010(不匹配就找前一位的next数组值1,然后+1为下标)

不-1右移的数组:-101012(不匹配就找当前位的next数组值2,就位下标)

2)借助KMP做匹配

定义两个下标j 指向模式串起始位置,i指向文本串起始位置。j初始值仍然为-1,因为next数组里记录是起始位置为-1。

  1. i从0开始,遍历文本串.
  2. 比较 s[i] 与 t[j + 1] :如果不相同,就令j=next[j]。如果相同,就让i和j同时后移,
  3. 如果j指向了模式串的末尾,说明模式串是文本串的子串。所以在文本串中匹配上的初始下标就是i-t.lenth+1.

3、代码

1)next数组右移一位

next[0]=-1,这里其实是右移字符串了。所以回退只用看当前位置所对应的下标

相当于只对模式串,求出它的next数组。

其实就是遇到了不匹配的位置,就找他前一位的next数组中的值,然后把这个值+1就是回退下标

class Solution {
    public void getNext(int[] next, String s){
        int j = -1;  // 初始j为-1
        next[0] = j;  // next数组的第一位填充-1
        for (int i = 1; i < s.length(); i++){
            // 回退要写while,因为是连续回退的,直到前后缀相等为止
            while(j >= 0 && s.charAt(i) != s.charAt(j+1)){  // 前后缀不相等的情况
                j=next[j];  // j回退到next[j]数值对应的下标处(因为next右移了,所以不用看它的前一位)
            }

            if(s.charAt(i) == s.charAt(j+1)){  // 前后缀相等的情况
                j++;  //j就右移(i在for里面也会右移)
            }
            next[i] = j;  // 然后更新next数组的值
        }
    }
    public int strStr(String haystack, String needle) {
        if(needle.length()==0){
            return 0;
        }

        int[] next = new int[needle.length()];  // next数组
        getNext(next, needle);  // 首先计算出模式串的next数组的值
        // 然后下面对文本串和子串进行匹配
        int j = -1;  // j指向模式串初始
        for(int i = 0; i < haystack.length(); i++){  // i指向文本串初始
            while(j>=0 && haystack.charAt(i) != needle.charAt(j+1)){
                j = next[j];   // 不匹配的话,模式串的j就回退到当前next数组值所对应下标
            }
            if(haystack.charAt(i) == needle.charAt(j+1)){ 
                j++;   // 匹配的话,i和j就同时右移
            }
            if(j == needle.length()-1){  // 直到j到了模式串的末尾,就说明模式串已经全部出现在文本串中
                return (i-needle.length()+1);  // 返回文本串中匹配上的初始下标
            }
        }
        return -1;  // 如果j没有到末尾,说明不是子串,返回-1
    }
}

2)next数组就是原始前缀表

next[0]=0,其实就是最原始的前缀表,回退要看当前位置的前一个值所对应的下标。

class Solution {
    //前缀表(不减一)Java实现
    public int strStr(String haystack, String needle) {
        if (needle.length() == 0) return 0;
        int[] next = new int[needle.length()];
        getNext(next, needle);  // 构建模式串的next数组
        
        int j = 0;
        for (int i = 0; i < haystack.length(); i++) {
            while (j > 0 && needle.charAt(j) != haystack.charAt(i)) 
                j = next[j - 1];  // 回退看当前位置的前一个
            if (needle.charAt(j) == haystack.charAt(i)) 
                j++;
            if (j == needle.length()) 
                return i - needle.length() + 1;
        }
        return -1;

    }
    
    private void getNext(int[] next, String s) {
        int j = 0;
        next[0] = 0;  // 这里是把next数组的第一位赋值为0,然后j从0开始右移
        for (int i = 1; i < s.length(); i++) {
            while (j > 0 && s.charAt(j) != s.charAt(i)) //比较i和j对应的字符就行了
                j = next[j - 1];  // 如果不等就回退,这里是看当前位置的前一个对应的下标
            if (s.charAt(j) == s.charAt(i)) 
                j++;
            next[i] = j; 
        }
    }
}

4、复杂度分析

时间复杂度:两个字符串长度为n、m,那么暴力解法就是O(n*m),但是用KMP解法就是O(n+m)

空间复杂度:O(m),只需要保存模式串的前缀表即可。

七、重复的子字符串

1、题目:459. 重复的子字符串 - 力扣(LeetCode)

判断一个字符串是否由某个子串重复多次构成。

比如 :输入: "abab" ,输出:True   ;输入  "aba" ,输出:False

2、思路

视频课:字符串这么玩,可有点难度! | LeetCode:459.重复的子字符串_哔哩哔哩_bilibili

暴力解法:遍历一个个子串,然后把每个子串去和主串进行比较(看能不能构成主串)(从最后一个元素往前进行遍历就行)n^2的时间复杂度。

1)移动匹配

比如一个字符串abcabc由两个abc组成,那么s+s必然就是两组重复的s。

即s+s=abcabcabcabc,里面一定还包括一个s。一定符合这个特性。

但是在实际判断是是否包含s的时候,还要去除首位字符,避免搜索出来的是拼接的结果。

但是问题在于要判断是否包含s的时候,contains,find 之类的库函数时间复杂度为O(m+n),所以我们在其中采用KMP方法来判断2S中是否包含S子串。

2)KMP

目标就是看2s去头去尾后的字符串中是否包含s子串。用KMP方法来搜索。

前缀:包含首字母、不包含尾字母的所有子串。(包含首字母,往后任意长度

后缀:包含尾字母,不包含首字母的所有子串。(包含尾字母,往后任意长度

结论:如果一个字符串是由重复子串组成的,那么它的最小重复单位就是它的最长相等前后缀不包含的那个部分

比如abababab的前缀和后缀都是ababab,所以不包含的部分是ab,那么最小重复子串就是ab

推理:前缀是t后缀是f。即t和f是相等的,即t[0]=f[0] t[1]=f[1],那么t[01]=f[01],即s[01]=s[23]……t[2]=f[2] t[3]=f[3],那么t[23]=f[23],即s[23]=s[45],所以可以判断出原字符串都是重复组成的

3、代码

最长相等前后缀的长度,就是看next数组的最后一位的数值m。

那么重复子串的长度,就是len-m。检验方法就是看原字符串能不能整除len-m

class Solution {
    public boolean repeatedSubstringPattern(String s) {
        if (s.equals("")) return false;

        int len = s.length();
        // 原串前面加个空格(哨兵),使下标从1开始,这样j从0开始,也不用初始化了
        s = " " + s;
        char[] chars = s.toCharArray();  // 放进字符数组
        int[] next = new int[len + 1];   // 这个是next数组
        // 构造 next 数组过程,j从0开始(空格),i从2开始
        for (int i = 2, j = 0; i <= len; i++) {  // j从0 开始,i从2开始            
            while (j > 0 && chars[i] != chars[j + 1]) j = next[j];  // 匹配不成功,j回到前一位置 next 数组所对应的值
            if (chars[i] == chars[j + 1]) j++;  // 匹配成功,j往后移            
            next[i] = j;  // 更新 next 数组的值
        }
        // 最后判断是否是重复的子字符串,看总长度能不能被子串长度整除
        if (next[len] > 0 && len % (len - next[len]) == 0) {
            return true;   // next[len]就是整个s的最长相等前后缀长度,len - next[len]就是最小子串的长度
        }
        return false;
    }
}

4、复杂度分析

  • 移动匹配法:时间复杂度O(n),空间复杂度O(1)
  • 然后用KMP,时间复杂度O(n),空间复杂度O(n)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值