

🔥个人主页:北极的代码(欢迎来访)
🎬作者简介:java后端学习者
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
摘要:
本题要求翻转字符串中的单词顺序,并去除首尾及单词间多余空格。核心解法分三步:先去除多余空格 (利用双指针和StringBuilder动态判断,确保单词间仅保留一个空格),再反转整个字符串 (双指针交换字符),最后逐个反转每个单词 (通过空格定位单词边界)。另一种解法是创建新字符数组,从后向前遍历原字符串,提取单词并依次放入新数组,天然实现顺序反转,最后去掉末尾多余空格。两种方法时间复杂度均为 O(n),前者空间 O(1),后者空间 O(n)。
题目背景:LeetCode151 翻转字符串的单词(中等)
给你一个字符串
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 11逐字符处理
start 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"
java
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;
}
去除空格之后,我们要做的就是翻转整个字符串了
利用双指针法:
逐步拆解
- 循环条件
while (start < end)只要左指针在右指针左边,就继续交换。当两个指针相遇或交错时,说明已经全部交换完毕。
- 交换三部曲
javajava char temp = sb.charAt(start); // 步骤1:暂存左边的字符 sb.setCharAt(start, sb.charAt(end)); // 步骤2:把右边字符放到左边位置 sb.setCharAt(end, temp); // 步骤3:把暂存的左边字符放到右边位置
- 移动指针
javajava 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 ↑ 空格位置第1轮循环(处理第一个单词 "olleh")
变量 值 说明 start 0 第一个单词从索引0开始 end 1 开始寻找单词结尾 内层 while:
end=1,
sb.charAt(1)='l'≠ 空格 → end=2end=2,
sb.charAt(2)='l'≠ 空格 → end=3end=3,
sb.charAt(3)='e'≠ 空格 → end=4end=4,
sb.charAt(4)='h'≠ 空格 → end=5end=5,
sb.charAt(5)=' '== 空格 → 循环结束找到单词范围:start=0, end=5(end指向空格)
javajava reverseString(sb, 0, 4); // 反转索引0~4 // "olleh" → "hello"
题目答案:
java
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;
}
}
}
这里我们还有第二种写法,代码比较简单:
java
//解法二:创建新字符数组填充。时间复杂度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开始,原来数组的指针是从最后开始。
第1轮循环(提取 "world")
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 = 6
第2轮循环(提取 "hello")
java
// 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(n)
不需要多次反转操作
缺点:
需要额外 O(n) 空间
代码稍长
对比两种解法
| 特性 | 解法一(原地反转) | 解法二(新数组填充) |
|---|---|---|
| 空间复杂度 | O(1) | O(n) |
| 代码复杂度 | 较高(需要多个辅助方法) | 较低(一个方法搞定) |
| 理解难度 | 较难 | 容易 |
| 适用场景 | 要求原地修改 | 无空间限制 |
结语:如果对你有帮助,请**点赞,关注,收藏,**你的支持就是我最大的鼓励!

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=260xs1j6t3s