资讯动态

C++刷题指南:双指针反转、区间反转与原地扩容的字符串三题精讲

发布时间:2026/10/13 16:17:20 来源:尧图企业网站定制
最近又刷了一轮力扣字符串题把 344、541 和一道很经典的“替换数字”放到一起过了三遍。这三题单独看都不难C 解法也都不超过 30 行但把它们串在一起刷你会发现它们正好覆盖了字符串操作中最容易被忽略的三类基本功原地反转的双指针模型、带规则反转的区间边界判断、以及扩容后从后往前的填充策略。如果你也在用 C 刷力扣尤其是刚刷完数组双指针、正准备进入字符串专项这篇日记应该能帮你少踩几个坑。我之前刷字符串题习惯拿到题就for遍历再用一个辅助数组存结果基本也能 AC。但后来在面试里被问到空间复杂度和边界条件的细节才发现很多题并没有真正吃透。所以这次我给自己定了个标准每道题先想清楚能否原地完成再想清楚能否把复杂度压到 O(n)最后补充测试边界。下面按这个标准逐题复盘。1. 三题连刷的整体思路1.1 为什么是这三题从无规则到有规则的递进有意思的是如果只看解法“替换数字”和 344 完全不沾边541 又是一道需要看半天的规则题。但我的感受是这三题其实是一个递进关系。344 教会你“双指针怎么走”541 教会你“双指针在什么条件下走、走到哪”替换数字教你“走的时候如何不覆盖还没处理的位置”。这三层正好对应我在实际开发里处理字符串时经常纠结的三件事怎么改、改哪里、怎么改不坏。从题量上看这三题的代码量都不大。344 算是最标准的双指针入门541 在热题 100 里没有但在各类字符串专项里属于经典题替换数字则是很多题单里“字符串与数组填充”的典型代表。把三道题放在一个晚上连刷负担也不重而且每一步都能在前一步的基础上加一点点复杂度比单独刷一道难题更容易形成体系感。1.2 共同的底层模型下标、区间、步进我在复盘时发现字符串操作题无论包装得多花哨核心都是下标和区间。344 的下标是两个指针在收拢541 的下标是每次固定往前跳 2k替换数字的下标则变成了一对“旧末尾”和“新末尾”。用 C 刷题时最常见的越界、覆盖、少翻转一个字符基本都是这三类下标问题引起的。这里可以打个比方字符串本质上是一个字符数组在 C 里就是std::string或vectorchar。std::string比普通数组多了一些方法但底层依然是通过连续内存存储字符。你操作下标本质上就是在操作连续内存里的偏移量。所以这三道题虽然表面上是“反转”“间隔反转”“替换数字”实际都在练习同一件事精确控制下标的移动节奏并且保证每一步都不会访问到不属于自己的位置。2. 344题 反转字符串用双指针把地基打牢2.1 题意与最小实现344 这道题的要求很直白给一个字符数组s要求原地反转字符串也就是把s[0]和s[n-1]交换然后向内推进并且额外空间只能是 O(1)。最简单的 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; } } };这里while (left right)是一个很重要的边界条件当字符数量为奇数时正中间的那个字符不需要交换偶数时left 和 right 会在越过中点前的最后一次交换后错开。比如长度为 4过程是left0right3交换后 left1right2left1right2交换后 left2right1left2right1不满足 left right退出。这样一定不会出现重复交换或者越界。很多初学者会写while (left right)这会导致中间位置被交换两次虽然结果碰巧一样但代码语义是错误的。我在刷这题时特地验证过包括奇数长度和偶数长度两种情况left right的写法在中间位置确实会多做一次无意义的 swap。2.2 先手写再学会调库函数我见过不少直接把reverse(s.begin(), s.end())放在Solution里就交上去的做法。说实话刷题时用库函数没问题std::reverse效率高、不用自己处理边界。但前提是你得能手写出来否则面试时被追问一句“底层怎么实现”就容易卡壳。std::reverse的实现思路就是双指针向中间靠拢不过它操作的是迭代器区间区间是左闭右开[first, last)。这个“右开”的细节非常关键因为后面的 541 题直接依赖它。如果忘了这个特性手写一个区间反转时把right写成last就会去操作一个不存在的元素。基于这个原因我通常建议先手写一遍 344再用std::reverse和自定义反转各自提交一次观察结果一致。这比单纯背代码有用得多。std::reverse在algorithm头文件里C 的std::string和std::vectorchar都支持随机访问迭代器所以可以直接传begin()和end()。2.3 复杂度与一个容易忽略的细节双指针反转的时间复杂度是 O(n)n 是字符串长度空间复杂度 O(1)。这个结论很容易记但有一个容易被忽略的点在 C 中如果传入的是std::string而不是vectorchar同样是原地反转用s.begin()和s.end()即可如果传入的是 C 风格字符串char*则需要自己计算长度且strlen本身就是一次 O(n) 遍历所以总复杂度仍然是 O(n)但多了一次扫描。力扣这题用的是vectorchar不需要考虑这种情况但我自己在本地练习时会把几种容器都写一遍加深印象。还有一个细节是s.size()返回的是size_t无符号类型。在 344 里直接取s.size() - 1时如果字符串为空就会得到一个大正数而不是 -1导致right越界。力扣的测试用例可能不会给空字符串但你自己写边界测试时一定要留意。3. 541题 反转字符串 II步进与区间判断是关键3.1 先把题目翻译成人话541 是我觉得“看题两分钟理解规则五分钟”的题。原题描述很长但翻译成人话就是三句话从字符串开头数每 2k 个字符分成一组反转每组的前 k 个字符。如果最后一组不足 k 个字符那就把这一组全部反转。如果最后一组在 k 到 2k-1 个字符之间就只反转前 k 个后面的不动。为什么会有这么奇怪的规则其实就是题目想考察你对“分段步进 边界判断”的掌控力。很多解法会把规则写得很复杂但代码只要三行核心逻辑。我第一次做这题时习惯性地用计数器一格一格走结果代码写了一堆 if还容易在边界上犯错后来换成固定步进i 2*k整个思路立刻清晰了。3.2 三种剩余情况对照为了把规则落到代码上我把边界情况整理成了一张表。假设当前步进到位置i字符串长度为n每次步进2k剩余情况条件反转区间区间长度剩余字符少于 k 个i k n[i, n)剩余全部剩余字符在 k 到 2k-1 之间i k n[i, ik)k剩余字符正好等于 k 个i k n[i, ik)k也就是剩余全部剩余字符为 0不满足循环条件无0注意最后一种情况for循环条件本身已经保证i n所以不会出现。另外“剩余字符正好等于 k”这一行既可以归到第一种也可以归到第二种最终反转区间其实是一样的。这就是为什么很多题解里只写一个if (i k n)就能覆盖所有情况。3.3 C 实现先上代码class Solution { public: string reverseStr(string s, int k) { int n s.size(); for (int i 0; i n; i 2 * k) { if (i k n) { reverse(s.begin() i, s.end()); } else { reverse(s.begin() i, s.begin() i k); } } return s; } };这里有一个特别重要的点std::reverse的区间是左闭右开。reverse(s.begin() i, s.begin() i k)反转的是[s[i], s[ik-1]]这一段不包含s[ik]。for循环里的i 2 * k是这题的精髓。很多错误的解法是把i放在循环里然后额外计数结果计数和步进两套逻辑互相干扰非常容易写乱。最好的方式是把 2k 作为循环步长因为题目本身就是“每隔 2k 个字符处理一次”。3.4 手写反转区间如果不想依赖库函数可以自己写一个reverseInRangevoid reverseInRange(string s, int l, int r) { while (l r) { swap(s[l], s[r--]); } }注意这里我定义的是“闭区间[l, r]”调用时的第二个参数要传i k - 1而不是i k。如果直接用std::reverse的“左闭右开”习惯很容易在第二个参数上差 1这是 541 题最经典的边界陷阱之一。我个人的建议是这道题直接用std::reverse最稳因为库函数已经帮你处理好了边界手写版本更适合在纸上推演边界条件或者面试时展示底层理解。两者都写一遍你对“闭区间”和“半开区间”的感受会深很多。3.5 边界测试写完之后一定要测这些用例s abcdefg, k 2输出应是bacdfeg。s abcd, k 2前 2 个反转输出应为bacd。s abcdefgh, k 3前 3 个反转、后 3 个反转输出为cbadefhg。我拿最后一个用例手动模拟过一次发现手算结果和代码结果不一致最终定位在自己按 3k 分段而不是 2k 分段这个细节也提醒我审题要抓“每隔多少”而不是“每多少”。另外k 1是一个很有意思的用例每 2 个字符反转前 1 个反转长度为 1 的区间等于什么都不做所以整个字符串保持不变。如果提交代码在k 1时结果变了说明反转区间长度写错了。4. 替换数字原地扩容与从后往前填充4.1 题目背景与朴素做法“替换数字”这道题在很多 C 刷题专栏里都出现过题目内容是给定一个只包含小写字母和数字字符的字符串把所有数字字符替换成number。比如a1b2c3变成anumberbnumbercnumber。第一次看到时很多人的第一反应是新建一个字符串遍历原字符串遇到数字就push_back一个number否则push_back原字符。这个做法确实能通过代码也很短string replaceDigitsSimple(string s) { string res; res.reserve(s.size()); for (char c : s) { if (c 0 c 9) { res number; } else { res c; } } return res; }这个解法的时间复杂度是 O(n)但额外空间是 O(n)。大多数情况下这样写没有任何问题也符合工程习惯。但既然这道题经常出现在“字符串双指针”的章节里我们就得掌握它的经典原地解法先扩容再从后往前填充。4.2 双指针从后往前的 O(n) 解法C 的std::string是可变的允许我们resize扩容。所以可以先统计数字个数然后一次性把字符串变长再用两个下标分别从新旧字符串的末尾向前走。具体步骤如下统计字符串中数字字符的数量count。记住旧长度oldLen s.size()。计算新长度newLen oldLen count * 5因为每个数字由 1 个字符变成 6 个字符的number净增加 5 个字符。调用s.resize(newLen)。让i指向oldLen - 1j指向newLen - 1从后往前遍历和填充。如果s[i]是数字就把number填充到j往前的连续 6 个位置否则把s[i]复制到s[j]。核心代码如下string replaceDigits(string s) { int count 0; for (char c : s) { if (c 0 c 9) { count; } } int oldLen s.size(); s.resize(oldLen count * 5); for (int i oldLen - 1, j s.size() - 1; i 0; --i, --j) { if (s[i] 0 s[i] 9) { s[j--] r; s[j--] e; s[j--] b; s[j--] m; s[j--] u; s[j] n; } else { s[j] s[i]; } } return s; }这里填充顺序是r e b m u n我再强调一下为什么是这个顺序因为j指向的是新字符串的末尾也就是最后一段空位的最右端。我们要把 6 个字符从左到右写成n u m b e r就必须从右往左依次填r e b m u n。如果写反字符串会变成rebmun的拼接看起来像一段乱码。写完每个字符后j都会向前移动一位。注意n那行用的是s[j] n而不是s[j--] n因为循环体结束后for里还有一个--j会把j再往前移一位。如果不小心写成s[j--] n就会跳过一格导致字符串中间留出一个未定义的字符位。4.3 为什么必须从后往前覆盖问题的根因前面提到朴素做法是新建数组那原地做法为什么不能从前往后改原因很简单从前往后遍历时把1替换成number会覆盖掉后面的字符。比如字符串a1b把a先放到新位置接着把1替换成 6 个字符这些字符会直接覆盖掉后面的b而b还没有被处理等你遍历到这个位置时原始字符已经丢了。有人可能会想那先开辟好空间然后把字符整体先复制到后面再从前往后写这当然可以但会多一次 O(n) 的拷贝。而“从后往前”的高明之处在于右侧的空间本来就是新加出来的从右往左写不会覆盖任何还没处理的旧字符。每个旧字符在被覆盖之前要么已经复制到了新的位置要么已经被“消费”了。这个思维不光在这道题里有用。比如合并两个有序数组的时候很多题解也要求从后往前填充目的同样是为了避免覆盖未处理的元素。一旦你理解了“从后往前处理是一种原地算法里常用的空间复用手法”这类题基本可以通用。5. 易错点与排查技巧5.1 三题合一的常见错误清单刷完这三道题我把常见的坑整理成了一份清单344 里用while (left right)导致中间字符被交换两次虽然结果碰巧一样但逻辑不干净。541 里把i 2*k写成i并用额外计数器维护 2k 分组代码又长又容易出错。541 里手写reverseInRange时闭区间的第二个参数写成i k导致多反转一个字符。替换数字时resize长度写错把count * 5写成count * 6导致多扩容一个字符位。替换数字时填充顺序写反从右往左填成了n u m b e r输出变成rebmun。用isdigit(c)时忘了包含cctype或者在一些特殊环境下受 locale 影响建议直接用c 0 c 9。忽略空字符串。三个题目对空字符串的期望都是直接返回但如果直接把s.size() - 1赋给right当n 0时会变成很大的无符号数导致越界。5.2 排查技巧把边界打印出来我自己的习惯是在提交前把边界条件在本地跑一遍。特别是 541 题我会把每种剩余情况分别打印出i、i k、s.size()然后对照题目描述判断reverse区间是否符合预期。打印这一步看似多此一举但遇到size_t无符号类型时能救你一次。举个例子s.size()返回的是size_t是无符号类型。如果i k也是无符号整数一旦i k的中间结果超出 int 范围或者和n的类型不一致比较行为会非常诡异。我的建议是先把n转换成int或者直接写成int n s.size();后续所有比较都用int可以避免很多隐蔽问题。当然力扣这类题的数据范围通常不会让int溢出但养成类型一致的习惯总没有坏处。5.3 一道典型变式把“替换数字”改成“替换空格”替换数字这套从后往前的模板稍微改改就能解决剑指 Offer 里的“替换空格”把空格替换成%20每个空格增加 2 个字符。代码里只需要把count的统计条件从数字改为空格把填充字符从number改为02%其它逻辑完全一样。类似地还可以改成统计某个字符然后替换成更长的串。所以相比死记硬背建议大家记住一个通用流程统计需要替换的字符数量 → 计算新长度 →resize扩容 → 双指针从后往前填充。这个流程在面试中可以用来解决一大票“字符串原地替换/原地填充”问题。你只要在纸上画一次覆盖过程就能理解为什么从后往前是安全的。这一轮刷下来我最大的体会是字符串题考的不是奇技淫巧而是对下标和边界条件的敏感度。344、541、替换数字三道题代码量加起来不到 80 行但每一个坑都值得拿小本本记下来。我之前面试时被问过“替换空格”的原地写法当场写太快把resize长度算错就是典型的对扩容逻辑不够熟。所以如果你也在准备面试或刚开始刷题不妨把这三题当成一个组合套餐先做一遍再看一遍这篇日记里的边界分析最后合上题解自己写一遍。写不出来的地方往往就是你最需要补的地方。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