算法题 反转字符串中的单词 III

557. 反转字符串中的单词 III

问题描述

给定一个字符串 s,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。

示例

输入: s = "Let's take LeetCode contest"
输出: "s'teL ekat edoCteeL tsetnoc"

输入: s = "God Ding"
输出: "doG gniD"

输入: s = "Hello"
输出: "olleH"

算法思路

双指针法

  1. 遍历字符串,找到每个单词的起始和结束位置
  2. 对每个单词使用双指针进行原地反转(或构建新字符串)
  3. 遇到空格时,处理前一个单词
  4. 处理最后一个单词(字符串末尾无空格)

分隔反转法

  1. 按空格分割字符串得到单词数组
  2. 对每个单词进行反转
  3. 用空格重新连接单词

栈法

  1. 遍历字符串,将非空格字符压入栈
  2. 遇到空格时,将栈中字符弹出并添加到结果
  3. 添加空格
  4. 处理最后一个单词

代码实现

方法一:双指针法(推荐)

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"

方法一执行过程

  1. i=0-4:字符"L,e,t,',s",无空格
  2. i=5:遇到空格,反转[0,4]"s'teL"
  3. start=6
  4. i=6-9:字符"t,a,k,e",无空格
  5. i=10:遇到空格,反转[6,9]"ekat"
  6. start=11
  7. i=11-18:字符"L,e,e,t,C,o,d,e",无空格
  8. i=19:遇到空格,反转[11,18]"edoCteeL"
  9. start=20
  10. i=20-26:字符"c,o,n,t,e,s,t",到达末尾
  11. i=27i==chars.length,反转[20,26]"tsetnoc"
  12. 返回 "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"
}

关键点

  1. 单词定义

    • 由非空格字符组成的序列
    • 空格作为单词分隔符
    • 保留原始空格位置和数量
  2. 反转范围

    • 只反转单词内部字符
    • 不改变单词之间的相对位置
    • 不改变空格的位置和数量
  3. 边界处理

    • 空字符串
    • 只有空格
    • 单字符单词
    • 字符串开头/结尾的空格
  4. 双指针

    • 使用start记录单词起始位置
    • 遇到空格或末尾时处理当前单词
    • 原地反转避免额外空间
  5. 性能优化

    • 方法一最高效,原地操作
    • 避免频繁的字符串拼接
    • 预分配字符数组或StringBuilder容量

常见问题

  1. 为什么方法一中循环条件是 i <= chars.length

    • 为了处理最后一个单词(末尾无空格)
    • i == chars.length 时,触发最后一个单词的反转
  2. 如果字符串开头有空格怎么办?

    • 算法会正确处理:start 会指向第一个非空格字符
    • 开头的空格会被保留,不影响单词反转
  3. 方法二中的 split(" ")split("\\s+") 有什么区别?

    • split(" ") 按单个空格分割,保留空字符串
    • split("\\s+") 按一个或多个空白字符分割,不保留空字符串
    • 本题需要保留原始空格结构,所以使用 split(" ")
  4. 如何处理制表符、换行符等其他空白字符?

    • 题目说明只考虑空格
    • 如果需要处理其他空白字符,可以修改判断条件:Character.isWhitespace(c)
  5. 方法一是否真的"原地"操作?

    • 在字符数组层面是原地操作
    • 但由于String在Java中不可变,最终需要创建新字符串
    • 相比其他方法,减少了中间对象的创建
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值