那一次,简单的字符串反转任务差点让我“翻车”(151. 反转字符串中的单词)


那一次,简单的字符串反转任务差点让我“翻车”… 🤯

嘿,各位码农同胞们!我是你们的老朋友,一个在代码世界里摸爬滚打了多年的老兵。今天,我想跟你们聊聊一个看似简单,却内藏玄机的话题:字符串处理

故事要从我最近接手的一个小需求说起…

我遇到了什么问题?🤔

我们正在开发一个内容创作平台,类似于一个博客系统。产品经理提出了一个很酷的功能:为了增强SEO效果和内容的可发现性,系统需要根据用户输入的文章标题,自动生成一个“反转关键词摘要”。

比如,用户输入了一个标题: 我在阿尔卑斯山 进行了一次史诗级的徒步旅行

系统需要自动生成一个摘要,格式是:旅行 徒步 史诗级的 一次 进行了 阿尔卑斯山 我在

听起来很简单,对吧?不就是把单词顺序反过来嘛。我当时也是这么想的,心想:“这不就是 split 一下, reverse 一下,再 join 一下就完事儿了?分分钟搞定!” 😉

然而,现实很快就给了我一记响亮的耳光。用户的输入是“自由”的,甚至是“放纵”的:

  1. 开头和结尾可能有N个空格" hello world "
  2. 单词之间可能有N个空格"a good example"

我最初那套“三板斧”下来,得到的结果简直是一场灾难:各种多余的空格、空字符串元素,拼接出来的格式一塌糊涂。这可不行,我们交付给用户的必须是干净、规范的字符串。

这个问题,本质上就是力扣上的这道经典题目:

LeetCode 151. 反转字符串中的单词

给你一个字符串 s ,请你反转字符串中 单词 的顺序。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。

提示解读时间 🧐:

  • 1 <= s.length <= 10^4: 字符串长度不小,暗示我们时间复杂度最好是线性的 O(N)O(N^2) 可能会超时。
  • s 中 至少存在一个 单词: 谢天谢地,不用处理空输入或者纯空格的特殊情况了。
  • 进阶提示: O(1) 额外空间的原地解法。这是面试官最喜欢问的环节,考验你是否能超越简单的API调用,深入到底层去优化。
我是如何用算法思维解决的!💡

面对这个“棘手”的烂摊子,我冷静下来,意识到这正是展示一个程序员基本功的绝佳机会。我尝试了三种不同的策略,从“快速搞定”到“秀出肌肉”。

解法一:API三连,大力出奇迹! (解法1)

这是最符合直觉、也是最实用的解法。作为一个追求效率的开发者,我们首先想到的当然是利用语言提供的强大工具箱。

思路:分三步走,简单粗暴。

  1. 清理两端 -> 2. 切分单词 -> 3. 反向拼接
/*
 * 思路:利用Java内置API三步走,简单高效。
 * 为什么用这些API?
 * - s.trim(): 一步到位清除首尾空格,比自己写循环判断高效且不易出错。
 * - s.split("\\s+"): 这是精髓!用正则表达式 `\\s+` (一个或多个空白符)
 *   来分割,完美解决了单词间多个空格的问题。如果用 " " 分割,会产生空字符串数组元素,后续处理很麻烦。
 * - StringBuilder: 在循环中拼接字符串,用StringBuilder是Java开发的基本素养。
 *   直接用 `+` 会频繁创建新String对象,性能开销大。
 */
public String reverseWords(String s) {
    // 1. 清理两端:干掉那些烦人的首尾空格
    String trimmedStr = s.trim();

    // 2. 切分单词:这是我恍然大悟的瞬间!用 "\\s+" 而不是 " "
    String[] words = trimmedStr.split("\\s+");

    // 3. 反向拼接:从后往前,构建我们想要的最终结果
    StringBuilder result = new StringBuilder();
    for (int i = words.length - 1; i >= 0; i--) {
        result.append(words[i]);
        if (i > 0) { // 最后一个单词后面可不能加空格哦
            result.append(" ");
        }
    }
    return result.toString();
}
  • 踩坑经验:我一开始真的用了 split(" "),结果对于 "a b" 这样的输入,切出来是 ["a", "", "", "b"]。循环处理时还要加一堆 if 来判断空字符串,代码又丑又乱。
  • 恍然大悟:当我记起正则表达式 \\s+ 时,感觉整个世界都清净了。一个简单的 + 号,优雅地解决了所有中间多余空格的问题。这就是工具的威力!
解法二:双指针扫描,自己动手丰衣足食 (解法2)

API虽好,但面试官或者架构师可能会追问:“如果不用 split,你怎么实现?” 这个问题考验的是你对字符串的底层操作能力。

思路:我们不依赖 split,而是从后往前手动找单词。

