### 4.4 151. Reverse Words in a String
Given an input string s, reverse the order of the words.
A word is defined as a sequence of non-space characters. The words in s will be separated by at least one space.
Return a string of the words in reverse order concatenated by a single space.
Note that s may **contain** leading or trailing spaces or multiple spaces between two words. The returned string should only have a single space separating the words. Do not include any extra spaces.
要求:空间复杂度O(1),时间复杂度O(n2),并且不使用split函数
难点:去除多余空格
使用双指针法,思路类似1.3 27 remove element。定义快慢指针,满指针用于确定存放位置,快指针用于遍历字符串。这里有一个地方需要特殊处理,即第一个单词前面不需要有空格。
>[!Tip] java因为没办法直接修改字符串,所以需要使用StringBuilder/StringBuffer
> StringBuilder 和 StringBuffer 都是 Java 中用于创建可变字符串的类,它们都继承自 AbstractStringBuilder 类。尽管它们的目的相似,但在多线程环境下使用时存在一些关键差异。
> 1. StringBuilder
> StringBuilder 是在 Java 1.5(Java 5)中引入的,用于在单线程环境下高效地构建可变字符串。
> 它不是线程安全的,因此在多线程环境下使用时可能会遇到并发问题,比如数据不一致。
> 由于它不需要考虑线程安全的问题,所以在单线程环境中使用 StringBuilder 会比 StringBuffer 更快。
> 当你需要频繁地修改字符串(如拼接、插入、删除等操作)时,使用 StringBuilder 是个不错的选择。
> 2. StringBuffer
> StringBuffer 是 Java 早期版本中引入的,用于在多线程环境下安全地构建可变字符串。
> 它是线程安全的,所有公开的方法都通过 synchronized 关键字进行了同步,这保证了在同一时刻只有一个线程可以修改 StringBuffer 实例。
> 然而,由于 synchronized 关键字的使用,StringBuffer 在多线程环境下的性能可能会受到影响,因为它在每次调用方法时都会进行线程锁定。
> 如果你在多线程环境中需要频繁地修改字符串,并且担心线程安全问题,那么 StringBuffer 是个合适的选择。
> 3.API
> StringBuilder 和 StringBuffer 的 API(应用程序编程接口)在 Java 中是非常相似的,因为它们都继承自 AbstractStringBuilder 类,并提供了大量用于操作可变字符串的方法。以下是这两个类的一些常用 API 方法概述:
> 构造方法
> 无参构造方法:创建一个空的字符串构建器,不包含任何字符。
> StringBuilder()
> StringBuffer()
> 带字符串参数的构造方法:创建一个字符串构建器,并将其内容初始化为指定的字符串。
> StringBuilder(String str)
> StringBuffer(String str)
> 常用方法
> append():将指定的数据追加到此字符序列的末尾。该方法支持多种类型的数据,包括字符串、字符、数字等,并且返回构建器对象本身,支持链式调用。
> StringBuilder append(xx)
> StringBuffer append(xx)
> insert():在指定位置插入指定的数据。
> StringBuilder insert(int offset, xx)
> StringBuffer insert(int offset, xx)
> delete():删除字符序列中指定范围的字符。
> StringBuilder delete(int start, int end)
> StringBuffer delete(int start, int end)
> deleteCharAt():删除指定位置的字符。
> StringBuilder deleteCharAt(int index)
> StringBuffer deleteCharAt(int index)
> replace():替换字符序列中指定范围的字符序列为新的字符序列。
> StringBuilder replace(int start, int end, String str)
> StringBuffer replace(int start, int end, String str)
> setCharAt():替换指定位置的字符。
> StringBuilder setCharAt(int index, char c)
> StringBuffer setCharAt(int index, char c)
> charAt():返回指定位置的字符。
> char charAt(int index)(此方法在 StringBuilder 和 StringBuffer 中都是继承自 CharSequence 接口的)
> reverse():反转此字符序列。
> StringBuilder reverse()
> StringBuffer reverse()
> length():返回此字符序列的长度。
> int length()(此方法在 StringBuilder 和 StringBuffer 中都是继承自 CharSequence 接口的)
> toString():返回此序列中数据的字符串表示形式。这是将 StringBuilder 或 StringBuffer 对象转换为 String 对象的标准方法。
> String toString()
> capacity()(仅 StringBuffer):返回当前容量。这是 StringBuffer 特有的方法,用于获取其内部字符数组的长度。虽然 StringBuilder 也有类似的内部机制,但它通常不提供直接获取容量的公共方法。
> int capacity()
```java
//方法1:使用StringBuilder 去除多余空格以后反转每一个单词
public String reverseWords(String s) {
//去除多余空格
StringBuilder sb = removeSpace(s);
sb = reverseString(sb,0,sb.length()-1);
sb = eachWord(sb);
return sb.toString();
}
public StringBuilder removeSpace(String s){
int start = 0;
int end = s.length()-1;
//去除开头结尾的所有空格
while(s.charAt(start) == ' ') start++;
while(s.charAt(end) == ' ') end--;
//去除中间的多余空格
StringBuilder sb = new StringBuilder();
while(start <= end){
char c = s.charAt(start);
//start遍历到的元素不为0/单词末尾可以存一个空格
if(c != ' ' || sb.charAt(sb.length()-1) != ' '){
sb.append(c);
}
start++;
}
return sb;
}
public StringBuilder reverseString(StringBuilder sb, int start, int end){
while(start < end){
char temp = sb.charAt(start);
sb.setCharAt(start,sb.charAt(end));
sb.setCharAt(end,temp);
start++;
end--;
}
return sb;
}
public StringBuilder eachWord(StringBuilder sb){
int start = 0;
int end = 1;
int n = sb.length();
while(start < n){
//从后往前判断。
while(end < n && sb.charAt(end) != ' '){
end++;
}
reverseString(sb,start,end-1);
start = end+1;
end = start+1;
}
return sb;
}
```
### 4.5 55. 右旋字符串(第八期模拟笔试)
`题目描述
字符串的右旋转操作是把字符串尾部的若干个字符转移到字符串的前面。给定一个字符串 s 和一个正整数 k,请编写一个函数,将字符串中的后面 k 个字符移到字符串的前面,实现字符串的右旋转操作。
`例如,对于输入字符串 "abcdefg" 和整数 2,函数应该将其转换为 "fgabcde"。
```java
public class moveRightString {
//简单版
public String moveRightString(String s, int k) {
StringBuilder sb = new StringBuilder();
for (int i = s.length()-k; i < s.length(); i++) {
sb.append(s.charAt(i));
}
for (int i = 0; i < s.length()-k; i++) {
sb.append(s.charAt(i));
}
return sb.toString();
}
//反转字符串版
public String moveRightString2(String s, int k) {
char[] c = s.toCharArray();
reverseChar(c,0,s.length()-k-1);
reverseChar(c,s.length()-k,s.length()-1);
reverseChar(c,0,s.length()-1);
return new String(c);
}
public void reverseChar(char[] c,int start, int end){
while(start < end){
char temp = c[start];
c[start] = c[end];
c[end] = temp;
start++;
end--;
}
}
}
```
### 4.6 28. Find the Index of the First Occurrence in a String
Given two strings needle and haystack, return the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.
KMP算法:用于解决字符串匹配问题。时间复杂度`O(m+n)。举例:
文本串:aabaabaaf
模式串: `aabaaf
暴力解法需要两层for循环,时间复杂度O(m*n)。KMP算法不会重复遍历已经匹配过的字符。
KMP的核心:找出最长相等前后缀
前缀:包含首字母,不包含尾字母的所有字符串。`(a aa aab aaba aabaa)
后缀:包含尾字母,不包含首字母的所有字符串。`(f af aaf baaf abaaf)
分析例子中的模式串aabaaf的最长相等前后缀:
(前缀 后缀)
`a 0
`aa 1(a a)
`aab 0(后缀有b,所以没有)
`aaba 1(a a)
`aabaa 2 (a a)(aa aa)
`aabaaf 0
得出模式串前缀表:`aabaaf
010120
该模式串在f的位置发生了冲突,后续f的前一位对应的从index为2的元素继续遍历即可。
使用KMP算法,一定要构造next数组。next数组的核心:遇到冲突位置后,向前回退。
next数组的不同实现方法:
`getNext (int[] next, String s)//传入next数组,并定义元素
构造next数组其实就是计算模式串s,前缀表的过程。 主要有如下三步:
1.初始化
定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置。
int j = 0;
next[0] = j;
next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j)
2.处理前后缀不相同的情况、3.处理前后缀相同的情况
```java
for(int i = 1; i < s.length; i++){
//前后缀不相同时,j需要回退。j = next[j-1];
while( j>0 && s[i] != s[j]){
j = next[j-1];
};
//前后缀相同时,j++;
if(s[i] == s[j]){
j++;
}
//更新next数组的值
next[i] = j;
}
```
因为kmp算法效率很高,理解起来也有一些复杂,所以我做了一个简单的ppt便于理解。![[KMP算法动画.pptx]]
完整代码:
```java
public class KMPIndexOfFirstOcc {
public int strStr(String haystack, String needle) {
//特殊情况:needle为""
if(needle.length() == 0) return 0;
int[] next = new int[needle.length()];
int j = 0;
getNext(next,needle);
for (int i = 0; i < haystack.length(); i++) {
//If the string and pattern string are not equal, then j=next[j-1], until j=0
while(j>0 && haystack.charAt(i) != needle.charAt(j)) j = next[j-1];
if(haystack.charAt(i) == needle.charAt(j)) j++;
if(j == needle.length()) return i-needle.length()+1;
}
return -1;
}
public void getNext(int[] next, String needle){
//initialize
int j = 0;
next[0] = j;
for (int i = 1; i < needle.length(); i++) {
//When the prefixes and suffixes are not equal, j=next [j-1] until j=0
while(j>0 && needle.charAt(i) != needle.charAt(j)) j = next[j-1];
if(needle.charAt(i) == needle.charAt(j)) j++;
next[i] = j;
}
}
}
class KMPIndexOfFirstOccTest {
public static void main(String[] args) {
KMPIndexOfFirstOcc kmp = new KMPIndexOfFirstOcc();
System.out.println(kmp.strStr("aabaabaaf","aabaaf"));
System.out.println(kmp.strStr("aabaabaaf",""));
System.out.println(kmp.strStr("sadbutsad","sad"));
System.out.println(kmp.strStr("leetcode","leeto"));
}
}
```
### 4.7 459. Repeated Substring Pattern
Given a string s, check if it can be constructed by taking a substring of it and appending multiple copies of the substring together.
`abcabcabc
000123123
> [!warning] 第一次自己写了一个错误版本。这个版本的错误在于,subString内部的next数组都要为0才可以。一旦内部出现重复就会误判为false。
```java
public class repeatedSubstring {
public boolean repeatedSubstringPattern(String s) {
int[] next = new int[s.length()];
getNext(next,s);
for (int i = 0; i < next.length; i++) {
System.out.println(next[i]);
}
int len = 0;
//加入了len < s.length()限定条件,防止next数组元素都为0时下标溢出
while(len < s.length() && next[len] == 0){ len++; }
//加入了len == s.length()的判定条件,防止next数组都为0的时候返回true
if(s.length() % len != 0 || len == s.length()) return false;
for (int i = len; i < s.length(); i++) {
if(next[i] != next[i-1]+1) return false;
}
return true;
}
public void getNext(int[] next, String s){
int j = 0;
next[0] = j;
for (int i = 1; i < s.length(); i++) {
while(j > 0 && s.charAt(i) != s.charAt(j)) j = next[j-1];
if(s.charAt(i) == s.charAt(j)) j++;
next[i] = j;
}
}
}
class repeatedSubstringTest {
public static void main(String[] args) {
repeatedSubstring rs = new repeatedSubstring();
System.out.println(rs.repeatedSubstringPattern("abaababaab"));
}
}
```
移动匹配法:
如果s是重复字符串,那么将两个s拼接在一起一定可以得到一个新的s。ps:拼接的时候要去掉s1的首字母和s2的尾字母,不然会找到原s。
`String s2 = s+s;
return s2.contains(s);
KMP法:
Q:最长相等前后缀和重复子串的关系?
A:最长相等前后缀不包含的部分就是重复子串。
正确代码如下:
```java
public class repeatedSubstring {
public boolean repeatedSubstringPattern(String s) {
int[] next = new int[s.length()];
getNext(next,s);
//最长相等前后缀不包含的部分就是重复子串.所以重复子串的长度就是字符串的长度-最长相等前后缀。且重读字符串的最长相等前后缀长度为next数组的最后一位。
//所以:
int len = next.length-next[next.length-1];
//最后一位不为0,且字符串长度可以被重复子串长度整除
if(next[s.length()-1]>0 && s.length()%len==0){
return true;
}
return false;
}
public void getNext(int[] next, String s){
int j = 0;
next[0] = j;
for (int i = 1; i < s.length(); i++) {
while(j > 0 && s.charAt(i) != s.charAt(j)){
j = next[j-1];
}
if(s.charAt(i) == s.charAt(j)) {
j++;
}
next[i] = j;
}
}
}
class repeatedSubstringTest {
public static void main(String[] args) {
repeatedSubstring rs = new repeatedSubstring();
System.out.println(rs.repeatedSubstringPattern("abaababaababaab"));
}
}