资讯动态

C++字符串刷题复盘:替换数字、反转字符串与双指针边界

发布时间:2026/9/30 12:40:07 来源:尧图企业网站定制
最近整理C刷题笔记把三道跟字符串相关的题目放在同一份文档里复盘替换数字、力扣344题反转字符串、力扣541题反转字符串II。前两道看起来都是入门难度但我在本地跑“替换数字”的时候第一次意识到字符串替换类操作不能只看结果对不对还得想清楚替换过程中长度变化带来的开销。541题又在一个很简单的解法后面藏了不少边界条件稍不注意就会踩空。这篇日记把三道题的完整思路、C实现和调试过程记录下来给同样用C刷力扣的朋友做参考。如果你刚接触C、正在从数组往字符串过渡这三道题恰好能帮你把双指针、步进循环、扩容倒写这几个基本功串起来。1. 为什么把替换数字、344题、541题放进同一篇刷题日记1.1 三道题覆盖了C字符串操作的三个关键阶段先说这三道题的来源差异。344和541都是力扣原题难度分别是简单和简单偏中等“替换数字”严格来说不在力扣而是很多C刷题路线里的扩展练习通常出现在需要读入一行字符串、输出替换结果的OJ场景中。把这三道题放在一起并不是因为它们名字里有“字符串”这么简单而是它们的核心手法刚好构成了一条递进链替换数字考察的是“扩容后倒序填充”本质是处理字符串长度变化的问题。344题考察的是“双指针交换”本质是原地修改字符序列。541题考察的是“按固定间隔切分区间并局部反转”本质是把344题的反转能力复用到一个步进循环里。从实际刷题体验看很多人在3个月后会忘记541题的写法不是因为难而是因为没有理解它和344题之间的承接关系。只要把344题的reverse思路吃透541题就是套了一层步长外壳的循环题。替换数字又刚好补上了“字符串长度变化”这个力扣简单题里很少出现的知识点。三者合在一起正好覆盖了做字符串题最常遇到的三类操作读入处理、局部修改、区间迭代。1.2 用C刷这类题时大多数人的卡点并不在语法我见过不少朋友C基本语法都会for、while、vector、string都用得挺熟但一到字符串题就卡住。深入观察后发现卡点往往集中在三个地方分不清string和vectorchar的差异344题给的是vectorchar有些人非要在上面调用length()再顺手erase结果报错一堆。不知道std::reverse接收的是迭代器区间写reverse(s, sk)会直接编译失败。遇到需要替换或插入的场景本能地想到从头往后处理结果复杂度变成O(n²)还不自知。这三道题刚好分别把这三个问题打了一次。重剑无锋基础题恰恰是暴露这些细节的最好工具。我比较推荐的做法是每道题先争取一遍通过然后再花五分钟想一想如果不用库函数能不能自己写如果输入长度扩大一百倍会不会超时。这种“做完之后再多问一句”的习惯比单纯刷遍数有用得多。2. 替换数字先理解为什么不能边遍历边插入2.1 题目原貌与OJ输入输出形式的自测方式这道题的描述很简洁给定一个字符串把其中所有的数字字符0到9替换成number其他字符保持不变。比如输入a1b2c3输出就是anumberbnumbercnumber。力扣上的题目通常只需要你补全一个类成员函数输入输出已经由评测机处理好了。但“替换数字”这类题目一般出现在独立OJ或刷题手册里要求你自己写完整的main函数自己负责cin读入和cout输出。很多习惯力扣刷题模式的朋友第一次写这种本地运行的代码时会不习惯其实它反而更接近工程实践你要对输入输出的样子有明确预期。我的自测方式很简单先准备这样几个用例放在注释里每改一次代码就跑一遍输入a1b2c3期望输出anumberbnumbercnumber输入abc期望输出abc输入123期望输出numbernumbernumber输入空串期望输出空串空串这个用例容易被忽略。如果代码里用了cin s空串其实不好模拟但你要清楚函数体处理空字符串时必须直接返回不能在resize之后再去访问oldSize - 1否则会是负数下标。2.2 核心思路倒序填充是如何省掉高额移动成本的先说最直觉的做法从左往右遍历字符串遇到数字就原地插入number。这个问题出在“插入”二字上。C的std::string底层是连续内存数组在中间插入元素时它后面的所有字符都必须整体向后移动。每遇到一个数字就移动一次假设字符串里有m个数字最坏情况下每个数字后面都跟着大量字符整体复杂度会逼近O(n*m)。字符串长度一旦上万这种写法就很难看。更优的做法是三步走先遍历一遍原字符串统计数字的个数count。把字符串resize到原长度 count * 5。为什么是5因为一个数字字符只占1个位置替换成number需要6个字符净增加5个字符。有多少个数字就额外多分配5*count个字符位置。从后往前遍历填充一个指针i指向原串末尾一个指针j指向扩容后的末尾s[i]不是数字就直接复制s[i]是数字就依次逆序写入r、e、b、m、u、n。倒序填充的精髓在于所有字符只移动一次不需要反复后移整块数据。从最后一个字符往前逐个处理虽然每个数字依然要写6个字符但整体是线性扫描复杂度O(n)。2.3 完整实现与易错点排查下面是我在本地测试通过的版本#include iostream #include string using namespace std; int main() { string s; cin s; int oldSize s.size(); int count 0; for (char c : s) { if (c 0 c 9) { count; } } s.resize(oldSize count * 5); int i oldSize - 1; int j s.size() - 1; while (i 0) { 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; i--; } else { s[j--] s[i--]; } } cout s endl; return 0; }这里有几个容易踩的点我按踩过之后印象从深到浅说resize之后原字符串后面多出来的位置默认是\0不要指望它是空格。但因为是从后往前覆盖所以这些空位置会在填充过程中被逐个替换掉只要最后j没使用到它们即可。s[j--] r的写法是“先赋值再自减”。第一次写的时候我把顺序弄反结果明明该落在位置20的内容跑到了位置19整串错位。检查时很难一眼看出来建议先把循环里的顺序固定成“先赋后减”。判断数字可以用c 0 c 9也可以使用cctype里的isdigit。刷力扣时考虑到头文件兼容性我更喜欢前者一个char判断不引入额外头文件。如果输入字符串长度为0oldSize - 1就是-1while (i 0)不会进入可以直接输出空串所以空串场景其实安全。补充一句题目为什么要求替换成number而不是其他词因为这正好能体现字符串扩容和倒序填充的价值。如果替换成一个短单词比如a那正序插入也能接受不需要费这个劲。刷题时看到替换目标长度比原来长第一反应就应该想到扩容和倒序而不是就地硬插。3. 344题反转字符串手写双指针到底在练什么3.1 题目约束与输入类型vector 的用意344题的题干很简短编写一个函数其作用是将输入的字符串反转过来。输入字符串以字符数组s的形式给出必须原地修改输入数组使用O(1)的额外空间。注意这里的输入类型是vectorchar不是string。力扣之所以这么设计是刻意不让学习者依赖字符串的拼接、插入等便捷性质逼你把注意力放在“交换两个位置上的字符”这个动作上。vectorchar和string在内存布局上很接近但string有大量现成的成员函数你可以轻松拼接、替换、找子串vectorchar则更接近原始数组你能用的核心操作就是索引、赋值、交换。如果你在本地用VSCode调试这道题可以直接把vectorchar的输入构造出来#include iostream #include vector using namespace std; void reverseString(vectorchar s) { int left 0; int right s.size() - 1; while (left right) { swap(s[left], s[right]); left; right--; } } int main() { vectorchar s {h, e, l, l, o}; reverseString(s); for (char c : s) { cout c; } cout endl; return 0; }3.2 双指针写法和C的swap双指针的核心逻辑一句话就能说清左指针从头往中间走右指针从尾往中间走每走一步就交换左右指针指向的元素直到两个指针相遇或交错。这段代码很多人写出来长这样void reverseString(vectorchar s) { int left 0, right s.size() - 1; while (left right) { char tmp s[left]; s[left] s[right]; s[right] tmp; left; right--; } }这里用了char tmp做临时变量。C11之后有更简洁的写法直接用标准库的swapwhile (left right) { swap(s[left], s[right]); left; right--; }两者做的事情一样但后者可读性更好。我个人的习惯是手写反转时用临时变量因为这样能让你时刻记得交换的本质在确认思路没问题后再看标准库的swap。这样既能应付面试官“你手写一下”的要求也能在写工程代码时自然使用库函数。复杂度方面时间O(n)只需要遍历半个数组额外空间O(1)因为除了几个临时变量没有使用任何辅助容器。3.3 写题时可以不用库函数的边界有人会问既然C标准库有std::reverse这一行就能解决reverse(s.begin(), s.end());那手写双指针的意义是什么我的看法是这道题的教学目标不是“把数组反转”而是让你建立“两根指针从两端对撞”的思维模型。这个模型会出现在后面很多题目里比如判断回文串、有序数组两数之和、求容器最多盛水。你用库函数做对一道题很容易但下次遇到新题时脑子里没有那个对撞指针的画面就很难迁移过去。那么实际刷题时该不该用std::reverse我的建议分两种情况如果是面试或学习阶段先手写再提一句“标准库里也有现成的reverse”展示你既懂原理也熟悉工程工具。如果是竞赛或刷题想节省时间直接用std::reverse没有问题评测机又不会因为用了库函数扣分。真正的关键不是“用不用库函数”而是“知不知道库函数底层在做什么”。344题就是检验这件事的一道题。4. 541题反转字符串II循环边界与反转区间4.1 规则解读2k一组的步进来历541题是344题的进阶版题干比较绕我先把规则拆开给定一个字符串s和一个整数k从字符串开头算起每计数至2k个字符就反转这2k个字符中的前k个字符。如果剩余字符少于k个则将剩余字符全部反转。如果剩余字符小于2k但大于或等于k个则反转前k个字符其余字符保持原样。举个例子s abcdefgk 2。每2k 4个字符一组前4个字符是abcd反转前2个得到bacd第5到第7个字符是efg剩余字符数是3大于等于k2所以反转前2个得到feg。最终结果是bacdfeg。很多人第一次读题会按人类直觉分组处理先取2k个再判断剩余情况。但代码实现里有一个更统一的视角。因为每次处理的起始位置都是0, 2k, 4k, 6k...所以主循环天然可以写成for (int i 0; i s.size(); i 2 * k) { ... }这个i 2*k就是题目的本质规律步进间隔永远是2k不管字符串剩余多少只管当前步的起点。4.2 统一边界条件i k 和 s.size()的关系关键差别出现在“这一组实际要反转多长”。仔细分析规则可以发现是否反转全部剩余字符取决于从当前位置i开始往后数k个字符是否越界如果i k s.size()说明剩余字符至少还有k个那么一定反转区间[i, i k)也就是前k个字符。如果i k s.size()说明剩余字符不足k个那就反转区间[i, s.end())也就是把剩余全部字符反转。这两种情况用一句if分开但不少初看代码的人会觉得奇怪为什么只判断“够不够k个”而不单独判断“够不够2k个”因为规则里“剩余小于2k但大于等于k个只反转前k个”这个分支其实已经被i k s.size()覆盖了。当剩余数量在[k, 2k)之间时i k依然不越界所以反转前k个完全正确当剩余数量少于k时i k越界代码落入else分支反转全部剩余字符。这样一个判断就统一了规则里的两个分支。建议自己拿s abcdefg, k 2走一遍循环i 0时i k 2 7反转[0, 2)得bacdefg。i 4时i k 6 7反转[4, 6)得bacdfeg。i 8时循环结束。这样验证完边界含义会明确很多。4.3 完整实现、测试用例与常用min写法我的实现如下#include string #include algorithm using namespace std; class Solution { public: string reverseStr(string s, int k) { for (int i 0; i (int)s.size(); i 2 * k) { if (i k (int)s.size()) { reverse(s.begin() i, s.begin() i k); } else { reverse(s.begin() i, s.end()); } } return s; } };这里有两处细节都是实测容易翻车的循环条件里的(int)s.size()。不转的话i是ints.size()返回size_t两者比较会产生有符号/无符号警告在某些编译环境里会有隐患。转成int后虽然字符串长度理论上有上限但刷题场景足够安全。reverse的区间是左闭右开。s.begin() i k表示的是反转区间的最后一个元素的下一个位置不是第k个元素本身。很多人第一次写会以为是闭区间导致漏掉一个字符或反转多一个字符。如果不想写if-else也可以这样reverse(s.begin() i, s.begin() min(i k, (int)s.size()));因为reverse本身就是左闭右开所以当剩余字符不足k时s.begin() (int)s.size()正好是末尾。这种写法更紧凑但说实话可读性不如if-else版本清晰。我推荐先写if-else跑通后再考虑改成min写法两种都要理解面试时随机应变。再看几个测试用例s abcd, k 2每2k4个字符一组反转前2个得到bacd。s abcdef, k 3每2k6个字符一组反转前3个得到cbadef。s abcdefg, k 3前6个字符中反前3个得到cbadef剩余1个字符少于3个全部反转得到cbadefg。s abc, k 1每2个字符一组每组反转1个字符等于没有变化返回abc。写代码前先在纸上把这些用例跑通再落到编辑器里会少走很多弯路。5. VSCode里本地调试力扣解法的一次实测5.1 配置g编译任务的tasks.json力扣网页编辑器可以跑题但“替换数字”这类OJ题和541题这种需要反复验证边界的情况我更习惯在VSCode里本地调试。热词里常看到“vscode配置c/c环境”这里分享一套我在Windows下用MinGW的配置简洁够用。先在项目根目录建.vscode/tasks.json用来配置编译任务{ version: 2.0.0, tasks: [ { label: C Build, type: shell, command: g, args: [ -g, -stdc17, ${file}, -o, ${fileDirname}/${fileBasenameNoExtension}.exe ], group: { kind: build, isDefault: true } } ] }这里的${file}是当前打开的C源文件${fileDirname}是它所在目录${fileBasenameNoExtension}是不带扩展名的文件名。编译生成的exe会放在源文件同目录下。如果g没有加到系统PATHcommand字段要写全路径比如C:\\MinGW\\bin\\g.exe。这是初学者最容易卡住的地方命令行说找不到编译器多半就是这个原因。5.2 配置launch.json让代码可以断点调试编译配置好之后还需要调试配置。在.vscode/launch.json里填入{ version: 0.2.0, configurations: [ { name: C Debug, type: cppdbg, request: launch, program: ${fileDirname}/${fileBasenameNoExtension}.exe, args: [], stopAtEntry: false, cwd: ${fileDirname}, environment: [], externalConsole: false, MIMode: gdb, miDebuggerPath: gdb.exe, preLaunchTask: C Build } ] }几个参数说清楚preLaunchTask填C Build表示每次按F5调试前先自动编译省去手动编译的步骤。miDebuggerPath是gdb的位置。Windows下装MinGW一般自带gdb.exe如果没配置可以把路径写成C:\\MinGW\\bin\\gdb.exe。externalConsole设为false调试时终端会集成在VSCode下面方便统一看输出。这套配置对力扣刷题来说足够用。以后每写一道新题只要源文件是单个.cpp按CtrlShiftB编译按F5调试不用反复改配置。5.3 把力扣函数改成本地可运行main的通用骨架力扣题给的类成员函数没法直接本地运行需要自己包一层main。我常用这样一个模板#include iostream #include string #include vector #include algorithm using namespace std; class Solution { public: string reverseStr(string s, int k) { // 力扣代码 } }; int main() { Solution sol; string s abcdefg; int k 2; string result sol.reverseStr(s, k); cout result endl; return 0; }如果是“替换数字”这种需要cin读入的题就直接在main里读int main() { string s; cin s; // 处理逻辑 cout s endl; return 0; }这里给一个建议不要在main里写太多测试逻辑每次改动就编译一次太浪费时间。我会在本地维护一个test.cpp里面放三四个测试用例用assert或者直接打印结果一次编译就能验证所有边界。6. 三道题复盘字符串题最容易被忽略的陷阱清单6.1 一张表看三个不同的字符串操作场景把三道题放在一起复盘可以更清楚地看到C字符串题最常见的几个考点题目操作类型核心技巧复杂度要求最容易踩的坑替换数字替换扩容先统计数字个数resize后倒序填充O(n)时间O(1)额外空间正序插入导致O(n²)忘记倒序写入344反转字符串原地反转双指针swapO(n)时间O(1)额外空间把vector 当string用忘记左闭右开541反转字符串II区间反转按2k步进循环reverse区间O(n)时间O(1)额外空间不含返回值边界判断写错ik和size()比较时的类型不一致三题串着看你还会发现一个共性它们都强调“少用额外容器”。344题明确要求O(1)额外空间541题虽然没强调空间但最佳解法也是原地反转替换数字的倒序填充同样只依赖字符串自身扩容后的空间。这说明在字符串题里原地修改不仅是一种能力更是一种思维倾向。能用好连续内存的局部性往往比不断创建新字符串更高效。6.2 常见陷阱自查清单结合我自己的踩坑经历整理一份针对字符串题的排查清单写完代码后逐条检查字符串长度会变吗如果会变先想清楚变长多少有没有扩容预留。输入是string还是vectorchar这两者不能混用尤其注意string有size()、length()vectorchar也有size()但成员函数和迭代器类型有差异。有没有比较int和size_t养成(int)s.size()的习惯避免有符号/无符号比较的潜在问题。reverse的区间是[first, last)写反转时先确认last是不是真的指向“最后一个元素的下一个位置”。空串和全部字符都满足某个条件的情况是否在测试用例里覆盖过。这五条看着琐碎但在本地调试时它们几乎能解决我在字符串题里遇到的一半报错。6.3 后续可以继续挑战的同类题这三道题做透后再往上走可以试试力扣热题100里的字符串题。我自己刷下来比较推荐按这个顺序继续151题“反转字符串中的单词”把344题的原地反转扩展到“先反转整体、再反转每个单词”的思路。剑指Offer 58-II“左旋转字符串”同样可以用局部反转加整体反转三步法。力扣459题“重复的子字符串”考察字符串匹配与周期性质能加深对字符串底层匹配的理解。力扣28题“找出字符串中第一个匹配项的下标”这是KMP算法的经典入口刷完对比暴力匹配收获很大。这几道题的核心手法几乎都能在替换数字、344、541三道题里找到影子。先打好双指针、步进循环、区间反转的基本功后面做中等题的时候至少不会在“从哪一段开始反转”这类问题上卡住。我在实际刷这几道题的过程中最大的感受还不是“会了某道题”而是学会了在写代码前先问自己三个问题输入到底是什么类型处理过程中字符串长度变不变反转或替换的区间边界到底在哪里。把这三个问题想清楚字符串类的简单题基本都能一遍过。

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

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

免费获取报价 →
↑