LeetCode 151 翻转字符串里的单词
本题非常细致地考察了对字符串的操作。
思路
题目要求使用空间复杂度为的算法,原地翻转
可以考虑两次反转字符串:
- 把整个字符串反转
- 把每个单词单独反转 这样得到的字符串就是只有单词顺序反转的。
先处理多余空格可以减少反转操作的计算量。由于Java的字符串不可修改,可以在把String转为StringBuffer类时顺便移除空格。
- 首先找到除了首尾空格的字符串的起始和结束点left和right,这样后续秩序要考虑单词间空格
- 在[left, right]区间内,遍历每个字符:
- 如果不是空格,加入buffer
- 如果是空格且前一个字符不是空格,也加入buffer 其实这种操作的思想类似于双指针法,只不过因为语言特性,使得空间复杂度为
使用split的思路
- 拆分出单词列表(注意:传入的分割符如果是空格,连续的空格会让返回列表中出现空串)
- 把非空串的单词用空格连接
kama 55 右旋转字符串
思路
基于空间复杂度设计算法(Java转为StringBuffer操作,空间复杂度为)
和前一题类似,本题相当于有两个单词的字符串,颠倒他们的位置,所以依然可以使用两次反转
- 反转整个字符串(后k格字母成为前k个字母)
- 分别对
[0,k-1],[k, length-1]两个串反转
解答
import java.util.*;
public class Main {
static StringBuffer sBuffer;
public static void main (String[] args) {
Scanner scanner = new Scanner(System.in);
int k = scanner.nextInt();
String s = scanner.next();
sBuffer = new StringBuffer(s);
reverseStr(0, s.length()-1);
reverseStr(0, k-1);
reverseStr(k, s.length()-1);
System.out.println(sBuffer.toString());
}
public static void reverseStr(int left, int right) {
while (left < right) {
char tmp = sBuffer.charAt(left);
sBuffer.setCharAt(left, sBuffer.charAt(right));
sBuffer.setCharAt(right, tmp);
left++;
right--;
}
}
}
LeetCode 28 实现 strStr()
文档讲解:programmercarl.com/0028.实现strS…
视频讲解:
思路
本题实质上就是实现KMP算法。解决的问题是字符串匹配问题。
问题定义
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
称haystack为文本串,needle为模式串
KMP算法
主要思想:当出现字符串不匹配时,可以知道一部分之前已经匹配过的文本内容,利用这些信息避免从头再做匹配
KMP算法的关键:前缀表
前缀表的内容
下标i之前(包括i)的字符串中,有多大长度的公共前缀后缀。 思想:当存在公共前后缀时,主串中已经匹配的正确后缀可作为下一次尝试匹配的前缀
前缀表的用途
用于匹配失败时指示回退位置。
计算前缀表
- 前缀:不包含最后一个字符的,所有以第一个字符开头的连续子串
- 后缀:不包含第一个字符的所有以最后一个字符结尾的连续子串
对每一个位置i,计算str[0:i+1]的最长公共前后缀
使用前缀表
- 看不匹配的前一个位置,也就是最后一个匹配的字符在前缀表中的数值k
- 把匹配模式串的指针移动到下标k
- 继续匹配
时间复杂度
n为文本串长度,m为模式串长度
- 生成next数组:
- 匹配过程:
- 总的时间复杂度:
构造next数组
本质上就是计算模式串s的前缀表,步骤如下:
-
初始化
-
定义两个指针。指针i指向后缀末尾,j指向前缀末尾。这里的前后缀指i之前(包含i)的串的前后缀。next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j)
-
对next数组初始化赋值,这里选择对前缀表统一减一的实现(为了在使用next数组是可以直接next[j]=j)
int j = -1; next[0] = j;
-
-
当i和j+1没匹配上,前后缀不同
j初始化为-1,那么i就从1开始,进行s[i] 与 s[j+1]的比较for (int i = 1; i < s.size(); i++) {while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 j = next[j]; // 向前回退 }使用next[j]回退是因为,next[j]表示了s[0:j+1]中最长公共前后缀,s[j+1]没匹配上,就要找 j+1前一个元素在next数组里的值
求next数组的时候就是在用kmp,把 [0,j] 的前缀看成是模式串,把 [1,i] 的后缀看成是文本串.
-
当i和j+1匹配上了,前后缀相同
if (s[i] == s[j + 1]) { // 找到相同的前后缀 j++; } next[i] = j;
使用next数组做匹配
定义两个下标j 指向(已匹配的)模式串起始位置,i指向文本串起始位置。由于我们使用前缀表统一减一的实现,j初始化为-1。就可以进行s[j+1]和t[i]的匹配 外循环对文本串进行遍历
for (int i = 0; i < s.size(); i++)
如果s[j+1]和t[i]不相同,就要根据next数组把j指针回退
while(j >= 0 && t[i] != s[j + 1]) {
j = next[j];
}
如果s[j+1]和t[i]相同,j++,i随for循环进入下一个位置
if (s[i] == t[j + 1]) {
j++; // i的增加在for循环里
}
当j指向模式串的末尾时,代表找到了完全匹配的序列。此时i指向文本串中完全匹配序列的最后一个位置,所以只需要返回i-j
if (j == (t.size() - 1) ) {
return (i - j);
}
复杂度分析
设模式串长度为m,文本串长度为n
- 时间复杂度:
- 空间复杂度:
LeetCode 459 重复的子字符串
思路
字符串匹配法
我们将两个 s 连在一起,并移除第一个和最后一个字符。如果 s 是该字符串的子串(使用语言的库函数),那么 s 就满足题目要求。(正确性证明略)
KM P法
对字符串匹配法中的匹配过程用KMP实现.
KMP法(优化)
我们设 i 为最小的起始位置,那么一定有 gcd(n,i)=i,即 n 是 i 的倍数。这说明字符串 s 是由长度为 i 的前缀重复次构成 由于 next[n−1] 表示 s 具有长度为 next[n−1]+1 的完全相同的(且最长的)前缀和后缀。那么对于满足题目要求的字符串,一定有 next[n−1]=n−i−1,即 i=n−next[n−1]−1;
对于不满足题目要求的字符串,n 一定不是 n−next[n−1]−1 的倍数。 (正确性证明略)
解答
class Solution {
public boolean repeatedSubstringPattern(String s) {
int[] next = getNext(s);
int n = s.length();
int i = n - next[n-1] - 1;
return (next[n-1] != -1) && (n % i == 0);
}
private int[] getNext(String s) {
int[] next = new int[s.length()];
int j = -1;
next[0] = j;
for (int i = 1; i < next.length; i++) {
while (j >= 0 && s.charAt(i) != s.charAt(j+1)) {
j = next[j];
}
if (s.charAt(i) == s.charAt(j+1)) {
j++;
}
next[i] = j;
}
return next;
}
}
今日收获总结
今日学习时长6h,KMP算法每次看还是懵。