151. 翻转字符串里的单词(难:数学推导)
笨办法:
class Solution {
public String reverseWords(String s) {
StringBuilder sb = new StringBuilder();
char[] sArr = s.toCharArray();
int i = s.length()-1;
while(i>=0){
if(sArr[i]==' '){ // 空格 还是那个问题,两个空格,处理前一个还是处理后一个? // 此时不应该i--,而要用while循环,将空格全部处理。
if(sb.length()==0 || sb.charAt(sb.length()-1)==' '){ // 已经有空格了
i--;
continue;
}else{
i--;
sb.append(' ');
}
}else{ // 非空格
int left = i;
while(left>=0 && sArr[left]!=' ') left--;
for(int k=left+1; k<=i; k++){
sb.append(sArr[k]);
}
i=left;
}
}
// 末尾空格
for(i = sb.length()-1; sb.charAt(i)==' '; i--){
sb.deleteCharAt(i);
}
return sb.toString();
}
}
笨办法的第一次逻辑优化与缺陷:
- 没考虑到字符串的开头空格
class Solution {
public String reverseWords(String s) { // 笨办法
StringBuilder sb = new StringBuilder();
char[] sArr = s.toCharArray();
int i = s.length()-1;
while(i>=0){
while(i>=0 && sArr[i]==' '){
// 是多余空格
if(!(i>0 && sArr[i-1]==' ')) {// 是第一空格
if(sb.length()>0) sb.append(' '); // 是否为字符串的末尾空格
// 是否为字符串的开头空格
}
i--;
}
int right = i;
while(i >= 0 && sArr[i]!= ' ') i--;
if(sArr[i+1]!=' '){ // 处理末尾空格
for(int k=i+1; k<=right; k++){
sb.append(sArr[k]);
}
}
}
return sb.toString();
}
}
笨办法的第二次优化:
- 考虑字符串的前后空格
- 将空格一次性遍历完、将单词一次性遍历完。
class Solution {
public String reverseWords(String s) { // 笨办法
StringBuilder sb = new StringBuilder();
char[] sArr = s.toCharArray();
int i = s.length()-1;
while(i>=0){
while(i>=0 && sArr[i]==' ') i--;// 遍历完所有空格
if(sb.length()>0 && i>=0) sb.append(' '); // 保证了去除字符串前后的空格
int right = i;
while(i >= 0 && sArr[i]!= ' ') i--; // 遍历完该单词
for(int k=i+1; k<=right; k++) sb.append(sArr[k]);
}
return sb.toString();
}
}
实际考验的算法:
- 去除空格
- 翻转整个字符串
- 翻转每个单词
卡码网55.右旋转字符串(数学推导)
真想取巧的话,直接substring拼接就行hhhh
数学推导:
- 反转整体字符串
- 反转前k个,反转其他的
import java.util.*;
class Main{
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int k = sc.nextInt();
String s = sc.next();
// 边缘条件
if(k>=s.length()) System.out.println(s);
char[] cs = new char[s.length()];
for(int i=0; i<s.length()+k; i++){
cs[(i+k)%s.length()] = s.charAt(i%s.length());
}
System.out.println(String.valueOf(cs));
}
}
28. 实现strStr()(难:kmp、求前缀表、理解最长相同前后缀)
next数组(前缀表不减一):next[i]
表示,子串substring(0, i+1)
(i+1
代表子串的长度),它的最长相同前后缀的长度。也正因此,可以回退——此时后缀已经匹配上,证明前缀也可以匹配。
求next数组(前缀表减一):本来next[i]==最长相同前后缀长度
,现在是next[i]==最长相同前后缀长度-1
。例如,主串aabxa
,模式串aabaa
,在j=2
时,j+1
指向字符a
,此时next[2]=-1
。此时,会回退j=-1
,而j+1
再次进行比较。
下标 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
str | a | a | b | x | a |
next数组(前缀表不减一) | 0 | 1 | 0 | 1 | 2 |
next数组(前缀表减一) | -1 | 0 | -1 | 0 | 1 |
注意:始终是j+1
与i
两个位置进行比较。
- 初始化
指针i
:后缀末尾。初始化为1
指针j
:前缀末尾。初始化为-1
(前缀表减一的实现)【因为是前缀末尾,所以也指的是前缀的长度】
next[0]
:初始化为j
(因为next[i]
表示子串substring(0, i+1)
的最长前后缀的长度)
接下来,遍历i
,进行s[i]
与s[j+1]
的比较。 - 处理前后缀不同的情况
如果前后缀末尾不相同,就进行回退:因为比较的是j+1
,该位置与i
不相同。而next[j]
已经求出,因此j
回退到next[j]
指向的位置。此时前缀仍旧匹配,接下来要比较j+1
与i
。
总之,在后缀末尾i
遍历的过程中,一旦不匹配,前缀末尾j
就要重来。
而j
始终代表匹配上的前缀末尾,j+1
是即将匹配的字符。 - 处理前后缀相同的情况
如果前后缀末尾相同,就要进行下一步比较:j++
。(在循环遍历时,i
才会++)
同时需要填充next数组,next[i] = j
,因为j
就表示substring(0, i+1)
这个串的最长相同前后缀的长度:前后缀分别是substring(0, j+1)
,substring(i-(j+1), i)
第一次实现的getNext()
:
j>=0
的判断应该放在while
循环中- 为了保证
i
一定赋上值,因此next[i] = j
放在for循环中:若是两指针的字符相等,则赋值;若一直不匹配,则此时j
的值为-1
,也赋值。
private static int[] getNext(String needle){
int[] next = new int[needle.length()];
int j=-1;
next[0] = j; // 此时,j 表示needle.substring(0, 1)的最长前后缀长度-1,也表示最长前缀的末尾
for(int i=1; i<needle.length(); i++){ // 因为next[0]已经赋值,因此从1开始遍历
while(needle.charAt(i)!=needle.charAt(j+1)){ // 只要不相等,就一直回退。 也因此,要先比较不相等的情况。
if(j>=0) j = next[j]; // 保证不越界
else next[i] = j;
}
if(needle.charAt(i)==needle.charAt(j+1)){ // 比较的是j+1,
j++;
next[i] = j; // 此时,j表示needle.substring(0, i+1)的最长前后缀长度-1,也表示最长前缀的末尾
}
}
return next;
}
随想录中的:
public void getNext(int[] next, String s){
int j = -1;
next[0] = j;
for (int i = 1; i < s.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;
}
}
最终:
- 求next数组,与求模式串的下标的算法类似,比较的都是
j+1
与i
,都是不停回退。区别仅在于,i
的遍历的起始位置不同。 while
循环不停确定j
的位置,因此不能先判断相等,而是要先判断不相等。最后,要么相等了,要么退回到模式串起始位置。总之,next[i]
一定会赋上值。
class Solution {
public int strStr(String haystack, String needle) {
int[] next = getNext(needle);
int j = -1;
for(int i=0; i<haystack.length(); i++){
while(j>=0 && needle.charAt(j+1)!=haystack.charAt(i)){
j = next[j];
}
if(needle.charAt(j+1)==haystack.charAt(i)){
j++;
}
if(j==needle.length()-1) return i-j;
}
return -1;
}
private static int[] getNext(String needle){
int[] next = new int[needle.length()];
int j=-1;
next[0] = j;
for(int i=1; i<needle.length(); i++){ // 因为next[0]已经赋值,因此从1开始遍历
while(j>=0 && needle.charAt(i)!=needle.charAt(j+1)){ // 只要不相等,就一直回退。 也因此,要先比较不相等的情况。
j = next[j]; // 保证不越界
}
if(needle.charAt(i)==needle.charAt(j+1)){ // 比较的是j+1,
j++;
}
next[i] = j;
}
return next;
}
}
459. 重复的子字符串(难:数学推导)
看了一遍随想录,直接说证明结果吧:最长相等前后缀不包含的子串的长度 可以被 字符串s的长度整除 <==> 不包含的子串 就是s的最小重复子串。
注意:判断条件
有最长相等前后缀、能整除
class Solution {
public boolean repeatedSubstringPattern(String s) {
int[] next = getNext(s);
int len = s.length();
int maxLen = next[len-1] + 1;
if(next[len-1]>=0 && len % (len - maxLen) == 0) return true;
return false;
}
private static int[] getNext(String needle){
int[] next = new int[needle.length()];
int j=-1;
next[0] = j;
for(int i=1; i<needle.length(); i++){ // 因为next[0]已经赋值,因此从1开始遍历
while(j>=0 && needle.charAt(i)!=needle.charAt(j+1)){ // 只要不相等,就一直回退。 也因此,要先比较不相等的情况。
j = next[j]; // 保证不越界
}
if(needle.charAt(i)==needle.charAt(j+1)){ // 比较的是j+1,
j++;
}
next[i] = j;
}
return next;
}
}