/*
 * 思路:不依赖API,从后向前手动扫描和截取单词。
 * 为什么用双指针?
 * - 指针 i 负责整体扫描,指针 j 负责标记单词的边界。
 *   这种“快慢”或“左右”指针的组合是处理数组/字符串问题的利器。
 * - s.substring(): 这是Java中截取子串的标准方法,性能良好。
 */
public String reverseWords(String s) {
    StringBuilder result = new StringBuilder();
    int i = s.length() - 1;
    while (i >= 0) {
        // Step 1: 从后往前,跳过所有空格
        while (i >= 0 && s.charAt(i) == ' ') i--;
        if (i < 0) break; // 扫描完了
      
        // Step 2: 找到了一个单词的结尾,用 j 记下来
        int j = i;
      
        // Step 3: 继续往前,找到这个单词的开头
        while (i >= 0 && s.charAt(i) != ' ') i--;
      
        // Step 4: 截取单词并拼接。注意substring的用法!
        // 如果result里已经有东西了,先加个空格分隔符
        if (result.length() > 0) result.append(" ");
        result.append(s.substring(i + 1, j + 1));
    }
    return result.toString();
}
  • 恍然大悟:从后往前扫描真的太妙了!因为我们是反转单词顺序,所以从后往前找到的第一个单词,正好就是我们结果字符串里的第一个单词。这样一来,拼接的逻辑就变得非常顺畅,不用先存到列表再反转,直接按顺序 append 就好。
解法三:原地反转,秀出你的空间复杂度理解 (解法3 - 进阶)

这是回应进阶挑战的“王炸”解法。它体现了对算法和内存管理的深刻理解。虽然在Java里 String 不可变,我们没法真正“原地”,但可以用 StringBuilderchar[] 来模拟这个思想。

核心思想:三步翻转法,非常经典!

  1. 清理字符串:将 " the sky " 清理成 "the sky"
  2. 整体反转"the sky" -> "yks eht"
  3. 单词反转"yks" -> "sky", "eht" -> "the"。最终得到 "sky the"

这是一个非常巧妙的双重反转技巧:(W1 W2)^R = W2^R W1^R

/*
 * 思路:模拟原地操作,通过“整体反转 + 局部反转”实现单词顺序的反转。
 * 为什么这么做?
 * - 这是解决此类问题的经典模式,能将空间复杂度降到O(1)(在C++等语言中)。
 * - 它将复杂问题分解为几个独立的、更简单的反转操作,逻辑清晰。
 */
public String reverseWords(String s) {
    // 1. 清理空格,得到一个紧凑的StringBuilder
    StringBuilder sb = cleanSpaces(s);
    // 2. 整体反转
    reverse(sb, 0, sb.length() - 1);
    // 3. 逐个单词反转
    int start = 0;
    for (int end = 0; end <= sb.length(); end++) {
        if (end == sb.length() || sb.charAt(end) == ' ') {
            reverse(sb, start, end - 1);
            start = end + 1;
        }
    }
    return sb.toString();
}
// 辅助方法1: 清理所有多余的空格
private StringBuilder cleanSpaces(String s) { /* ... 代码见上方解法3 ... */ }
// 辅助方法2: 反转StringBuilder的指定部分
private void reverse(StringBuilder sb, int start, int end) { /* ... 代码见上方解法3 ... */ }
  • 恍然大悟:第一次看到这个解法时,我真的惊了!原来反转操作还可以这么玩。它完美地诠释了算法的优雅和巧妙。理解了这个,很多类似的原地操作问题都迎刃而解了。
举一反三:这些技能还能用在哪?

掌握了这些字符串处理技巧,可不仅仅是为了应付一道算法题。在实际开发中,它们无处不在:

  1. 用户输入验证与清洗 (Sanitization):任何接受用户输入的系统,比如注册表单、评论区、搜索框,都需要对输入进行清洗,去除多余空格、非法字符,这正是 cleanSpaces 的用武之地。
  2. ETL与数据迁移:从旧系统或不同来源(如CSV文件)导入数据时,数据格式往往千奇百怪。我们需要编写健壮的脚本来解析、清洗、转换这些数据,splittrim 和手动解析都是常用武器。
  3. 日志分析:分析服务器日志时,经常需要按特定分隔符(如空格、制表符)提取字段,并可能需要重新排序以进行聚合分析。
  4. URL Slug生成:将文章标题 My New Awesome Post 转换为 my-new-awesome-post,也需要处理空格并用连字符替换。
更多练习,成为字符串大师!

如果你觉得这道题很有趣,想继续挑战自己,这里有几道力扣上的“亲戚”题目,快去试试身手吧!

希望这次的分享能对大家有所启发。记住,每一个看似简单的需求背后,都可能隐藏着对我们基本功的深刻考验。下次再遇到类似问题,希望你也能从容应对,甚至秀出更优雅的解决方案!我们下次再见!👋

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值