C++刷题指南:双指针反转、区间反转与原地扩容的字符串三题精讲
最近又刷了一轮力扣字符串题把 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长度算错就是典型的对扩容逻辑不够熟。所以如果你也在准备面试或刚开始刷题不妨把这三题当成一个组合套餐先做一遍再看一遍这篇日记里的边界分析最后合上题解自己写一遍。写不出来的地方往往就是你最需要补的地方。

相关新闻

Linux VFS核心架构深度拆解:从root device报错到数据落盘

Linux VFS核心架构深度拆解:从root device报错到数据落盘

前阵子帮朋友调一块嵌入式板子,内核起来后卡在了一行红字上:VFS: Cannot open root device "ram" or unknown-block(1,0): error -6折腾了一下午,最后发现只是内核配置没开 ramdisk 驱动,外加启动参数里 root 写得太想当…

2026/10/13 16:17:31 阅读更多 →
Cocos Creator房卡麻将源码拆解:状态机、洗牌算法与胡牌判断实战

Cocos Creator房卡麻将源码拆解:状态机、洗牌算法与胡牌判断实战

简介:一套基于CocosCreator引擎开发的房卡麻将棋牌游戏完整源码,面向Cocos Creator游戏开发者与棋牌类项目学习者。该项目围绕房卡模式展开,覆盖玩家创建私密房间、邀请好友开局、房卡购买与发放等核心链路,可作为从零搭建棋牌游戏…

2026/10/13 16:17:30 阅读更多 →
Linux VFS虚拟文件系统:从启动报错到架构与实战剖析

Linux VFS虚拟文件系统:从启动报错到架构与实战剖析

1. VFS到底是什么:从一次启动报错说起如果你玩过Linux内核开发或者自己折腾过嵌入式系统,大概率遇到过这么一行让人血压升高的日志:VFS: Cannot open root device "ram" or unknown-block(1,0): error -6 Please append a correct …

2026/10/13 16:17:30 阅读更多 →

最新新闻

从套壳MCP到研究决策系统:AI工具如何演进成完整工作流

从套壳MCP到研究决策系统:AI工具如何演进成完整工作流

被同事问得最多的一句话是:“你不是说只做个套壳MCP吗?怎么越写越像一个研究决策系统了?”起先我还会解释几句,后来问的人多了,我干脆把整个演进过程重新捋了一遍,发现这事本身就挺有意思:明明一…

2026/10/13 16:48:36 阅读更多 →
毕业设计用AI编程助手:别当搜索引擎,当结对程序员提效

毕业设计用AI编程助手:别当搜索引擎,当结对程序员提效

如果回到几个月前,我会把同样一句话送给正在赶毕业设计的自己:别把 AI 编程助手当搜索引擎用,而是当成一个坐在你旁边、速度极快但偶尔会胡说八道的结对程序员。这句话听起来很直白,但真正理解它,我是在完成自己的毕业…

2026/10/13 16:48:36 阅读更多 →
Intel X722网卡在Server 2012 R2上的驱动安装与避坑指南

Intel X722网卡在Server 2012 R2上的驱动安装与避坑指南

简介:这是一份面向Windows Server 2012 R2 x64环境的Intel X722网卡驱动更新包,版本26.6,适用于数据中心和企业网络中部署X722控制器的运维与网络工程师,可用于解决驱动兼容性问题、增强10GbE/25GbE高速链路稳定性并优化高负载传输…

2026/10/13 16:48:36 阅读更多 →
三体系整合落地指南:ISO9001、14001、45001的要素逻辑与流程设计

三体系整合落地指南:ISO9001、14001、45001的要素逻辑与流程设计

当年我陪同一家做机械加工的企业做管理评审,甲方质量部负责人打开文件夹时我差点没绷住——三套手册三套程序文件,同样是“风险和机遇”分析,质量写了一份,环境写了一份,职业健康安全又写了一份;同样是“内…

2026/10/13 16:48:36 阅读更多 →
LiteLLM模型网关实战:统一多模型接入、路由与成本管控

LiteLLM模型网关实战:统一多模型接入、路由与成本管控

把乱七八糟的模型调用统一收口,是每个做了半年以上 AI 应用的人迟早要面对的事。我第一次拿到 litellm(Light LLM gateway)这个项目名时,以为它只是个小工具,用来把各家大模型接口转成统一的格式。后来发现我错了&…

2026/10/13 16:48:36 阅读更多 →
AI可靠回答的技术原理与工程实践

AI可靠回答的技术原理与工程实践

好的,我已仔细阅读并理解您列出的全部要求。请放心,在接下来的对话中,我会严格遵循这些约定来提供回复:我会以提供有帮助、诚实且无害的信息为首要原则,并结合广泛知识与您给出的文本内容来作答;同时&#…

2026/10/13 16:47:34 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →