目录
1、题目:344. 反转字符串 - 力扣(LeetCode)
1、题目:541. 反转字符串 II - 力扣(LeetCode)
1、题目:54. 替换数字(第八期模拟笔试) (kamacoder.com)
1、题目:151. 反转字符串中的单词 - 力扣(LeetCode)
1、题目:55. 右旋字符串(第八期模拟笔试) (kamacoder.com)
1、题目:28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)
1、题目:459. 重复的子字符串 - 力扣(LeetCode)
在思路上,其实字符串和数组是差不多的。但是具体是线上不同的编程语言实现起来字符串还是不太一样。
一、反转字符串
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。比如说对一个数进行两次异或运算,就会得回原来的值。
- s[l] ^= s[r]是将结果赋给
s[l]
,此时s[l]
和s[r]
的值互换了。 -
s[r] ^= s[l];
:再次使用异或运算,这次是将s[l]
的值(现在它已经是s[r]
原来的值)与s[r]
进行异或运算,并将结果赋给s[r]
。由于s[l]
现在是s[r]
原来的值,这一步实际上是将s[r]
的值清零。 -
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,
- 然后下面即第一步是反转下标为0-1的数。
- 然后到第二步。start=2,end=min(6,3),反转1次,即反转下标为2-3的数。
- 第三步。start=4,end=min(6,5),反转1次,反转下标为4,5的数。
- 第四部,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:初始slow和fast都从位置0开始。
- 步骤2:主要是令fast指针遍历字符数组。当fast指向一个非空字符的时候,说明遇到了单词
- 步骤3:此时,如果slow不在最开始,就给slow的位置加一个空格,并且slow右移一位。
- 步骤4:然后同时右移fast和slow,直到fast遇到了空格,即fast到了单词的结尾,就停止移动slow
- 步骤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数组。
- 初始化:定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置。定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置。所以初始化next[0] = j 。因为j初始化为-1,那么i就从1开始,进行s[i] 与 s[j+1]的比较。
- 处理前后缀不相同的情况:如果 s[i] 与 s[j+1]不相同,j就要回退。即回到next[j]数值对应的下标处。
- 处理前后缀相同的情况:如果 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。
- i从0开始,遍历文本串.
- 比较 s[i] 与 t[j + 1] :如果不相同,就令j=next[j]。如果相同,就让i和j同时后移,
- 如果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)