
先问一个问题你在面试现场被要求写一个反转字符串的函数时第一反应是不是上来就写s.reverse()如果你点头了那这篇文章你值得花十分钟看完。LeetCode 344这道题我刷过不止一次也从面试官视角见过现场翻车的候选人老实说这道题的通过率虽然常年稳居高位但能把它讲明白、写干净、说清楚为什么用双指针的人并没有想象中那么多。这道题的全貌是这样的给你一个字符数组s要求原地反转不能申请额外的数组空间额外空间复杂度必须控制在 O(1)。一句话总结就是——用双指针从数组两端往中间走交换左右两个字符直到相遇。道理谁都能看懂但看懂和写对之间隔着不少细节这篇文章我把原理、实现、易错点、其他解法的对比以及这道题在整个反转类题目里的位置全部拆开讲一遍。1. 题目到底在考什么LeetCode 344的考点拆解1.1 题面里的三个隐藏要求先看这个题的原版描述编写一个函数其作用是将输入的字符串反转过来。输入字符串以字符数组s的形式给出。不要给另外的数组分配额外的空间你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。很多第一次做这道题的人会被输入字符串以字符数组s的形式给出这句话带偏以为这不就是转成数组嘛。实际上这句话是整个题目的核心约束之一——它意味着你拿到的就是一个可以原地修改的可变字符序列而不是 Java 里那种不可变的String对象。在 C 里对应的是vectorchar在 Python 里是List[str]本质都是同一个东西一块连续的、可以按下标访问和修改的内存。三个隐藏要求拆开看是这样的原地修改禁止新开一个等长数组然后把原数组倒着填进去。这是题目最基本的红线。O(1) 额外空间除了几个临时变量不允许使用随输入规模增长的额外存储。这句话直接毙掉了用栈、用递归、用新字符串拼接这一类方案。输入形式是字符数组这规定了你不能把题目转化成字符串 API 调用来解决。你想调用StringBuilder.reverse()可以但你得先把char[]转成String再转回来这个操作本身就因为两次转换产生了额外空间已经不满足题意了。1.2 为什么反转一个字符串还需要算法这是我觉得 344 最容易被低估的地方。反转字符串本身没有任何高深的数学原理它甚至不需要你懂什么数据结构但它恰好是双指针思想里最简洁、最适合作为入门模板的题目。双指针在算法题里是一个庞大的家族有快慢指针链表判环、有左右对撞指针有序数组的两数之和、有滑动窗口最长无重复子串。344 属于左右对撞指针这一类里最朴素的代表——一个指针从左往右一个指针从右往左两个指针向中间逼近直到相遇。你可以把双指针理解成两个人从一根绳子的两端同时往中间收绳子每收一步就把两端对应的字符交换一次最后整根绳子就被掉了个个儿。这个比喻虽然简单但它能帮助你记住一个关键信息两个指针是同时移动的而且它们移动的次数是 n/2 次不是 n 次。很多人在写循环的时候下意识写成遍历整个数组结果反转完之后又反转回去了这就是没有理解指针从两端对向移动这个本质。1.3 这道题在 LeetCode 上的定位344 在 LeetCode 上被标记为简单通过率常年维持在 70% 上下。但这个数字有迷惑性——因为提交的人里有大量第一次刷题的新手也有大量直接调用库函数通过的。真正到了面试场景面试官不会满足于你调 API 调对了他会追问你如果不允许用库函数呢如果输入特别长怎么办你能不能分析一下时间和空间复杂度所以我的建议是这道题一定要以能徒手写出双指针代码 能完整解释原理 能说出边界条件为标准去准备不要以提交通过为标准。LeetCode 的绿色勾是底线不是目标。2. 双指针法的核心思路从两端向中间逼近2.1 用一个具体例子走一遍全过程假设输入是[h, e, l, l, o]长度为 5。定义两个指针left指向下标 0right指向下标 4。第一步交换s[0]和s[4]数组变成[o, e, l, l, h]然后left加 1 变成 1right减 1 变成 3。第二步交换s[1]和s[3]数组变成[o, l, l, e, h]然后left变成 2right变成 2。第三步此时left和right指向同一个下标 2不需要再交换。循环结束反转完成。如果你仔细观察这个过程会发现交换的次数正好是n / 2向下取整。长度为 5 时交换了 2 次中间的l自己跟自己交换没有意义。长度为 4 时比如[a, b, c, d]left 和 right 会依次经过 (0,3) 和 (1,2)交换 2 次然后 left 变成 2、right 变成 1指针交错循环结束。这里有一个非常重要的细节循环的终止条件是left right不是left right。如果是奇数长度的数组会在最后多做一次自己跟自己交换虽然不影响结果但逻辑上多了一次无意义的操作面试如果问到这一步能说出等于的时候不需要交换会是一个加分项。2.2 双指针代码的标准写法Java 版本class Solution { public void reverseString(char[] s) { int left 0; int right s.length - 1; while (left right) { char temp s[left]; s[left] s[right]; s[right] temp; left; right--; } } }C 版本class Solution { public: void reverseString(vectorchar s) { int left 0; int right s.size() - 1; while (left right) { swap(s[left], s[right]); left; right--; } } };Python 版本class Solution: def reverseString(self, s: List[str]) - None: left, right 0, len(s) - 1 while left right: s[left], s[right] s[right], s[left] left 1 right - 1你可以看到三个主流语言的实现逻辑完全一致差别只在交换的语法糖上。C 里有 STL 的swapPython 里有元组解包Java 则需要自己写临时变量。这个区别本身也说明了为什么算法题推荐用自己最熟悉的语言去刷——写交换语句这种高频操作应该做到肌肉记忆级别不要在考场上还要想Python 该怎么交换两个变量。2.3 复杂度分析的两个层面时间复杂度和空间复杂度是这道题面试时几乎必被追问的两个问题。时间复杂度是 O(n)因为每个字符最多被访问一次。严格来说是 n/2 次交换每次交换涉及两次读取和两次写入但常系数在渐进分析里可以被忽略所以就是 O(n)。这里如果你能主动说一句虽然每个元素被移动了一次但实际上只有 n/2 次交换操作会显得你理解得比标准答案更深一层。空间复杂度是 O(1)。除了left、right和交换用的临时变量temp之外没有使用任何与输入规模相关的额外存储。三个变量是固定大小不随 n 变化所以是常数级空间。这里要特别注意的是如果你用递归来实现双指针空间复杂度就会变成 O(n)递归调用栈的深度这就违反了题目的约束。关于这一点后面第 4 节会详细展开。3. 那些我在刷题和面试里真实踩过的坑3.1 坑一把双指针写成了两个循环这个坑我在刚开始刷题的时候踩过也在帮别人 review 代码的时候见过很多次。核心问题是写成这样for (int i 0; i s.length; i) { char temp s[i]; s[i] s[s.length - 1 - i]; s[s.length - 1 - i] temp; }这段代码表面上也能得到正确的结果但有一个严重问题它遍历了整个数组也就是说对于长度为 n 的数组它做了 n 次交换。而事实上当你交换了s[0]和s[n-1]之后s[n-1]和s[0]就已经各归其位了等到循环走到i n-1的时候它会再把这两个位置交换一次结果等于什么都没做。我在本地跑过这个代码输入[a,b,c,d]输出还是[a,b,c,d]完全没变。用双指针的核心价值之一就是用 n/2 次操作完成原本需要 n 次操作的工作。当你发现自己的代码做了 n 次交换基本可以断定哪里写错了。一个简单的自查方法反转后的数组和原数组在对称位置上是互换关系如果你遍历全部下标每个位置会被交换两次等于没有交换。3.2 坑二用s.length当作循环终止条件导致的时间浪费这个坑比较隐蔽出现在一种看起来很像双指针的写法里int left 0; int right s.length - 1; while (left s.length) { // 交换逻辑 left; right--; }这种写法的问题在于left会一直增加到s.length而right会一直减少到-1循环结束后两个指针都已经越界了。虽然交换逻辑本身在 left right 时是正确的但循环条件写错了会让代码多做一半的无用功而且越界访问在 C 里是未定义行为在 Java 里会直接抛ArrayIndexOutOfBoundsException。正确的循环条件只有一个标准while (left right)。原因前文已经提过——当 left 等于 right 时奇数长度或 left 超过 right 时偶数长度所有需要交换的位置都已经处理完了。这个条件不仅正确而且从数学上严格对应了最多只需要处理 n/2 次这个事实。3.3 坑三调用库函数导致的空间违规在 LeetCode 的讨论区里你会看到不少一行代码解决的提交比如调用Collections.reverse(Arrays.asList(s))或者直接用StringBuilder的 reverse。这些提交能通过多半是因为测试用例没有严格审查空间使用而且 Java 的Collections.reverse内部其实也是双指针实现的。但如果你在面试中写这种东西我建议你做好被追问的准备。追问通常是这个套路面试官先问这个 API 内部是怎么实现的如果你答不上来他会接着问你觉得这个 API 的空间复杂度是多少然后问如果题目要求你不能用这个 API 呢。这一串问题下来你基本上是招架不住的。所以我的建议是刷题阶段可以看一眼别人的一行代码解法开拓思路但提交之后一定要自己手写一遍双指针版本。库函数能帮你通过用例但帮不了你通过面试。3.4 坑四把 String 和 char[] 混为一谈最后这个坑更多是概念层面的。因为题目描述里说的是反转字符串有些第一次刷题的人就直接写public String reverseString(String s) { return new StringBuilder(s).reverse().toString(); }这在 LeetCode 344 里是通不过的因为函数的签名要求的是void reverseString(char[] s)。也就是说这道题考察的不是你会不会封装一个字符串反转工具而是你能不能在一个已经给你可变内存空间的前提下完成原地修改。Java 的 String 是不可变对象每次修改都会生成新对象所以在 Java 里凡是涉及大量字符串修改的场景都应该优先考虑 StringBuilder 或 char[]。原型设计模式的思想在这里也有点影子如果你要在一个固定对象上反复修改就不要每次创建新对象而是操作对象内部的状态。面试中如果你能把为什么题目选择 char[] 而不是 String这个问题答清楚面试官对你好感度会明显提升。答案是如果输入是 String那这道题在 Java 里根本无法做到 O(1) 空间反转因为 String 不可变任何反转操作都必须创建新字符串。所以题目才用 char[] 作为输入形式让原地修改成为可能。4. 双指针不是唯一解但它是这道题的最优解4.1 库函数解法能用但等于没做以 Java 为例你可以通过Collections.reverse配合Arrays.asList来完成反转public void reverseString(char[] s) { ListCharacter list new ArrayList(); for (char c : s) list.add(c); Collections.reverse(list); for (int i 0; i s.length; i) s[i] list.get(i); }这种写法最后确实反转了数组但它至少犯了两个错误第一new ArrayList()本身就是 O(n) 的额外空间第二这种写法完全没有体现解决这个问题的思路面试官问一句讲讲原理你就卡壳了。所以我通常会把这个方案定性为它不是解法它是对 API 的背诵。如果你在刷题阶段是为了锻炼算法能力请跳过这种写法。4.2 栈解法思路直观但空间不达标栈是一个天然适合反转的数据结构——你把字符依次压入栈中再依次弹出弹出的顺序就是原来的逆序。public void reverseString(char[] s) { StackCharacter stack new Stack(); for (char c : s) stack.push(c); for (int i 0; i s.length; i) s[i] stack.pop(); }这个方案思路很简单代码也很短但它使用了 O(n) 的额外空间栈里存了所有字符完全违反了题目对 O(1) 空间的要求。如果你去面试面试官让你反转字符串你写了个栈他大概率会追问一句你能不能不用额外空间实现这时候你再切到双指针反而会让整个对话显得是被推着走的。我的建议是栈解法作为思维方式了解一下就行比如你完全可以用它来理解后进先出的概念但不要把这种解法当作 344 的正式答案。在 344 这道题里O(1) 空间是硬性约束所有不满足这个约束的解法都应该直接排除。4.3 递归解法代码优雅但空间爆炸递归写法是一个非常经典的错误示范public void reverseString(char[] s) { reverseHelper(s, 0, s.length - 1); } private void reverseHelper(char[] s, int left, int right) { if (left right) return; char temp s[left]; s[left] s[right]; s[right] temp; reverseHelper(s, left 1, right - 1); }这段代码逻辑上完全正确而且写法很简洁——每递归一层就交换一对字符然后缩小指针范围。但它有一个致命问题递归的深度是 n/2而每次递归都会占用一层调用栈所以空间复杂度是 O(n)。如果输入是一个 10 万字符的数组这段代码会直接栈溢出。我能理解有些人喜欢用递归解法是因为它看起来更像是在用双指针但算法题不是表演不是代码越短越好也不是递归越优雅越好。面试官希望你最优地解决问题而不是炫技。双指针的迭代写法在所有方面都优于递归写法所以这一节没有争议。4.4 解法横向对比为了让你更直观地理解为什么双指针是 344 的最优解我把几种常见解法放在一起对比一下解法时间复杂度空间复杂度是否满足题意面试推荐指数双指针迭代O(n)O(1)满足强烈推荐库函数O(n)O(n)不满足不推荐栈O(n)O(n)不满足了解即可递归O(n)O(n)不满足了解即可从这里可以清晰地看出唯一同时满足O(n) 时间和O(1) 空间两个条件的就是双指针迭代解法。它也是这道题真正的标准答案。5. 从 344 延伸出去反转类题目和字符串操作的进阶5.1 字符串逆序在不同语言里的输出姿势344 这道题是一个起点但从它延伸出去的字符串逆序问题比想象中更常见。比如很多初学者想问字符串逆序输出 c 语言怎么做如果你已经理解了双指针你会发现这个问题的本质不是怎么输出而是怎么在字符串内部交换字符。C 语言里没有现成的字符串类操作的是char[]或者char*所以双指针的思想非常直接地适用。下面是一个 C 语言的字符串逆序实现void reverseString(char* s, int sSize) { int left 0; int right sSize - 1; while (left right) { char temp s[left]; s[left] s[right]; s[right] temp; left; right--; } }再比如 C 的std::reverse函数它内部就是双指针实现的。这些语言层面的函数本质上都在做同一件事对向逼近交换字符。理解了 344你对它们的理解会从会用函数变成知道它为什么这么实现。5.2 LeetCode 541反转字符串 II——分段反转的思维升级344 的进阶版本是 LeetCode 541给定一个字符串 s 和一个整数 k从字符串开头算起每计数至 2k 个字符就反转这 2k 字符中的前 k 个字符。如果剩余字符少于 k 个则将剩余字符全部反转如果剩余字符小于 2k 但大于或等于 k 个则反转前 k 个字符其余字符保持原样。这个题的解法思路建立在 344 的基础之上把字符串按 2k 一段切分在每一段里用双指针反转前 k 个字符。public String reverseStr(String s, int k) { char[] arr s.toCharArray(); for (int start 0; start arr.length; start 2 * k) { int left start; int right Math.min(start k - 1, arr.length - 1); while (left right) { char temp arr[left]; arr[left] arr[right]; arr[right] temp; left; right--; } } return new String(arr); }你可以看到这里依然用到了双指针交换的核心逻辑只是交换的范围从整个数组变成了分段范围。如果你把 344 彻底搞懂了541 对你来说就只是在外面套一层循环的问题。5.3 LeetCode 151翻转字符串里的单词——双指针的另一个维度另一道经典题是 LeetCode 151给定一个字符串逐个翻转字符串中的每个单词。这道题有一个著名的解法思路先反转整个字符串再反转每个单词。举个例子输入the sky is blue先整体反转变成eulb si yks eht再逐个单词反转变成blue is sky the。这个过程里反转部分的代码核心还是双指针——不管是反转整个字符串还是反转每个单词区间都是在某个 [left, right] 范围内做对称交换。这道题还涉及字符串处理和去空格的细节但如果你能意识到它其实是在多次调用344 的核心逻辑你的算法思维就提升了一个层次。刷题最忌讳的是刷一个记一个没有把知识点串起来而双指针恰好是串联这些题目的一条主线路。5.4 字符串类型问题的通用思维框架以 344 为例如果你把 344 扩展到一个更大的视角可以总结出一个处理字符串数组类题目的通用思考框架第一步确认输入输出形式是可变数组还是不可变字符串这决定了你能不能原地修改。第二步确认空间约束O(1) 空间意味着你只能使用有限几个变量基本可以排除栈、哈希表、递归。第三步寻找对称性或单调性反转问题找对称位置查找问题找单调关系子串问题找窗口特性。第四步用双指针或滑动窗口实现核心逻辑并在循环条件里处理好边界。这个框架不是 344 独有的它适用于大部分字符串和数组问题。每次拿到新题先走一遍这个思考流程解题方向会清晰很多。6. 面试实战当 344 出现在你面前6.1 面试官真正想考察的东西从面试官视角看344 是一道绝佳的开局题。它足够简单可以让候选人进入状态但它又足够多细节能区分背过答案和真正理解的候选人。我会观察以下几点沟通确认候选人是否在写代码前确认了输入输出形式、是否问清楚能否使用额外空间。344 几乎把所有约束都写在题目里了但如果候选人上来就写说明他可能没有读题的习惯。边界处理空数组、单元素数组、奇偶长度是否都能正确覆盖。检查循环条件是left right还是left right能看出候选人有没有真的推演过边界。复杂度分析能否清晰说出时间和空间复杂度并解释为什么。语言基本功交换语句是否熟练能否在白板上写对语法。如果你能做到以上四点这道题基本上就稳了。6.2 一个完整的现场回答示范我给你演示一段面试时可以采用的回答思路供参考首先题目明确要求原地修改和使用 O(1) 额外空间所以我会排除所有需要新建数组或字符串的方案。输入是 char[] 而不是 String这让我可以直接修改数组元素。我会使用双指针法定义 left 指向数组开头right 指向数组末尾在 left 小于 right 的条件下循环每次把 left 和 right 指向的字符交换然后 left 右移、right 左移。当 left 大于等于 right 时说明所有需要交换的位置都已经处理完毕。时间复杂度是 O(n)因为每个字符最多被访问一次空间复杂度是 O(1)因为我只使用了三个额外变量。这段话看起来简单但它包含了确认约束 → 排除错误方案 → 解释算法流程 → 分析复杂度。这就是面试官想听到的完整逻辑链。6.3 关于刷题顺序和复习节奏的建议最后分享一点个人经验。我在刷 LeetCode 的时候并不推荐一上来就按照题号从 1 开始刷到底而是建议按专题刷——数组、链表、哈希表、双指针、滑动窗口、动态规划一个专题一个专题地过。344 就是双指针专题里非常好的入门题刷完它可以接着刷 27移除元素、283移动零、125验证回文串这一系列同类型题形成一个小的知识闭环。在这个专题中你还会遇到字面意义上的变种比如字符串比较是否相等这个看似基础的问题在 Java 里就分成equals比较内容和比较引用两个维度又比如字符串分割在很多语言里会生成一个新数组这同样涉及空间开销的考量。这些内容看着和 344 无关但它们共同组成了一个程序员处理字符串问题的基本素养——知道什么时候能原地操作知道什么时候必须新建对象。回到 344 这道题本身我的最终建议只有一句话不要因为它是简单题就跳过用手写板把它写到滚瓜烂熟并且能讲出每一种替换方案的失败原因。很多看似基础的能力恰恰是面试中最能反映功底的部分。等你把双指针练成了肌肉记忆再遇到任何反转类、对撞类、区间类的问题都会有一种这题我见过的从容感。