首页 / 资讯中心 / 文章详情

力扣151题详解:反转字符串中的单词,双指针与空格处理

力扣151题详解:反转字符串中的单词,双指针与空格处理 ★ FEATURED ARTICLE
最近有朋友跟我聊力扣刷题说151题反转字符串中的单词看着简单结果一提交就是各种空指针和越界。这题我没记错的话是力扣热题100里的常客也是面试里特别喜欢拿来考察字符串处理和双指针基本功的题目。很多人以为它就是个split加reverse的送分题可真正动手写才发现里面那些空格、顺序、原地修改的细节一个不留神就翻车。这篇文章我会把这题的题目拆解、多条解题路径、边界处理、面试追问点全部聊透不管是第一次刷还是准备二刷的人都能找到能直接用的思路。1. 题目到底在考什么别被“反转”两个字骗了1.1 题目还原与示例原题描述很简洁给你一个字符串 s需要反转字符串中单词的顺序。比如输入the sky is blue输出blue is sky the。乍一看就是把字符串按空格拆开、倒序拼回去但题目里夹了两条额外要求一是单词之间只能保留一个空格二是字符串开头和结尾的多余空格要全部去掉。也就是说 hello world 这样的输入输出必须是world hello不能在末尾又带个空格。题目要求你能不能使用辅助空间进阶版本是最好用 O(1) 的额外空间原地完成反转。然而在很多语言里字符串是不可变的比如 Java 的 String所以原地反转这一要求通常要转换成“字符数组原地交换”来实现。Python 虽然写法简洁但字符串不可变如果你用 split 和 join本质上还是申请了新空间。1.2 题眼拆解空格、顺序、边界这道题真正想考的其实是三件事第一对“单词”的定义是否清晰第二对边界条件的敏感度第三能否在不借助高级 API 的情况下用底层操作完成字符串反转。很多人一上来直接调s.trim().split(\\s)然后逆序拼接看起来没问题但面试官只要追问“如果不能用正则表达式呢”“如果不能用 split 呢”思路就断了。单词的定义在这里很关键单词是由连续的非空格字符组成的多个空格分隔单词。所以在处理时核心不是“反转字符”而是“提取单词并调整顺序”。任何解法最后都要回答一个问题你是选择先把单词从字符串里抠出来还是直接在原串上做位置交换。这两种思路对应两条完全不同的代码结构。边界情况需要特别留意几种空字符串全部都是空格的字符串首尾有空格但中间只有一个空格单词之间存在多个连续空格。这些情况虽然示例里可能只给了一个但测试用例往往很毒。如果严格按照“只保留一个空格”的规则处理那么输出里不该有任何多余空格开头和结尾必须是单词字符。2. 三种主流解法从偷懒到真正有含金量2.1 直接调 API 的“快速解法”和它的代价先说说大家最熟悉的做法。以 Python 为例代码可以短到让人怀疑def reverseWords(s: str) - str: return .join(s.split())这里s.split()默认按任意空白字符拆分并且会自动滤掉空白项比如 hello world 会直接得到[hello, world]再用空格连接完美满足题目要求。Java 里对应的是s.trim().split(\\s)不过要稍微注意 split 的正则和空字符串的情况。这种写法作为练习当然没问题甚至日常开发里我也推荐这么写因为干净、不容易出错。但放在刷题和面试场景里它的价值就很有限你没法解释底层发生了什么也没法处理“必须原地修改”的进阶要求。Python 的split()会生成单词列表并分配新字符串Java 的 split 同样会创建数组和子串对象它们的时间复杂度是 O(n)空间复杂度也是 O(n)。如果题目明确要求 O(1) 空间这种解法直接不满足条件。所以我的建议是第一次刷可以先用这个写法理解题目输出规则但一定要在后面实现更底层的版本否则到了面试现场一段 split 代码很难证明你掌握了字符串处理的能力。2.2 基于双端扫描和单词拼接的思路不依赖 split 也不依赖正则的常规思路是从后往前扫描字符串每次识别出一个完整单词然后把它拼接到结果。这种方法在逻辑上非常直观也适合用来跟面试官讲清楚“我们如何手动解析单词”。具体怎么走先定义两个指针 i 和 j都从字符串末尾开始。让 i 跳过所有空格找到单词的最后一个字符然后将 j 移动到单词的开头也就是让 j 一直向前走直到遇到空格或者走到字符串开头。这样s[j1:i1]就是当前从后往前找到的第一个单词。把它追加到结果里然后继续向前跳过空格找下一个单词。由于我们是倒序遍历所以单词出现的天然顺序就是题目要求的“反转后顺序”不需要额外的栈。这种解法也不依赖正则只需要处理字符的下标移动。以一个例子说明输入 hello world 倒着扫先跳过末尾两个空格i 停在dj 往左移动到o再往前遇到空格得到world继续向左跳过三个空格i 停在oj 往左移动到h再往前遇到开头得到hello。按找到的顺序拼接就是world hello。这个解法的空间复杂度依然不理想因为结果拼接会生成新字符串但它在面试中很容易讲明白而且比纯 split 更显功底。如果你准备时间不充裕这条代码可以作为你的“保底方案”。2.3 高级路线先整体反转再局部反转单词这是进阶解法的核心思路先反转整个字符串这样单词的顺序就倒过来了但因为单词内部字符也跟着反转所以第二次需要把每个单词内部再反转一次。简单说就是整串反转 → 去除多余空格 → 单词内部反转。举个例子原始the sky is blue整体反转eulb si yks eht再逐个反转单词内部blue is sky the这种方法的好处是可以用原地操作完成直接在字符数组上做交换空间复杂度 O(1)。它也顺便处理了空格在扫描过程中把相邻空格压缩成单个然后移动字符到前面最后截断多余部分。这样做的好处是不管原始字符串里有多少个连续空格最终结果都会保持单词间只有一个空格、首尾无空格。整个过程可以完全在同一个 char 数组上进行不需要额外创建字符串数组或列表。这条路线之所以“高级”是因为它把“反转”分解成了两个阶段每一阶段都是简单操作组合起来却完成了包含空格清理在内的复杂要求。面试官通常更认可这种思路因为它展示了你对字符串底层操作的理解而且天然满足进阶要求。3. 手把手实现从思路到能跑的代码3.1 先确定用什么语言和模板我平时演示高频题更喜欢用 Java因为它的字符串不可变能逼着你处理字符数组正好契合这题的进阶要求。如果你用 Python也可以基于列表实现类似的原地操作。下面的示例我会用 Java 写一个完整实现然后补充 Python 的对应版本。用 Java 时第一步是把字符串转成字符数组char[] chars s.toCharArray();之后所有操作都在这个数组上完成。由于需要原地修改最终答案要用new String(chars, 0, newLength)截取有效部分。整个过程分为三个方法反转整个区间reverse(chars, left, right)压缩空格并返回新长度trimSpaces(chars)最后反转每个单词。class Solution { public String reverseWords(String s) { char[] chars s.toCharArray(); int len trimSpaces(chars); reverse(chars, 0, len - 1); int start 0; for (int end 0; end len; end) { if (end len || chars[end] ) { reverse(chars, start, end - 1); start end 1; } } return new String(chars, 0, len); } private int trimSpaces(char[] chars) { int left 0, right chars.length - 1; while (left right chars[left] ) left; while (left right chars[right] ) right--; int write 0; boolean space false; for (int i left; i right; i) { if (chars[i] ! ) { chars[write] chars[i]; space false; } else if (!space) { chars[write] ; space true; } } return write; } private void reverse(char[] chars, int left, int right) { while (left right) { char tmp chars[left]; chars[left] chars[right]; chars[right] tmp; left; right--; } } }3.2 逐行解释关键步骤先看trimSpaces这一步。它做了两件事首先用两个指针找到去掉首尾空格后的有效区间然后从左往右扫描如果遇到非空格字符就直接往前写如果遇到空格且前一个不是空格就写入一个空格。这里我用了一个布尔变量space来标记上一次写入的是不是空格。这样即使原字符串里有连续三个空格最终也只会保留一个。最后返回写入的长度write这个长度就是清理后的有效长度也是整体反转和单词定位的边界。为什么先做空格清理再做整体反转因为清理之后字符串首尾就是单词字符反转后依然首尾是单词字符处理起来省心。如果先整体反转再去清理也不是不行但中间会多一层空格判断的复杂度。个人建议顺序不要乱先清理、再整体反转、最后逐个单词反转。这其实是很多标准解法的范式面试时可以直接复用。接着看整体反转。reverse(chars, 0, len - 1)用的是最普通的双指针交换没有太多可说的但要注意边界传入的 right 是有效长度减一也就是最后一个字符的下标千万别写成len否则会把已经清理掉的空闲位置也反转进去污染结果。最后是逐个单词反转。我用了循环for (int end 0; end len; end)当end len或chars[end] 时说明正好走完了一个单词。这时调用reverse(chars, start, end - 1)反转这个单词内部然后把start更新为end 1指向下一个单词的开头。这里注意end len这个分支很关键因为清理后的字符串末尾没有空格所以最后一个单词结束后不会有空格标志必须靠end len触发反转。很多人丢了这个条件最后一个单词就没被反转结果前面对整体反转的功夫白费了。3.3 Python 的原地思路对照Python 虽然字符串不可变但可以把字符串转成 list of characters也走同样的三步。代码如下def reverseWords(s: str) - str: chars list(s) # 清理多余空格 left, right 0, len(chars) - 1 while left right and chars[left] : left 1 while left right and chars[right] : right - 1 write 0 space False for i in range(left, right 1): if chars[i] ! : chars[write] chars[i] write 1 space False elif not space: chars[write] write 1 space True effective write - 1 # 反转整个有效区间 chars[:effective 1] reversed(chars[:effective 1]) # 反转每个单词 start 0 for end in range(effective 1): if end effective or chars[end 1] : chars[start:end 1] reversed(chars[start:end 1]) start end 2 return .join(chars[:effective 1])这里有个小细节循环里我判断end effective or chars[end 1] 用“下一个位置是空格”来定位单词边界。两种写法都可以但要注意别越界。Python 用切片反转更简洁但如果你要跟面试官讲“实际上和 Java 的交换一样”心里得有底。从代码量上看Python 的循环条件和 Java 略有差异但原理同构。我建议你选一种语言吃透再把另一种也写下因为面试时你可能会被要求现场切换语言提前准备能省很多时间。4. 常见坑与排错实录这些坑我基本都踩过4.1 空格处理的三类典型错误第一类是没清理首尾空格就整体反转。比如输入 hello world 整体反转会得到 dlrow olleh 你再去反转单词首尾还是有空格最终结果不符合要求。所以要严格先做空格清理或者在做完整体反转后额外截断但那样代码会更乱。第二类是连续多空格没有压缩。很多人只用了String.trim()然后按单个空格 split遇到hello world时split 出来的数组里会混入空字符串导致输出多出一堆空格。用 Java 的split(\\s)可以解决但如果你不想用正则就一定要在图里的压缩步骤里处理。第三类是单词反转边界出错。最常见的是把最后一个单词漏掉因为代码里只判断chars[end] 而清理后的字符串末尾没有空格。我见过很多版本跑示例通过但在没有多余空格的字符串上最后一段保持原样。归根到底就是没写end len这个终止条件。这个坑非常好记有效字符串最后要么有空格要么结束。如果你只用空格作为单词边界就必须额外处理“结束”这个边界。4.2 复杂度分析别答错这题的时间复杂度是 O(n)无论哪种合法解法都是线性扫描因为你至少要把每个字符看一遍。但是空间复杂度要区分版本split版本是 O(n)后序遍历拼接版本也是 O(n)因为结果字符串需要新内存只有字符数组原地操作版本是 O(1) 额外空间不考虑结果字符串本身的存储。面试答复杂度时别只说“O(n)”要说明空间是 O(1) 还是 O(n)。因为进阶要求明确写了额外空间 O(1)如果你答 split 版本是 O(1)那基本就凉了。实际例子Java 的String[] arr s.split( )一定会创建一个新数组这不算 O(1)。原地版本虽然使用了字符数组但那是对输入字符串的拷贝通常可以解释为修改输入数据结构。如果面试官较真你可以说“假设输入允许被修改直接在原数组上操作额外辅助空间只是几个变量所以是 O(1)”。4.3 面试官追问的底层逻辑这道题在面试里最常见的追问是“能不能不用 split”这背后问的是你有没有掌握手动解析字符串的能力而不是只会调库。第二个常见追问是“如果字符串很长内存受限怎么办”这时候你要能说出原地反转思路和 O(1) 空间复杂度。还有一个细节有些面试官会问“如果单词定义不同怎么办比如把数字也当成单词或者要求按标点分隔”实际上这个思路依然通用先识别单词边界再决定是提取还是反转。只要把“空格”这个分隔符换成任意分隔条件整个算法框架不变。所以真正值得记住的不是代码而是“先整体处理再局部处理”的两阶段思想。你可以在白板上画出“整串反转→局部反转”的推演面试官会觉得你对底层理解很扎实。5. 这道题怎么刷才划算策略、扩展与个人建议5.1 它在力扣热题100里的定位151题在力扣热题100中属于字符串章节里的基础题。它不像动态规划那样需要复杂的推导也不像图论那样需要大量模板它是纯粹的双指针与字符串处理题。这类题特别适合用来练习“先想清楚边界再动笔”的刷题习惯。很多刷题者喜欢一上来就写代码结果各种边界错误这题就是一个很好的矫正器。从面试频率来看反转类问题几乎是必考系列。LeetCode 上还有反转链表、反转数组、反转区间等同类题151 题是少数同时包含“反转字符串”和“单词切片”的题目。刷透这一道很多变体都会顺手很多。这也是为什么我建议不要停留在 split 解法至少要把“整串反转局部反转”的思路练熟。5.2 同类型题目串联做字符串反转的变体很多我建议按组刷题目核心区别刷题要点344. 反转字符串只反转整个字符数组双指针交换无空格处理541. 反转字符串 II每2k个字符反转前k个分段处理注意循环边界151. 反转字符串中的单词单词顺序反转单词内部不反转先整串反转再局部反转单词557. 反转字符串中的单词 III单词内部反转顺序不变同样是局部反转但跳过整串反转把这四题放在一起看你会意识到“反转”这个动作在不同题目里被拆解成了不同的组合整体反转改顺序、局部反转改内部、分段控制只处理一部分。151 题把整体反转和局部反转都用上了所以它是这个系列里的枢纽题。刷完 151再回来看 557你会发现不过是少做第一步。5.3 我的实测心得与建议我最早刷这题时也是先写了 split 版本虽然秒过但后来在模拟面试里被问“如果不能用 split”直接卡住。后来把整串反转的方法做了三遍才真正理解为什么“先清理空格再整串反转”最顺手。这里分享一个我自己的习惯写这类字符串题时我会在代码的注释里标注三个边界条件空输入、全空格、末尾单词。每次写完代码先对着这三个用例人工走一遍而不是直接提交。运行失败再改虽然也能过但面试时没有提交按钮所以人工走查能力才是关键。如果你现在刚接触这道题我建议按这个顺序做先用 split 解法理解输出规则 → 再用双端扫描拼接法写一版 → 最后尝试原地反转法。三版代码都写一遍你才算真正“刷”过这道题而不是“看过”这道题。特别是原地反转法强烈建议你在编辑器里一行一行打出来别复制粘贴因为手打会让你注意到很多细节比如start end 1和end len这种边界条件。最后说一个很多人忽略的小技巧在处理字符串空格时与其不断记忆“什么时候加空格”不如先压缩空格到规范格式再统一拼接。这样代码结构更清晰也不容易漏。比如你可以在循环里先写单词遇到新单词前再插入空格用if (res.length() 0) res.append( )控制。这个套路在很多字符串题里都能复用比如实现简单 CSV 解析或自定义 split效果非常稳。
阅读完成 · 觉得有帮助?
咨询建站