557. 反转字符串中的单词 III
问题描述
给定一个字符串 s,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。
示例:
输入: s = "Let's take LeetCode contest"
输出: "s'teL ekat edoCteeL tsetnoc"
输入: s = "God Ding"
输出: "doG gniD"
输入: s = "Hello"
输出: "olleH"
算法思路
双指针法:
- 遍历字符串,找到每个单词的起始和结束位置
- 对每个单词使用双指针进行原地反转(或构建新字符串)
- 遇到空格时,处理前一个单词
- 处理最后一个单词(字符串末尾无空格)
分隔反转法:
- 按空格分割字符串得到单词数组
- 对每个单词进行反转
- 用空格重新连接单词
栈法:
- 遍历字符串,将非空格字符压入栈
- 遇到空格时,将栈中字符弹出并添加到结果
- 添加空格
- 处理最后一个单词
代码实现
方法一:双指针法(推荐)
class Solution {
/**
* 反转字符串中的每个单词
* 使用双指针原地反转每个单词
*
* @param s 输入字符串
* @return 每个单词反转后的字符串
*/
public String reverseWords(String s) {
if (s == null || s.length() <= 1) {
return s;
}
char[] chars = s.toCharArray();
int start = 0; // 当前单词的起始位置
for (int i = 0; i <= chars.length; i++) {
// 遇到空格或到达字符串末尾,处理当前单词
if (i == chars.length || chars[i] == ' ') {
// 反转从start到i-1的字符
reverse(chars, start, i - 1);
// 更新下一个单词的起始位置
start = i + 1;
}
}
return new String(chars);
}
/**
* 反转字符数组中指定范围的字符
*
* @param chars 字符数组
* @param left 起始位置
* @param right 结束位置
*/
private void reverse(char[] chars, int left, int right) {
while (left < right) {
char temp = chars[left];
chars[left] = chars[right];
chars[right] = temp;
left++;
right--;
}
}
}
方法二:分隔反转法
class Solution {
/**
* 反转字符串中的每个单词 - 分隔反转法
*
* @param s 输入字符串
* @return 每个单词反转后的字符串
*/
public String reverseWords(String s) {
if (s == null) {
return s;
}
// 按空格分割
String[] words = s.split(" ");
// 反转每个单词
for (int i = 0; i < words.length; i++) {
words[i] = reverseWord(words[i]);
}
// 用空格重新连接
return String.join(" ", words);
}
/**
* 反转单个单词
*/
private String reverseWord(String word) {
if (word.length() <= 1) {
return word;
}
char[] chars = word.toCharArray();
int left = 0, right = chars.length - 1;
while (left < right) {
char temp = chars[left];
chars[left] = chars[right];
chars[right] = temp;
left++;
right--;
}
return new String(chars);
}
}
方法三:栈法
import java.util.*;
class Solution {
/**
* 反转字符串中的每个单词 - 栈法
*
* @param s 输入字符串
* @return 每个单词反转后的字符串
*/
public String reverseWords(String s) {
if (s == null) {
return s;
}
StringBuilder result = new StringBuilder();
Stack<Character> stack = new Stack<>();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c != ' ') {
// 非空格字符压入栈
stack.push(c);
} else {
// 遇到空格,弹出栈中所有字符
while (!stack.isEmpty()) {
result.append(stack.pop());
}
// 添加空格
result.append(' ');
}
}
// 处理最后一个单词(字符串末尾无空格)
while (!stack.isEmpty()) {
result.append(stack.pop());
}
return result.toString();
}
}
方法四:StringBuilder法
class Solution {
/**
* 反转字符串中的每个单词 - StringBuilder法
*
* @param s 输入字符串
* @return 每个单词反转后的字符串
*/
public String reverseWords(String s) {
if (s == null) {
return s;
}
StringBuilder result = new StringBuilder();
StringBuilder word = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c != ' ') {
// 构建当前单词
word.append(c);
} else {
// 反转当前单词并添加到结果
result.append(word.reverse()).append(' ');
// 重置单词构建器
word.setLength(0);
}
}
// 处理最后一个单词
result.append(word.reverse());
return result.toString();
}
}
算法分析
- 时间复杂度:O(n)
- 所有方法都需要遍历字符串一次
- 每个字符被处理常数次
- 空间复杂度:
- 方法一:O(n),转换为字符数组
- 方法二:O(n),分割存储单词
- 方法三:O(n),栈空间
- 方法四:O(n),StringBuilder空间
- 方法对比:
- 方法一:最高效,原地操作,推荐使用
- 方法二:逻辑清晰,但有额外分割开销
- 方法三:使用栈,符合"反转"的直观理解
- 方法四:使用StringBuilder,代码简洁
算法过程
输入:s = "Let's take LeetCode contest"
方法一执行过程:
i=0-4:字符"L,e,t,',s",无空格i=5:遇到空格,反转[0,4]→"s'teL"start=6i=6-9:字符"t,a,k,e",无空格i=10:遇到空格,反转[6,9]→"ekat"start=11i=11-18:字符"L,e,e,t,C,o,d,e",无空格i=19:遇到空格,反转[11,18]→"edoCteeL"start=20i=20-26:字符"c,o,n,t,e,s,t",到达末尾i=27:i==chars.length,反转[20,26]→"tsetnoc"- 返回
"s'teL ekat edoCteeL tsetnoc"
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
System.out.println("Test 1: " + solution.reverseWords("Let's take LeetCode contest"));
// "s'teL ekat edoCteeL tsetnoc"
// 测试用例2:两个单词
System.out.println("Test 2: " + solution.reverseWords("God Ding"));
// "doG gniD"
// 测试用例3:单个单词
System.out.println("Test 3: " + solution.reverseWords("Hello"));
// "olleH"
// 测试用例4:空字符串
System.out.println("Test 4: " + solution.reverseWords(""));
// ""
// 测试用例5:只有空格
System.out.println("Test 5: " + solution.reverseWords(" "));
// " "
// 测试用例6:单字符单词
System.out.println("Test 6: " + solution.reverseWords("a b c"));
// "a b c"
// 测试用例7:长单词
System.out.println("Test 7: " + solution.reverseWords("programming"));
// "gnimmargorp"
// 测试用例8:包含标点符号
System.out.println("Test 8: " + solution.reverseWords("Hello, World!"));
// ",olleH !dlroW"
// 测试用例9:开头和结尾有空格
System.out.println("Test 9: " + solution.reverseWords(" Hello World "));
// " olleH dlroW "
// 测试用例10:多个连续空格
System.out.println("Test 10: " + solution.reverseWords("Hello World"));
// "olleH dlroW"
}
关键点
-
单词定义:
- 由非空格字符组成的序列
- 空格作为单词分隔符
- 保留原始空格位置和数量
-
反转范围:
- 只反转单词内部字符
- 不改变单词之间的相对位置
- 不改变空格的位置和数量
-
边界处理:
- 空字符串
- 只有空格
- 单字符单词
- 字符串开头/结尾的空格
-
双指针:
- 使用start记录单词起始位置
- 遇到空格或末尾时处理当前单词
- 原地反转避免额外空间
-
性能优化:
- 方法一最高效,原地操作
- 避免频繁的字符串拼接
- 预分配字符数组或StringBuilder容量
常见问题
-
为什么方法一中循环条件是
i <= chars.length?- 为了处理最后一个单词(末尾无空格)
- 当
i == chars.length时,触发最后一个单词的反转
-
如果字符串开头有空格怎么办?
- 算法会正确处理:
start会指向第一个非空格字符 - 开头的空格会被保留,不影响单词反转
- 算法会正确处理:
-
方法二中的
split(" ")和split("\\s+")有什么区别?split(" ")按单个空格分割,保留空字符串split("\\s+")按一个或多个空白字符分割,不保留空字符串- 本题需要保留原始空格结构,所以使用
split(" ")
-
如何处理制表符、换行符等其他空白字符?
- 题目说明只考虑空格
- 如果需要处理其他空白字符,可以修改判断条件:
Character.isWhitespace(c)
-
方法一是否真的"原地"操作?
- 在字符数组层面是原地操作
- 但由于String在Java中不可变,最终需要创建新字符串
- 相比其他方法,减少了中间对象的创建
798

被折叠的 条评论
为什么被折叠?



