从凌乱到优雅:我如何驯服用户输入中的“野马”空格(151. 反转字符串中的单词)


🤯 从凌乱到优雅:我如何驯服用户输入中的“野马”空格

大家好,我是你们的老朋友,一个在代码世界里摸爬滚打多年的老兵。今天想和大家聊聊一个我最近在项目中遇到的“小”问题,以及它如何演变成一场关于字符串处理的深度探索之旅。希望我的经历能给你带来一些启发。😎

我遇到了什么问题?

上个月,我接手了一个优化任务:为我们的电商网站的后台管理系统添加一个“用户搜索词云”功能。需求很简单:把用户最近搜索的热门词汇展示出来,但要按照搜索频率倒序排列,并且格式要干净、统一。

听起来不难,对吧?然而,当我从数据库里捞出原始搜索记录时,我傻眼了。用户的输入简直是“狂野派”艺术的典范:

  • " iphone 15 pro " (前后都有空格)
  • "macbook air m2 chip" (单词间N个空格)
  • " " (甚至只有空格)

后端为了尽可能保留原始信息,将这些搜索记录用特定的分隔符(比如 ||)拼接成了一个巨大的字符串。我的任务,就是解析这个大字符串,提取出每个单词,然后按我们的业务逻辑(反转单词顺序)重新组合。

举个例子,如果一段原始记录是 "the sky is blue || a good example ",我需要处理后得到两个结果:"blue is sky the""example good a"

我面临的核心挑战,和151. 反转字符串中的单词这道题几乎一模一样:

  1. 反转单词顺序。
  2. 清理首尾多余的空格。
  3. 将单词间的多个空格压缩成一个。

“踩坑”与“顿悟”:我的三种解法探索

解法一:API组合拳,大力出奇迹!

我最初的想法是:这不就是字符串处理嘛,Java 提供了那么多强大的 API,直接用就完事了!于是,我兴致勃勃地写下了第一版代码:

// 错误示范 ❌
String[] words = s.split(" "); 
// ... 然后反转数组再拼接

然后,当我用 "a good example" 这个例子测试时,灾难发生了。split(" ") 产生了 ["a", "good", "", "", "example"] 这样的数组!中间的多个空格被分割成了空字符串。我瞬间石化… 😨

这就是我踩的第一个坑: split() 方法如果用简单的空格 " " 作为分隔符,它会忠实地把每两个空格之间的“空”也当作一个元素。

“恍然大悟”的瞬间来了! 我想起了正则表达式。\s 代表任何空白字符(包括空格、制表符等),+ 代表一个或多个。所以,"\\s+" 正是为这种情况量身定做的“清理大师”!

于是,我的最终方案,一套优雅的 API 组合拳,诞生了:

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public String reverseWordsAPI(String s) {
    // 1. trim(): 削平两端!像个园丁修剪掉字符串首尾多余的“杂草”(空格)。
    // "  hello world  " -> "hello world"
    String trimmedStr = s.trim();

    // 2. split("\\s+"): 精准爆破!用正则表达式按一个或多个空格分割,绝不产生空字符串。
    // "a good   example" -> ["a", "good", "example"]
    List<String> wordList = Arrays.asList(trimmedStr.split("\\s+"));

    // 3. Collections.reverse(): 乾坤大挪移!直接将列表中的单词顺序反转。
    // ["a", "good", "example"] -> ["example", "good", "a"]
    Collections.reverse(wordList);

    // 4. String.join(" ", ...): 完美缝合!用单个空格将所有单词优雅地连接起来。
    return String.join(" ", wordList);
}

这套组合拳代码简洁,可读性极强,对于绝大多数业务场景来说,这就是最优解。它清晰地将问题分解为“修剪-分割-反转-连接”四步,完美!

解法二:双指针,手术刀般的精准控制

API虽好,但我的技术导师向我抛出了一个问题:“如果这是在某个性能要求极高,或者不允许使用高级API的嵌入式环境,你该怎么办?”

好问题!这促使我思考更底层的实现。我决定用双指针,从后向前手动构建结果。这样可以避免 split 产生的中间数组,理论上能更好地控制内存。

思路是这样的:

  1. 我们从字符串的末尾开始扫描。
  2. 指针 j 先向前走,跳过所有尾随空格,直到找到一个单词的最后一个字符。
  3. 然后指针 ij 的位置继续向前,找到这个单词的开头(即遇到下一个空格)。
  4. 截取 [i+1, j+1] 这个区间的单词,把它加到一个 StringBuilder 中。
  5. 重复这个过程,直到扫描完整个字符串。
