
1. 从 Day 8 开始字符串题到底在考什么进入代码随想录算法训练营第 8 天主题切换到字符串 part 01。说实话很多人在数组、链表部分感觉自己悟了一到字符串就开始发虚——明明每个字符都认识组合在一起就不会做了。这背后的原因其实很简单字符串题本质上还是数组题只是把 int 换成了 char但多了很多字符串专属的坑比如结束符、长度语义、原地修改的边界处理。Day 8 这批题目就是帮你把这些坑一个一个填平的。字符串 part 01 覆盖的核心内容基本就是反转字符串、反转字符串 II、替换空格、翻转字符串里的单词、左旋转字符串这几道经典题。乍一看都是简单题但把它们连起来做一遍你会发现一个清晰的递进从最基础的双指针原地操作到处理循环边界再到整体反转 局部反转这种组合思路最后过度到字符数组扩容和从后向前填充。这套组合拳打下来字符串题的基本盘就算立住了。这篇文章我会按训练营 Day 8 的实际节奏把每道题的关键思路、代码写法、常见误区和扩展思考都过一遍。不管你是正在跟训练营的学员还是自己刷 LeetCode 的读者只要把这几道题吃透后续遇到更复杂的字符串题比如 KMP、滑动窗口至少不会在基础操作上卡壳。2. 反转字符串双指针思想在字符串上的第一次落地2.1 题目要求与做题前的认知准备原题是 LeetCode 344编写一个函数其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出要求原地修改输入数组使用 O(1) 的额外空间。这题拿到手第一步要明确的不是怎么反转而是搞清楚一个前提题目给的是字符数组不是 C 的 string。为什么强调这一点因为很多新手在本地练习时用 string 写惯了一到面试手写环节面对 char[] 就懵了。char[] 和 string 最大的区别在于char[] 没有 length() 或 size() 方法你得自己算长度如果是 C 风格字符串可以用 strlen但要注意它依赖结尾的 \0。LeetCode 上给的是 char[] 形式的参数同时传入数组长度这就避开了 strlen 的问题。但如果你在本地实现写一个反转 char[] 的函数最稳妥的写法是同时传入长度参数。2.2 双指针写法的完整推导反转数组这件事最朴素的思路是新建一个等长数组从原数组末尾开始遍历填入。但题目限制了 O(1) 额外空间所以必须原地操作。原地反转的标准解法就是双指针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--; } } };这个写法本身很简单但我想拆开讲三层因为这决定了你能不能写出变体题第一层循环条件是 left right 而不是 left right。中间那个元素奇数长度时不需要动所以严格小于就够了。很多人写 也能跑通但属于多做一步无意义操作在追求极致的面试场合显得不够干净。第二层swap 的实现。C 的 STL 里有现成的 swap直接用没问题。但面试官偶尔会追问不借助库函数怎么交换两个字符至少要知道两种替代方案临时变量法和异或法。临时变量法最直观char temp s[left]; s[left] s[right]; s[right] temp;。异或法则需要理解 a ^ b; b ^ a; a ^ b; 的原理这个技巧虽然在实际项目中不常用可读性差但在算法面试里算是个加分项。第三层时间复杂度 O(n)空间复杂度 O(1)。这两个结论要脱口而出。2.3 为什么说这题是模板题做完 344 别急着走它最大的价值是给你一个反转区间的思维模板。所谓反转区间是指给定一个数组和一个区间 [l, r]把这个区间内的元素倒过来。344 只是区间恰好是整个数组的特殊情况。掌握了这个模板后面 541 反转字符串 II、151 翻转字符串里的单词都会用到它。我自己写题时的习惯是先把区间反转封装成一个独立函数方便复用。在面试中手写的话可以直接在循环里写但如果你发现一道题需要多次反转不同区间封装成函数一定更清晰void reverseRange(vectorchar s, int l, int r) { while (l r) { swap(s[l], s[r]); l; r--; } }这个函数我在 Day 8 里反复用后面几道题全都能套。所以我的建议是344 这道题别看它简单务必把区间反转这个版本也写一遍后面你会感谢自己。3. 反转字符串 II循环步长和边界判断才是真正的考点3.1 题目解读和最容易踩的坑LeetCode 541 反转字符串 II给定一个字符串 s 和一个整数 k从字符串开头算起每计数至 2k 个字符就反转这 2k 个字符中的前 k 个字符。如果剩余字符少于 k 个则将剩余字符全部反转。如果剩余字符小于 2k 但大于或等于 k 个则反转前 k 个字符其余字符保持原样。这题看起来是 344 的简单升级实际上一堆人栽在循环步长上。最常见的错误写法是for (int i 0; i s.size(); i)然后在循环体里判断是否到了需要反转的位置。这种写法不是不能做但会导致逻辑混乱而且你不得不用一个额外的计数器去追踪当前是第几个字符。正确的思路是让循环步长直接等于 2k。因为题目规定的行为模式是每 2k 个字符为一组每组只处理前 k 个所以 i 每次跳过 2k 个字符循环体内只负责判断这一组需要反转多长这是最贴合题目语义的写法class Solution { public: string reverseStr(string s, int k) { for (int i 0; i s.size(); i 2 * k) { // 剩余字符少于 k 个反转剩余全部 if (i k s.size()) { reverseRange(s, i, s.size() - 1); } else { // 剩余字符大于等于 k 个反转前 k 个 reverseRange(s, i, i k - 1); } } return s; } };3.2 边界条件的两种判断方式代码里的判断条件 i k s.size() 是核心。你可以这样理解如果 i k 已经越过了字符串末尾说明从 i 开始到末尾不足 k 个字符按题目要求全部反转否则说明至少有 k 个字符就反转 k 个。这里有个细节值得单独说为什么不是等于而是大于。当 i k s.size() 时说明从 i 开始到末尾恰好 k 个字符此时应该反转 k 个也就是进入 else 分支。所以判断不足 k 个要用 而不是 。边界条件差一个等号结果就完全不对了这种差之毫厘的题在面试中特别能看出基础是否扎实。3.3 库函数 reverse 到底能不能用很多题解里直接用了 STL 的 reverse(s.begin() i, s.begin() i k)因为 C 的 reverse 接受迭代器区间天然支持反转前 k 个。但训练营里 Carl 的建议是练习阶段尽量手写反转因为面试中实现一个反转函数是高频考点而且手写能帮你加深对区间边界的理解。我自己在训练营期间的做题原则是核心方法论相关的比如双指针、KMP必须手写纯工程性的辅助函数比如排序的底层实现可以用库函数。展开说一句这不是为了手写而手写。反转字符串 II 这道题真正想考察的是你会不会根据规则确定反转区间。如果你只会调用库函数自己没有实现过区间反转的细节一旦题目变成反转每个单词中的元音字母这类变体你可能连区间边界都算不清楚。3.4 这道题的实际应用场景刷题的人常常会问反转字符串 II 这种题除了面试还有什么用其实字符串的批量区间处理在工程中很常见。举个例子做日志脱敏时要对每 16 位手机号片段做掩码处理或者对文本中每 2k 字节的块做校验和计算这类按固定步长遍历 按条件处理子区间的思路和 541 是完全一样的。所以别觉得这种题是面试专用它训练的是处理区间数据的敏感度。4. 替换空格从后向前填充的经典套路4.1 题目描述和朴素解法的性能问题剑指 Offer 05 替换空格请实现一个函数把字符串 s 中的每个空格替换成%20。这题在很多地方被归为简单题但它背后的数组扩容 从后向前填充思想实际上是一个高频考点只是换了个马甲出现在各种题目里。先来看最容易想到的解法新建一个字符串遍历原字符串遇到空格就追加 %20否则追加原字符。这个解法完全正确时间和空间复杂度都是 O(n)。如果你在笔试中遇到这题直接这么写没有任何问题。但在面试场景中面试官很可能会加一个限制假设在原有字符串上进行修改保证输入的字符串后面有足够多的空余空间。这就是在考察你能否实现原地替换。原地替换的朴素思路是从前向后遍历遇到空格就将后续字符统一后移两个位置再把 %20 填进去。这个做法的问题在于每遇到一个空格后面所有字符都要移动一次最坏情况下时间复杂度是 O(n^2)。4.2 为什么从后向前填充是更优解正确的思路分三步第一步统计原字符串中空格的数量。每个空格会替换成 3 个字符%20相比原来的 1 个字符多出 2 个所以扩充后的字符串长度 原长度 2 * 空格数。第二步将字符串扩充到新长度。在 C 中可以用 resize 或 reserve 索引赋值在 Java 中用 StringBuilder在 Python 中 string 不可变所以只能用列表模拟。第三步从后向前遍历并填充。具体操作是设置两个指针oldIndex 指向原字符串末尾newIndex 指向扩容后的末尾。从后向前遍历如果 oldIndex 指向的不是空格就直接复制到 newIndex 的位置如果是空格就依次填入 0、2、%注意顺序从后向前填所以要倒着写。class Solution { public: string replaceSpace(string s) { int count 0; int oldLen s.size(); for (char c : s) { if (c ) count; } s.resize(oldLen 2 * count); int newLen s.size(); for (int i oldLen - 1, j newLen - 1; i 0; i--, j--) { if (s[i] ! ) { s[j] s[i]; } else { s[j] 0; s[j - 1] 2; s[j - 2] %; j - 2; } } return s; } };为什么从后向前就能避免 O(n^2)因为从后向前填充时每个字符最多被移动一次。从前向后移动时后面的字符每遇到一个空格就要被挪一次挪的次数和它前面的空格数量成正比大量字符被反复移动。从后向前则不存在这个问题整体时间复杂度直接降到 O(n)。这个思想非常重要类似数组扩容 从后向前的套路在后续做数组合并、数组合并去重等题目时还会遇到。4.3 面试追问的常见变体面试官在问完这道题后特别喜欢追问几个变体如果替换的字符串更长比如把空格替换成%20abc原方法还适用吗答案是适用核心依然是先扩容再从后向前填充只是每个空格需要多填几个字符而已。如果源字符串和替换后的字符串内存重叠会不会覆盖未处理的数据从后向前填充天然规避了这个问题这也是它比从前向后安全的原因之一。另外要注意语言差异。在 Python 中字符串是不可变对象无法原地修改所以标准做法是用列表收集字符再 join。这不代表Python 做不到原地而是语言特性决定的面试时主动说明这一点会加分。5. 翻转字符串里的单词整体反转加局部反转的组合拳5.1 解题思路的第一次跃迁LeetCode 151 翻转字符串里的单词原题是翻转字符串里的单词顺序给定一个字符串逐个翻转字符串中的每个单词同时要处理多余空格——开头、结尾的空格都要去掉单词间如果有多个空格只保留一个。这题如果按照字面意思去从后向前找单词也能做但实现起来非常繁琐要处理各种空格边界。代码随想录里给出的思路很有意思先把整个字符串反转再逐个反转每个单词。以 the sky is blue 为例整体反转得到 eulb si yks eht再把每个单词内部反转 back 成 blue is sky the顺序问题就解决了。为什么这个方法成立核心在于单词的顺序反向和单词内部的字符顺序是两个可以分离的操作。整体反转解决了单词顺序的反转但同时也把每个单词内部的字母顺序弄反了第二次对每个单词做局部反转恰好抵消了这个副作用。这种先整体后局部的思路在字符串题里是一个极高频的套路。5.2 实现过程中最关键的难点去除多余空格很多人卡在这道题上不是因为不理解两次反转的思路而是不知道怎么写去除多余空格。这里的实现有很多种我推荐训练营里用的双指针写法因为它和数组章节的移除元素高度呼应。写一个 removeExtraSpaces 函数思路和数组移除指定元素几乎一样用 slow 指针指向新字符串的当前写入位置fast 指针遍历原字符串。遍历过程中找到每个单词的起始位置先手动加一个空格除了第一个单词前不加然后把整个单词逐个字符搬过来void removeExtraSpaces(string s) { int slow 0; for (int fast 0; fast s.size(); fast) { if (s[fast] ! ) { if (slow ! 0) s[slow] ; while (fast s.size() s[fast] ! ) { s[slow] s[fast]; } } } s.resize(slow); }代码里 if (slow ! 0) 表示当前不是第一个单词需要在单词前补一个空格。while 循环里 fast s.size() 防止越界s[fast] ! 保证只搬运单词字符。最后 s.resize(slow) 截断后面残余的字符。这个过程建议动手画一下指针移动的示意图一次就能搞清楚。画的时候注意slow 指针在新字符串中的位置永远小于或等于 fast 指针的位置所以覆盖是安全的。5.3 三次反转的具体执行顺序完整代码可以分成三步第一步去除多余空格。 第二步反转整个字符串。 第三步遍历字符串对每个单词区间做局部反转。第三步的写法也很有意思用 start 记录当前单词起点用 end 扫描到空格或字符串末尾。扫描到空格时说明 [start, end - 1] 是一个单词反转这个区间然后跳过空格更新 start。这里有个细节如果字符串末尾没有空格要在循环结束后单独处理最后一个单词的反转否则会漏掉它。class Solution { public: string reverseWords(string s) { removeExtraSpaces(s); reverseRange(s, 0, s.size() - 1); int start 0; for (int end 0; end s.size(); end) { if (end s.size() || s[end] ) { reverseRange(s, start, end - 1); start end 1; } } return s; } };注意 for 循环里 end s.size() 而不是 end s.size()这是为了能在 end s.size() 时触发处理最后一个单词的逻辑。这个边界条件也是高频坑点我在评论区见过很多次为什么最后一个单词没反转的提问基本都是因为循环条件写成了 。5.4 为什么这个思路具有通用性整体反转 局部反转不仅能解决翻转单词顺序还能解决很多需要调整顺序但不能用额外空间的问题。比如后面紧接着要做的左旋转字符串本质上是同一个思路再比如旋转数组把数组后 k 个元素移到前面解法也是先整体反转再分别反转两个部分。一旦你把这个模式内化遇到类似题目会自然想到这个方向而不是去纠结如何逐个搬运元素。6. 左旋转字符串三次反转的数学解释6.1 题目描述和常规解法的局限剑指 Offer 58-II 左旋转字符串字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。比如输入 abcdefg 和数字 2函数返回 cdefgab。最直观的解法是用 substr 做拼接return s.substr(n) s.substr(0, n)。这个解法一行搞定而且在工程中完全够用。但如果我们追求的是一次原地实现就需要引入反转的思想。实际上很多面试官会直接问能不能不用额外空间完成这时 substr 方案就不合规了。6.2 三次反转的推导过程解法分三步反转前 n 个字符。反转 n 到末尾的字符。反转整个字符串。以 abcdefg, n 2 为例反转前 2 个字符 ab - ba字符串变为 bacdefg反转剩余部分 cdefg - gfedc字符串变为 bagfedc整体反转 bagfedc - cdefgab结果正是 cdefgab。这个结果不是巧合它的数学原理是把字符串分成 A 和 B 两部分原始字符串是 AB左旋 n 个字符得到 BA。三次反转等价于(反 A)(反 B) 反转后 反(A反) 反(B反) 反转(反(A反) 反(B反)) BA写成公式就是reverse(reverse(A) reverse(B)) BA。这个推导可以类比到右旋如果是右旋 k 个字符先把整个字符串反转再分别反转前 k 个和剩余部分得到的就是右旋结果。6.3 左旋和前面题目的内在联系到这里你会发现左旋转字符串和翻转语句中的单词本质上都在用同一个思想通过多次反转来调整字符串的局部顺序。区别只在于反转的边界怎么划分。单词题里边界是空格左旋题里边界是 n。这个边界划分本身就是这类题的核心考点。这道题可以用前面封装的 reverseRange 函数直接写class Solution { public: string reverseLeftWords(string s, int n) { reverseRange(s, 0, n - 1); reverseRange(s, n, s.size() - 1); reverseRange(s, 0, s.size() - 1); return s; } };注意这里的类型是 stringreverseRange 的参数写作 int 类型即可因为 string 的索引可以用 size_t 或 int在函数内部需要配合 s.size() 的使用保持一致。6.4 实际工程里的对应场景左旋的工程化应用比你想的多。比如给用户展示长文本时某些场景下需要把一段文本从中间某个位置滚动展示或者在实现循环队列的字符缓冲时需要把前缀字符移到尾部。虽然是抽象问题但分段反转的思维可以迁移到任何线性序列上。这也解释了为什么这类题在面试中出现频率那么高——它考察的不是你会不会调 reverse而是你能不能把一个具体的工程问题抽象成序列操作。7. Day 8 练习题之外的补充与反思7.1 字符串底层的几个冷知识点做 Day 8 的题目时有几个关于 C 字符串的底层细节很有趣值得单独记录string 的底层是动态分配的字符数组和 vector 在存储模型上几乎一致所以很多数组的操作可以平移到 string。size() 和 length() 在 string 里等价都返回元素个数。区别于 C 风格字符串的 strlen后者依赖于 \0 结束符时间复杂度 O(n)。string 支持通过下标 s[i] 修改字符这一点和 Java 的 String 不可变特性不同。如果你用 Java 刷题想做原地修改就要先转成 char[]。在 C 中 substr 的第二个参数是长度而不是结束位置很多从 Python 转过来的选手容易在这里踩坑。这些细节在做题时不一定都用到但面试中遇到为什么 C string 能 O(1) 访问这类问题时会很有帮助。7.2 字符串题目的三种核心模式总结Day 8 的题目做下来字符串操作的基本模式可以归纳为三种双指针原地操作对应 344 反转字符串。定步长区间处理对应 541 反转字符串 II。整体反转 局部反转对应 151 翻转单词、58-II 左旋字符串。数组扩容 从后向前填充对应 05 替换空格。这四种模式在后续的字符串题目里会反复出现。比如实现 strStr() 时用到的 KMP 是全新的算法但它的前缀表求解过程中用到的依然是双指针思想再比如压缩字符串的题目会用双指针做原地读写。7.3 刷题节奏上的建议代码随想录的训练营节奏本身已经帮大家把题目按难度排好序了所以没有必要跳着刷或追求数量。Day 8 这几道题我建议按 344 - 541 - 剑指05 - 151 - 剑指58-II 的顺序做因为它们的难度和思路是逐步递进的。每道题做完可以试着把题目的变体写一遍——比如把 541 的 k 改成 3 会怎样把 151 的单词分隔符从空格改成逗号会怎样这些变体练习能帮你真正吃透题目的核心逻辑而不是背下代码。7.4 关于库函数的边界认知在训练营和实际面试中总是有人纠结能不能用库函数。我的观点是这样的如果库函数是这道题要考察的核心那当然不能直接用如果库函数只是辅助工具用了反而更高效。比如 344 的核心是双指针思想你直接用 STL 的 reverse 就失去了练习的意义。但 541 里用 reverse 做区间反转只要你能清晰说出为什么反转这个区间而非那个区间用库函数并无不可。关键不是能不能用而是你知不知道不用库函数时自己在干什么。这两者的区别在面试官深挖时立刻见分晓。我个人在训练营期间踩过一次比较深的坑是 541 的循环步长。一开始我写的是 for (int i 0; i s.size(); i)然后在循环体里维护一个 count 变量最后发现代码越写越复杂边界越调越乱。后来改成 i 2 * k 后整个题目瞬间清爽。这个经历让我意识到循环变量的设计直接影响问题的复杂程度——循环步长如果能贴合问题的天然语义边界判断就会简单很多。同样地151 的 removeExtraSpaces 函数我第一次写的时候用了 erase 逐个删除空格结果又慢又容易错换成双指针覆盖后代码效率和正确率都上了一个台阶。最后分享一个小技巧也是我自己做字符串题习惯性的收尾动作每道题写完后把正常输入、全空格输入如 、 、单个字符输入、刚好是 k 或 2k 倍数长度的输入都跑一遍。字符串题的边界问题大多藏在这些特殊输入里花两分钟测一遍往往能避免在评论区被读者抓 Bug。这个习惯坚持下来你在字符串题上的稳定性会明显提升。