那一次,简单的字符串反转任务差点让我“翻车”… 🤯
嘿,各位码农同胞们!我是你们的老朋友,一个在代码世界里摸爬滚打了多年的老兵。今天,我想跟你们聊聊一个看似简单,却内藏玄机的话题:字符串处理。
故事要从我最近接手的一个小需求说起…
我遇到了什么问题?🤔
我们正在开发一个内容创作平台,类似于一个博客系统。产品经理提出了一个很酷的功能:为了增强SEO效果和内容的可发现性,系统需要根据用户输入的文章标题,自动生成一个“反转关键词摘要”。
比如,用户输入了一个标题: 我在阿尔卑斯山 进行了一次史诗级的徒步旅行
系统需要自动生成一个摘要,格式是:旅行 徒步 史诗级的 一次 进行了 阿尔卑斯山 我在
听起来很简单,对吧?不就是把单词顺序反过来嘛。我当时也是这么想的,心想:“这不就是 split
一下, reverse
一下,再 join
一下就完事儿了?分分钟搞定!” 😉
然而,现实很快就给了我一记响亮的耳光。用户的输入是“自由”的,甚至是“放纵”的:
- 开头和结尾可能有N个空格:
" hello world "
- 单词之间可能有N个空格:
"a good example"
我最初那套“三板斧”下来,得到的结果简直是一场灾难:各种多余的空格、空字符串元素,拼接出来的格式一塌糊涂。这可不行,我们交付给用户的必须是干净、规范的字符串。
这个问题,本质上就是力扣上的这道经典题目:
给你一个字符串
s
,请你反转字符串中 单词 的顺序。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
提示解读时间 🧐:
1 <= s.length <= 10^4
: 字符串长度不小,暗示我们时间复杂度最好是线性的O(N)
,O(N^2)
可能会超时。s 中 至少存在一个 单词
: 谢天谢地,不用处理空输入或者纯空格的特殊情况了。- 进阶提示:
O(1)
额外空间的原地解法。这是面试官最喜欢问的环节,考验你是否能超越简单的API调用,深入到底层去优化。
我是如何用算法思维解决的!💡
面对这个“棘手”的烂摊子,我冷静下来,意识到这正是展示一个程序员基本功的绝佳机会。我尝试了三种不同的策略,从“快速搞定”到“秀出肌肉”。
解法一:API三连,大力出奇迹! (解法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
不可变,我们没法真正“原地”,但可以用 StringBuilder
或 char[]
来模拟这个思想。
核心思想:三步翻转法,非常经典!
- 清理字符串:将
" the sky "
清理成"the sky"
。 - 整体反转:
"the sky"
->"yks eht"
。 - 单词反转:
"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 ... */ }
- 恍然大悟:第一次看到这个解法时,我真的惊了!原来反转操作还可以这么玩。它完美地诠释了算法的优雅和巧妙。理解了这个,很多类似的原地操作问题都迎刃而解了。
举一反三:这些技能还能用在哪?
掌握了这些字符串处理技巧,可不仅仅是为了应付一道算法题。在实际开发中,它们无处不在:
- 用户输入验证与清洗 (Sanitization):任何接受用户输入的系统,比如注册表单、评论区、搜索框,都需要对输入进行清洗,去除多余空格、非法字符,这正是
cleanSpaces
的用武之地。 - ETL与数据迁移:从旧系统或不同来源(如CSV文件)导入数据时,数据格式往往千奇百怪。我们需要编写健壮的脚本来解析、清洗、转换这些数据,
split
、trim
和手动解析都是常用武器。 - 日志分析:分析服务器日志时,经常需要按特定分隔符(如空格、制表符)提取字段,并可能需要重新排序以进行聚合分析。
- URL Slug生成:将文章标题
My New Awesome Post
转换为my-new-awesome-post
,也需要处理空格并用连字符替换。
更多练习,成为字符串大师!
如果你觉得这道题很有趣,想继续挑战自己,这里有几道力扣上的“亲戚”题目,快去试试身手吧!
- 186. 反转字符串中的单词 II:这道题直接要求你用
O(1)
空间完成,是解法3的直接应用场景。 - 557. 反转字符串中的单词 III:不反转单词顺序,而是反转每个单词内部的字母。
- 58. 最后一个单词的长度:另一个考察从字符串末尾解析单词的好题目。
希望这次的分享能对大家有所启发。记住,每一个看似简单的需求背后,都可能隐藏着对我们基本功的深刻考验。下次再遇到类似问题,希望你也能从容应对,甚至秀出更优雅的解决方案!我们下次再见!👋