public String reverseWordsTwoPointers(String s) {
    // 为什么用 StringBuilder?
    // 在循环中用 `+` 拼接字符串,每次都会创建一个新的 String 对象,非常浪费性能。
    // StringBuilder 是可变的,append 操作效率极高,是循环拼接字符串的不二之选!
    StringBuilder sb = new StringBuilder();
    int j = s.length() - 1;

    while (j >= 0) {
        // 步骤1: j 指针跳过所有空格,定位到单词的尾部
        while (j >= 0 && s.charAt(j) == ' ') j--;
        if (j < 0) break; // 防止全是空格的情况

        // 步骤2: i 指针从 j 开始,找到单词的头部
        int i = j;
        while (i >= 0 && s.charAt(i) != ' ') i--;

        // 步骤3: 截取并添加单词。如果 sb 不是空的,就先加个空格
        if (sb.length() > 0) {
            sb.append(' ');
        }
        sb.append(s.substring(i + 1, j + 1));

        // 步骤4: 继续向前扫描
        j = i;
    }
    return sb.toString();
}

这个方法让我对字符串的内部结构有了更深的理解,虽然代码变长了,但每一步都在我的精准控制之下。

解法三:原地算法,挑战性能极限!

LeetCode 的进阶提示总是那么迷人:“请尝试使用 O(1) 额外空间复杂度的原地解法。

这简直是终极挑战!在 Java 里,String 是不可变的,所以“原地”操作通常意味着把它转换成 char[],然后在不创建新数组来存储单词的前提下完成所有操作。

这个解法的思想非常精妙,堪称“空间换时间”的反面教材——“逻辑换空间”:

第一步:原地清理空格。
用快慢指针 fastslowfast 勇往直前地探索,slow 则像个管家,只把 fast 找到的“有用”的东西(单词和必要的单个空格)搬到数组的前面。操作结束后,chars 数组的前 slow 个位置就是格式化好的字符串了。

第二步:整体反转。
把这个清理过的 char 数组([0, slow-1])整个反转。此时,字符串会变成一堆“乱码”,比如 "a good example" 会变成 "elpmaxe doog a"。别慌!魔法即将发生。

第三步:逐词反转。
再次遍历这个“乱码”数组,找到每一个单词(它们现在被单个空格隔开),然后把每个单词再反转一次。

  • "elpmaxe" -> "example"
  • "doog" -> "good"
  • "a" -> "a"

瞧!经过这番“负负得正”般的操作,我们得到了最终答案!

public String reverseWordsInPlace(String s) {
    char[] chars = s.toCharArray();
    // 步骤1: 移除多余空格,返回新长度
    int newLen = removeExtraSpaces(chars);
    // 步骤2: 整体反转
    reverse(chars, 0, newLen - 1);
    // 步骤3: 逐词反转
    int start = 0;
    for (int end = 0; end < newLen; end++) {
        if (chars[end] == ' ') {
            reverse(chars, start, end - 1);
            start = end + 1;
        }
    }
    reverse(chars, start, newLen - 1); // 反转最后一个单词

    return new String(chars, 0, newLen);
}
// (removeExtraSpaces 和 reverse 的辅助函数实现见上一个回答)

这个解法虽然在 Java 中因为 toCharArray() 的存在,总空间复杂度还是 O(N),但它完美地展示了 O(1) 额外空间的算法思想。这是面试中展现你算法功底的绝佳机会!

举一反三:这个模式还能用在哪?

这个“清理、反转、重组”的模式,在很多场景都非常有用:

  1. 日志处理:服务器日志通常格式固定但分隔符不一(如多个空格或Tab)。我们可以用类似的方法来解析和重排日志字段,方便分析。
  2. 数据清洗:在机器学习或数据分析前,需要对从各种来源(如网页爬虫、用户表单)获取的文本数据进行预处理,去除不规范的格式。
  3. URL参数解析:解析 URL 查询字符串,并可能需要按特定顺序重组参数。

结语

从一个看似简单的需求出发,我经历了从“API小子”到“指针大师”,再到“原地魔术师”的蜕变。这个过程让我深刻体会到:

  • 永远不要低估基础:一个简单的空格处理,背后却隐藏着对 API、数据结构和算法的深刻理解。
  • 知其然,知其所以然:了解 split 的坑,理解 StringBuilder 的优势,明白原地算法的精妙,这才是工程师的价值所在。
  • 场景决定技术:90% 的情况用 API 组合拳,快速搞定;剩下 10% 的特殊场景,你的底层功力就是你的核心竞争力。

希望我的分享对你有帮助。下次再遇到凌乱的字符串时,希望你也能像个优雅的骑士,轻松驯服它们!🚀

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值