攻克 LeetCode 186:反转字符串中的单词,你学会了吗?
一、引言
在编程的世界里,LeetCode 无疑是一块检验实力的试金石。众多大厂面试中,LeetCode 上的题目频繁出现,成为了程序员们迈向心仪岗位的必经之路。今天,我们就来深入剖析一道经典题目 ——LeetCode 186. 反转字符串中的单词。这道题看似简单,实则暗藏玄机,它不仅考察了对字符串操作的熟练程度,更考验了我们的算法思维和代码实现能力。通过对这道题的探讨,相信大家能在字符串处理的技巧上更上一层楼,为应对各种复杂的编程场景增添一份自信。那么,让我们一起揭开这道题的神秘面纱吧!
二、题目解读
(一)题目描述
对于 LeetCode 186 这道题,给定的输入是一个字符数组 s 。题目中对 “单词” 有着明确的定义,单词是一个由非空格字符组成的序列,并且在字符数组 s 里,单词之间将会由单个空格进行分隔。我们要做的就是反转这个字符数组中单词的顺序,而且必须设计并实现原地解法来解决该问题,也就是不分配额外的空间。
为了让大家更好地理解,我们来看下示例。比如示例 1 中,输入的字符数组 s 为 ["t","h","e"," ","s","k","y"," ","i","s"," ","b","l","u","e"] ,经过处理后,输出的字符数组就变为 ["b","l","u","e"," ","i","s"," ","s","k","y"," ","t","h","e"] ,可以看到单词的顺序被成功反转了。再看示例 2,当输入的字符数组 s 为 ["a"] 时,输出依然是 ["a"] ,因为只有一个单词,它本身就是反转后的结果了。
(二)限制条件分析
这道题有几个关键的限制条件需要我们重视。首先,字符数组 s 的长度范围是 1 <= s.length <= 105,我们在编写代码时要考虑到这个长度范围对算法效率等方面的影响。其次,字符数组 s 中的元素 s[i] 可以是一个英文字母(大写或小写)、数字、或是空格 ' ' ,意味着我们在处理过程中要对这些不同类型的字符进行相应判断和操作。再者,题目中明确指出 s 中至少存在一个单词,并且 s 不含前导或尾随空格,同时也保证了 s 中的每个单词都由单个空格分隔,这些条件约束了我们对输入数据的预期,也为我们设计算法时的边界情况处理提供了依据,比如在判断单词起始和结束位置等操作上,都要紧扣这些条件来准确实现代码逻辑,不然很容易出现错误或者不符合题目要求的情况。
三、解题思路剖析
(一)整体思路概述
要解决 LeetCode 186 这道 “反转字符串中的单词” 的题目,关键思路在于分两步走。第一步是先将整个字符数组进行反转,这一步操作就像是把一整串珠子先整个颠倒过来一样,为后续处理单词反转做好铺垫。第二步就是在已经反转后的字符数组基础上,去分别反转每个单词,如此一来,经过这两步操作后,就能实现题目要求的单词顺序反转啦,整体的方向把握好,后面具体实现就有章可循了。
(二)具体步骤解析
1. 反转整个字符数组
首先要反转整个字符数组是有原因的。我们想象一下,如果不先整体反转,单词原本的顺序是正向排列的,在后续去定位每个单词的边界时,就得从前往后去寻找空格等标识来区分单词,相对会比较麻烦。而当我们先把整个字符数组反转后,原本在后面的单词就到前面来了,单词之间的空格位置等关系也相应发生了变化,反而更方便我们去依据空格等条件来定位单词的边界了。比如给定字符数组 s = ["t","h","e"," ","s","k","y"," ","i","s"," ","b","l","u","e"] ,在整体反转后就变成了 ["e","u","l","b"," ","s","i"," ","y","k","s"," ","e","h","t"] ,这样后续处理起来就改变了我们判断单词起始和结束位置的方式,更利于下一步操作了。
2. 反转每个单词
在完成整体字符数组反转后,接下来就是要反转每个单词了。我们可以通过遍历字符数组的方式,以空格作为识别单词边界的标志。当遍历到空格时,就意味着前面的字符组成了一个单词,这时就可以采用双指针法或者类似的方法来对这个单词进行反转。例如,我们可以设定一个 start 指针指向单词的起始位置,一个 end 指针在遇到空格前不断后移来确定单词的范围,然后交换这两个指针所指向的字符,逐步实现单词的反转。以下是部分示例代码片段:
for (int end = 0; end < s.length; end++) {
if (s[end] == ' ') {
reverse(s, start, end - 1);
start = end + 1;
}
}
通过这样的循环遍历和操作,就能把每个单词都进行反转了。
3. 特殊情况处理
在解题过程中,还需要考虑一些特殊情况。比如当字符数组中只有一个单词的时候,像输入为 ["a"] 这种情况,因为本身就只有一个单词,那它其实就是反转后的结果了,代码逻辑中要能正确处理这种单个单词的输入,避免多余的操作导致错误。再比如对于字符串首尾可能存在空格的情况,虽然本题明确指出不含前导或尾随空格,但如果拓展题目或者在更通用的场景下,就需要提前对这些空格进行处理,可以通过一些条件判断先去除首尾空格,保证代码的完整性和正确性,让算法在各种可能的输入情况下都能准确地反转字符串中的单词。
四、代码实现示例
(一)编程语言选择
在这里,我们选择 Java 语言来进行代码实现。Java 语言有着诸多优势,首先它的语法结构清晰,代码的可读性很强,这对于我们自己编写以及后续他人阅读代码、理解逻辑都非常有帮助。而且在 LeetCode 刷题的过程中,Java 也是被广泛使用的编程语言之一,有着大量的内置函数和类库可以辅助我们高效地实现各种算法逻辑,对于解决像 “反转字符串中的单词” 这类涉及字符数组操作、逻辑判断较多的题目来说,Java 能很好地满足需求,所以选用 Java 是比较合适的选择。
(二)代码详细讲解
以下是使用 Java 实现的解决 LeetCode 186 这道题的代码:
public class LeetCode186 {
public void ReverseWords(char[] s) {
// 反转整个字符数组
reverse(s, 0, s.length - 1);
// 反转每个单词
int start = 0;
for (int end = 0; end < s.length; end++) {
if (s[end] == ' ') {
reverse(s, start, end - 1);
start = end + 1;
}
}
// 反转最后一个单词
reverse(s, start, s.length - 1);
}
private void reverse(char[] s, int left, int right) {
while (left < right) {
char temp = s[left];
s[left] = s[right];
s[right] = temp;
left++;
right--;
}
}
}
我们逐行来分析一下这段代码的功能和逻辑:
- public void ReverseWords(char[] s)函数:这是整个代码的核心函数,它接收一个字符数组 s 作为参数,目的就是按照题目要求对这个字符数组里单词的顺序进行反转。
- reverse(s, 0, s.length - 1);这行代码:它调用了下面定义的 private void reverse(char[] s, int left, int right) 私有函数,这里传入的参数是 0 和 s.length - 1 ,意思就是先对整个字符数组进行反转。为什么要这么做呢?就像我们之前解题思路里说的,先整体反转字符数组,能改变单词的位置关系,更方便后续依据空格去定位每个单词的边界来进行单词的反转操作。例如原本正向排列的字符数组,经过这一步后,后面的单词就来到前面了,方便后续处理。
- int start = 0; 这行代码:定义了一个变量 start ,它用于标记单词的起始位置,初始化为 0,后续会随着遍历字符数组不断更新它的值来准确指向每个单词开始的地方。
- for (int end = 0; end < s.length; end++) {...} 这个循环结构:通过 end 变量从字符数组的开头开始遍历,一直到数组的末尾(end < s.length 这个条件控制)。在循环体里面,有个关键的条件判断 if (s[end] == ' ') {...} ,这个判断就是在检测当前遍历到的字符是不是空格。如果是空格,那就意味着从 start 到 end - 1 这个区间的字符组成了一个单词,然后就调用 reverse(s, start, end - 1); 来对这个单词进行反转,这里再次调用了 reverse 函数,使用双指针的方式(left 和 right ,在 reverse 函数内部实现交换字符的操作)去交换单词内字符的顺序,从而实现单词的反转。接着,把 start 更新为 end + 1 ,因为下一个单词的起始位置就是当前空格的下一个字符位置了,这样就能继续去寻找下一个单词并进行相应的反转操作。
- reverse(s, start, s.length - 1); 这行代码:它的作用是反转最后一个单词。因为在前面的循环中,当遍历到倒数第二个空格时,已经反转了倒数第二个单词,而最后一个单词还没进行反转操作,所以需要通过这行代码,以最后一个单词的起始位置 start 和字符数组末尾的位置 s.length - 1 作为参数,调用 reverse 函数来反转最后一个单词,确保整个字符数组里所有单词的顺序都被正确反转了。
- private void reverse(char[] s, int left, int right) 函数:这个函数是专门用来实现反转字符区间的功能的。它接收字符数组 s 以及要反转区间的左右边界 left 和 right 作为参数。在函数内部,通过一个 while 循环,只要 left < right ,就交换 s[left] 和 s[right] 的值,然后 left 自增,right 自减,如此反复,就能实现把传入区间内的字符顺序进行反转的效果,这个函数在整体反转字符数组以及反转每个单词的过程中都起到了关键的反转操作作用。
总的来说,这段代码通过先整体反转字符数组,再逐个反转单词,最后处理最后一个单词的方式,严格遵循题目要求的原地解法(没有分配额外的空间),有效地实现了反转字符串中单词顺序的功能。
五、复杂度分析
(一)时间复杂度
在分析本题代码的时间复杂度时,我们需要考虑代码中不同操作所消耗的时间。
首先,来看 reverse(s, 0, s.length - 1); 这一步操作,它用于反转整个字符数组。在 reverse 函数内部,通过一个 while 循环来交换字符,循环执行的次数取决于字符数组 s 的长度 n,其时间复杂度是 ,这里的 n 就是 s.length。
接着,在遍历字符数组去反转每个单词的过程中,有一个 for 循环 for (int end = 0; end < s.length; end++) {...} ,这个循环同样最多会遍历整个字符数组一遍,也就是执行 n 次左右。而在循环体里面,每次遇到空格时会调用 reverse(s, start, end - 1); 来反转单词,每个单词的反转操作也是通过类似的双指针交换字符的方式实现,其时间复杂度取决于单词的长度,设单词平均长度为 m,对于每个单词反转操作时间复杂度是 ,但总体上所有单词的反转操作加起来依然是和字符数组长度 n 相关的线性时间复杂度,因为所有单词长度之和不会超过 n。
最后,reverse(s, start, s.length - 1); 反转最后一个单词这一步同样也是时间复杂度为 (m 为最后一个单词长度),同样属于整体线性复杂度的一部分。
综合来看,整个代码的时间复杂度就是 ,这里 n 为字符数组 s 的长度。和一些可能采用额外空间来辅助进行单词分离然后再重组等操作的非原地解法相比,本解法避免了额外的复杂数据结构构建以及多次遍历等情况,在时间效率上有着不错的表现,尤其是在处理大规模的输入字符数组时,能在合理的时间内完成单词顺序反转的任务,符合实际编程场景中对算法时间复杂度的要求。
(二)空间复杂度
本题的一个重要要求是实现原地解法,也就是不分配额外的空间来解决问题。
在我们给出的代码实现中,除了定义了几个指针变量(如 start、end、left、right 等)以及用于交换字符的临时变量 temp 外,并没有使用额外的数据结构来存储数据。这些指针变量和临时变量所占用的空间都是固定的、常数级别的,不会随着输入字符数组 s 的长度变化而变化。
所以,无论输入的字符数组规模多大,代码执行过程中所需要的额外空间始终保持在一个常数范围,空间复杂度就是 。这和那些需要借助新的数组或者其他复杂数据结构来先存储单词等信息再进行处理的解法相比,优势明显。在很多实际应用场景中,尤其是对内存资源有限制或者对空间效率要求较高的情况下,原地解法的低空间复杂度特点使得它能够更好地满足需求,避免因过多的额外空间占用而导致程序出现内存不足等问题,让程序可以更加稳定、高效地运行。
六、总结与拓展
(一)总结解题要点
通过对 LeetCode 186 “反转字符串中的单词” 这道题目的探讨,我们梳理出了清晰的解题思路和关键步骤。整体思路上采用分两步走的策略,先是反转整个字符数组,改变单词的位置关系,让后续依据空格定位单词边界更为便捷。接着,遍历字符数组,以空格为标识来确定单词范围,通过双指针法等手段反转每个单词,同时还要注意对特殊情况如单个单词输入、字符串首尾空格(虽然本题限定不含,但拓展场景需考虑)等进行妥善处理。
在代码实现方面,我们以 Java 语言为例,核心函数ReverseWords(char[] s)承担了主要的逻辑处理,通过调用reverse(char[] s, int left, int right)函数来完成字符区间的反转操作,先对整个字符数组反转,再在循环中反转各个单词,最后反转最后一个单词,严格遵循原地解法,不分配额外空间。
双指针法在本题处理字符串相关操作中展现出了极大的灵活性和实用性,通过巧妙地移动指针以及交换对应位置字符,实现了单词以及字符数组的反转功能。希望大家能够牢记这种处理字符串问题的思路和双指针的运用技巧,在遇到类似题目时可以快速准确地解题。
(二)相关题目推荐
如果你想进一步巩固对字符串操作以及反转相关知识点的掌握,以下是一些与之类似或相关的 LeetCode 题目推荐:
- LeetCode 151. Reverse Words in a String(翻转字符串中的单词):这道题同样是针对字符串里单词顺序进行反转的题目,不过在一些细节限制条件或者解题思路拓展上会稍有不同,可以对比练习,加深对字符串处理和单词反转逻辑的理解。
- LeetCode 557. Reverse Words in a String III(翻转字符串中的单词 III):也是聚焦于字符串中单词反转的题目,但可能在输入输出要求、具体的算法细节处理等方面存在差异,有助于拓宽我们处理不同场景下单词反转问题的能力。
- LeetCode 77. 组合(Combination):虽然这不是直接的字符串反转题目,但涉及到对元素组合的处理逻辑,在思考如何运用指针、循环以及递归等方式去生成组合的过程中,可以锻炼我们的编程思维和逻辑推导能力,对于理解像本题中处理单词边界、字符位置交换等操作背后的通用逻辑有一定帮助。
- LeetCode 344. 反转字符串(Reverse String):主要是单纯对整个字符串进行反转,相比本题少了单词分割以及单词内部反转等复杂情况,可以作为基础的反转操作练习题目,先熟练掌握整体字符串反转的代码实现和思路,为解决更复杂的包含单词的字符串反转问题打基础。
大家可以试着去做做这些题目,进一步提升自己的编程思维和解题能力,不断积累经验,在面对各类算法题目时都能游刃有余。
七、结束语
编程之路漫漫,LeetCode 的每一道题都是我们前进路上的基石。通过对这道 “反转字符串中的单词” 题目的深入学习,我们不仅掌握了一种解决特定问题的方法,更重要的是提升了我们的算法思维和编程能力。希望大家在今后的刷题过程中,能够保持耐心和毅力,不断总结经验,举一反三。如果你在解题过程中有任何独特的思路或者遇到了什么问题,欢迎在评论区留言分享,让我们一起在编程的海洋中畅游,向着更高的目标前进!
源码
我们的项目源码可以在 GitHub 上获取哦,具体地址为LeetCode186。大家如果想要深入了解项目的实现细节、参与贡献代码或者基于源码进行二次开发等,都可以通过该地址进行查看和下载呢。欢迎各位开发者一同来探索和完善这个项目呀。
公众号
您可以通过扫描下方的公众号二维码进行关注呀,二维码清晰明了,方便快捷,只需打开手机上的扫一扫功能,对准二维码轻轻一扫,即可进入公众号页面,开启便捷的信息获取之旅,与我们在公众号平台进行互动交流哦。


浙公网安备 33010602011771号