【LeetCode刷题日记】:151翻转字符串的单词(两种解法)

🔥个人主页:北极的代码(欢迎来访)

🎬作者简介:java后端学习者

❄️个人专栏:苍穹外卖日记SSM框架深入JavaWeb

命运的结局尽可永在,不屈的挑战却不可须臾或缺!

摘要:

本题要求翻转字符串中的单词顺序,并去除首尾及单词间多余空格。核心解法分三步:先去除多余空格 (利用双指针和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 <= 104
  • s 包含英文大小写字母、数字和空格 ' '
  • 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;
    }

去除空格之后,我们要做的就是翻转整个字符串了

利用双指针法:

逐步拆解

  1. 循环条件 while (start < end)

只要左指针在右指针左边,就继续交换。当两个指针相遇或交错时,说明已经全部交换完毕。

  1. 交换三部曲
java 复制代码
java

char temp = sb.charAt(start);        // 步骤1:暂存左边的字符
sb.setCharAt(start, sb.charAt(end)); // 步骤2:把右边字符放到左边位置
sb.setCharAt(end, temp);             // 步骤3:把暂存的左边字符放到右边位置
  1. 移动指针
java 复制代码
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
                      ↑
                   空格位置
第1轮循环(处理第一个单词 "olleh")
变量 说明
start 0 第一个单词从索引0开始
end 1 开始寻找单词结尾

内层 while

  • end=1, sb.charAt(1)='l' ≠ 空格 → end=2

  • end=2, sb.charAt(2)='l' ≠ 空格 → end=3

  • end=3, sb.charAt(3)='e' ≠ 空格 → end=4

  • end=4, sb.charAt(4)='h' ≠ 空格 → end=5

  • end=5, sb.charAt(5)=' ' == 空格 → 循环结束

找到单词范围:start=0, end=5(end指向空格)

java 复制代码
java

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

相关推荐
lifallen2 小时前
Flink 深度解析:从 TM、Task、Operator、UDF 到 Mailbox 与 OperatorChain
java·大数据·flink
Seven972 小时前
【从0到1构建一个ClaudeAgent】协作-Worktree+任务隔离
java
XS0301062 小时前
Java 基础(五)值传递
java·开发语言
会编程的土豆2 小时前
【日常做题】栈 中缀前缀后缀
开发语言·数据结构·算法
阿扬ABCD2 小时前
python项目:外星人入侵小游戏
开发语言·python·pygame
进击的荆棘2 小时前
递归、搜索与回溯——回溯
数据结构·c++·算法·leetcode·dfs
倒霉蛋小马2 小时前
SpringBoot3中配置Knife4j
java·spring boot·后端
我叫黑大帅2 小时前
从零实现一个完整 RAG 系统:基于 Eino 框架的检索增强生成实战
后端·面试·go
NotFound4862 小时前
实战分享怎样实现Spring Boot 中基于 WebClient 的 SSE 流式接口操作
java·spring boot·后端