

摘要:
本题要求翻转字符串中的单词顺序,并去除首尾及单词间多余空格。核心解法分三步:先去除多余空格(利用双指针和StringBuilder动态判断,确保单词间仅保留一个空格),再反转整个字符串(双指针交换字符),最后逐个反转每个单词(通过空格定位单词边界)。另一种解法是创建新字符数组,从后向前遍历原字符串,提取单词并依次放入新数组,天然实现顺序反转,最后去掉末尾多余空格。两种方法时间复杂度均为 O(n),前者空间 O(1),后者空间 O(n)。
给你一个字符串 s ,请你反转字符串中 单词 的顺序。
单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。
返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
示例 1:
输入:s = "the sky is blue"
输出:"blue is sky the"示例 2:
输入:s = " hello world "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。示例 3:
输入:s = "a good example"
输出:"example good a"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。提示:
1 <= s.length <= 104s 包含英文大小写字母、数字和空格 ' 's 中 至少存在一个 单词其实这道题目整体看上去是翻转,其实还有一个重要的逻辑就是去空,单词的开头不能有空格,单词与单词之间就是靠一个空格来分隔的,但是只有一个,如果有多个就要去除。
这正是题目要求的
反转后的字符串中不能存在前导空格和尾随空格。 如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个
那我们来思考该怎么操作呢,首先就应当先把不需要的空格都去掉,否则这会影响我们之后的翻转操作,因此这个操作的优先级比较高。
那我们怎么去掉这些空格呢
首先是首尾,其实还比较简单,主要的就是单词与单词之间只能有一个空格,我们定义一个方法,之后调用去空格。我们进行判断首位,直接用while循环,很轻松的就去掉两个空格了
然后我们利用StringBuilder创建一个可变的字符串,因为java的字符串本身是不可变的。这样我们就能对字符串进行增删改查。之后就是处理单词之间的空格了,
char c = s.charAt(start);
if (c != ' ' || sb.charAt(sb.length() - 1) != ' ') {
sb.append(c);
}
start++;情况 | 条件 | 说明 |
|---|---|---|
1 | c != ' ' | 当前字符不是空格(是字母),直接加入 |
2 | c == ' ' 且 sb 最后一个字符不是空格 | 当前是空格,但上一个加入的不是空格,说明这是单词间的分隔符,加入一个空格 |
这里的操作就是去掉多余的空格,保证只能有一个空格。
sb.charAt(sb.length() - 1) 不是只找一次最后的 'd',而是每处理一个字符时,动态查看 sb 当前已经构建好的内容的最后一个字符是什么,从而决定要不要添加新的空格。这是一个实时判断的过程。
输入:" hello world "
text
start → h e l l o w o r l d ← end
0 1 2 3 4 5 6 7 8 9 10 11start | c | sb当前内容 | sb最后一个字符 | 条件判断 | 是否追加 | sb新内容 |
|---|---|---|---|---|---|---|
0 | 'h' | "" | (不存在) | c≠空格 ✅ | ✅ | "h" |
1 | 'e' | "h" | 'h' | c≠空格 ✅ | ✅ | "he" |
2 | 'l' | "he" | 'e' | c≠空格 ✅ | ✅ | "hel" |
3 | 'l' | "hel" | 'l' | c≠空格 ✅ | ✅ | "hell" |
4 | 'o' | "hell" | 'l' | c≠空格 ✅ | ✅ | "hello" |
5 | ' ' | "hello" | 'o' | c==空格 且 sb最后一个字符='o'≠空格 ✅ | ✅ | "hello " |
6 | ' ' | "hello " | ' ' | c==空格 且 sb最后一个字符=' ' ❌ | ❌ | "hello " (不变) |
7 | 'w' | "hello " | ' ' | c≠空格 ✅ | ✅ | "hello w" |
8 | 'o' | "hello w" | 'w' | c≠空格 ✅ | ✅ | "hello wo" |
9 | 'r' | "hello wo" | 'o' | c≠空格 ✅ | ✅ | "hello wor" |
10 | 'l' | "hello wor" | 'r' | c≠空格 ✅ | ✅ | "hello worl" |
11 | 'd' | "hello worl" | 'l' | c≠空格 ✅ | ✅ | "hello world" |
private StringBuilder removeSpace(String s) {
// System.out.println("ReverseWords.removeSpace() called with: s = [" + 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);
if (c != ' ' || sb.charAt(sb.length() - 1) != ' ') {
sb.append(c);
}
start++;
}
// System.out.println("ReverseWords.removeSpace returned: sb = [" + sb + "]");
return sb;
}利用双指针法:
逐步拆解
1. 循环条件 while (start < end)
只要左指针在右指针左边,就继续交换。当两个指针相遇或交错时,说明已经全部交换完毕。
2. 交换三部曲
java
char temp = sb.charAt(start); // 步骤1:暂存左边的字符
sb.setCharAt(start, sb.charAt(end)); // 步骤2:把右边字符放到左边位置
sb.setCharAt(end, temp); // 步骤3:把暂存的左边字符放到右边位置3. 移动指针
java
start++; // 左指针右移
end--; // 右指针左移之后就是单独的翻转单词了,把单词的顺序倒过来,这样就完成了我们要的翻转单词位置了
我们这里定义start end,是为了确定每个单词的首尾位置,这要确定之后,我们再调用第二步的翻转全部,也就是把这一个单词的顺序翻转正常了,每个单词的分界线是空格,通过空格来确定。
假设当前 sb = "olleh world"(这是反转整个字符串后的结果,目标是变成 "hello world")
text
初始: o l l e h w o r l d
0 1 2 3 4 5 6 7 8 9 10
↑
空格位置变量 | 值 | 说明 |
|---|---|---|
start | 0 | 第一个单词从索引0开始 |
end | 1 | 开始寻找单词结尾 |
内层 while:
sb.charAt(1)='l' ≠ 空格 → end=2
sb.charAt(2)='l' ≠ 空格 → end=3
sb.charAt(3)='e' ≠ 空格 → end=4
sb.charAt(4)='h' ≠ 空格 → end=5
sb.charAt(5)=' ' == 空格 → 循环结束
找到单词范围:start=0, end=5(end指向空格)
java
reverseString(sb, 0, 4); // 反转索引0~4
// "olleh" → "hello"class Solution {
/**
* 不使用Java内置方法实现
* <p>
* 1.去除首尾以及中间多余空格
* 2.反转整个字符串
* 3.反转各个单词
*/
public String reverseWords(String s) {
// System.out.println("ReverseWords.reverseWords2() called with: s = [" + s + "]");
// 1.去除首尾以及中间多余空格
StringBuilder sb = removeSpace(s);
// 2.反转整个字符串
reverseString(sb, 0, sb.length() - 1);
// 3.反转各个单词
reverseEachWord(sb);
return sb.toString();
}
private StringBuilder removeSpace(String s) {
// System.out.println("ReverseWords.removeSpace() called with: s = [" + 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);
if (c != ' ' || sb.charAt(sb.length() - 1) != ' ') {
sb.append(c);
}
start++;
}
// System.out.println("ReverseWords.removeSpace returned: sb = [" + sb + "]");
return sb;
}
/**
* 反转字符串指定区间[start, end]的字符
*/
public void reverseString(StringBuilder sb, int start, int end) {
// System.out.println("ReverseWords.reverseString() called with: sb = [" + sb + "], start = [" + start + "], end = [" + end + "]");
while (start < end) {
char temp = sb.charAt(start);
sb.setCharAt(start, sb.charAt(end));
sb.setCharAt(end, temp);
start++;
end--;
}
// System.out.println("ReverseWords.reverseString returned: sb = [" + sb + "]");
}
private void reverseEachWord(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;
}
}
}//解法二:创建新字符数组填充。时间复杂度O(n)
class Solution {
public String reverseWords(String s) {
//源字符数组
char[] initialArr = s.toCharArray();
//新字符数组
char[] newArr = new char[initialArr.length+1];//下面循环添加"单词 ",最终末尾的空格不会返回
int newArrPos = 0;
//i来进行整体对源字符数组从后往前遍历
int i = initialArr.length-1;
while(i>=0){
while(i>=0 && initialArr[i] == ' '){i--;} //跳过空格
//此时i位置是边界或!=空格,先记录当前索引,之后的while用来确定单词的首字母的位置
int right = i;
while(i>=0 && initialArr[i] != ' '){i--;}
//指定区间单词取出(由于i为首字母的前一位,所以这里+1,),取出的每组末尾都带有一个空格
for (int j = i+1; j <= right; j++) {
newArr[newArrPos++] = initialArr[j];
if(j == right){
newArr[newArrPos++] = ' ';//空格
}
}
}
//若是原始字符串没有单词,直接返回空字符串;若是有单词,返回0-末尾空格索引前范围的字符数组(转成String返回)
if(newArrPos == 0){
return "";
}else{
return new String(newArr,0,newArrPos-1);
}
}
}这就是典型的新开辟一个空间,把旧数组循环遍历到新数组,实现天然的翻转。
创建新数组的时候,需要多一个 位置,用来放空格,遍历的时候新数组的指针是从0开始,原来数组的指针是从最后开始。
java
// 跳过空格
i=15: ' ' → i=14
i=14: ' ' → i=13
i=13: 'd' → 停止
right = i = 13
// 向左找单词左边界
i=13: 'd' ≠ ' ' → i=12
i=12: 'l' ≠ ' ' → i=11
i=11: 'r' ≠ ' ' → i=10
i=10: 'o' ≠ ' ' → i=9
i=9: 'w' ≠ ' ' → i=8
i=8: ' ' == ' ' → 停止
// 单词范围 [i+1, right] = [9, 13]
// 复制 "world" + 空格
newArr: [w][o][r][l][d][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
0 1 2 3 4 5
newArrPos = 6java
// i 当前是 8,指向空格,继续跳过空格
i=8: ' ' → i=7
i=7: ' ' → i=6
i=6: 'o' → 停止
right = i = 6
// 向左找单词左边界
i=6: 'o' ≠ ' ' → i=5
i=5: 'l' ≠ ' ' → i=4
i=4: 'l' ≠ ' ' → i=3
i=3: 'e' ≠ ' ' → i=2
i=2: 'h' ≠ ' ' → i=1
i=1: ' ' == ' ' → 停止
// 单词范围 [i+1, right] = [2, 6]
// 复制 "hello" + 空格
newArr: [w][o][r][l][d][ ][h][e][l][l][o][ ][ ][ ][ ][ ][ ]
0 1 2 3 4 5 6 7 8 9 10 11
newArrPos = 12设计 | 原因 |
|---|---|
newArr 长度 +1 | 每个单词后加空格,最后一个空格会被去掉 |
从后往前遍历 | 天然实现单词顺序反转 |
单词后加空格 | 统一处理,最后再去掉末尾空格 |
newArrPos-1 | 去掉最后多余的那个空格 |
优点:
缺点:
特性 | 解法一(原地反转) | 解法二(新数组填充) |
|---|---|---|
空间复杂度 | O(1) | O(n) |
代码复杂度 | 较高(需要多个辅助方法) | 较低(一个方法搞定) |
理解难度 | 较难 | 容易 |
适用场景 | 要求原地修改 | 无空间限制 |
结语:如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!
