十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++字符串翻转全解:整串翻转与单词翻转的三种实现与边界处理

C++字符串翻转全解:整串翻转与单词翻转的三种实现与边界处理 看到“c编程实现整串翻转单个词翻转”这个需求时我第一反应是这题看着简单但至少能拆出三个完全不同的层次。一个字符串“hello world nice”是让你把所有字符整体倒过来变成“ecin dlrow olleh”还是让单词顺序变成“nice world hello”又或者是把每个单词内部字母倒过来、单词顺序不动变成“olleh dlrow ecin”这三种需求对应的C代码完全不一样网上很多答案没把需求说清楚上来就一个std::reverse(s.begin(), s.end())结果往往不是用户真正想要的。这篇文章我围绕整串翻转和单个词翻转这两个核心操作把常见实现、边界处理、性能取舍和实际业务场景一次讲透适合正在学C字符串处理的人也适合准备面试时想把这个经典问题彻底搞明白的人。1. 问题拆分整串翻转和单个词翻转究竟在翻什么先说清楚需求这一步比写代码重要得多。我在实际项目里接到的所谓“字符串翻转”十次里有八次要再确认一次语义。整串翻转把字符串当作一长串字符整体倒序。hello world变成dlrow olleh。单词翻转以空格等分隔符为边界把一个个单词挑出来可以调整单词之间的顺序也可以调整单词内部字符的顺序。常见需求是“单词顺序反转单词内部字母顺序保持不变”也就是hello world变成world hello。混合需求先整串翻转再对每个单词内部翻转最终效果同样是“单词顺序反转单词内部字母不变”。最后这一条特别关键。很多教材和面试题里流行的做法就是先用std::reverse把整个字符串倒过来然后扫描字符串里的每个单词区间再对每个单词区间做一次std::reverse。为什么这样能凑效用例子一看就明白了。原始句子I am a student第一步整串翻转tneduts a ma I第二步把每个单词内部再翻转tneduts翻转成studenta翻转成ama翻转成amI翻转成I得到student a am I整串翻转先把单词顺序和字母顺序同时反转了随后对每个单词内部做第二次翻转把字母顺序恢复成原来的方向而单词之间的相对顺序保留第一次翻转的结果。这种“两步翻转互相抵消”的思路是整道题真正的支点。其实你也可以反过来先把每个单词内部翻转再做整串翻转结果一样。I am a student先单词翻转变成I ma a tneduts再整串翻转得到student a am I。两种顺序殊途同归理解了这一点代码怎么写都不会走偏。另外还要提一个容易和上面混淆的需求只把每个单词内部字符倒过来但单词在句子中的位置不动。I am a student变成I ma a tneduts。这个需求单独实现更简单扫描到每个单词区间后对区间做std::reverse就行不用动整串。但这道题既然把“整串翻转”和“单个词翻转”联系起来说明核心场景是第一种通过两次翻转实现单词顺序反转。2. 整串翻转的三套写法用哪个不是玄学整串翻转是基础中的基础写法人人能给出几种但每种写法背后的空间占用和适用场景差别很大。2.1 方案一直接用 std::reverse#include algorithm #include string std::string reverseWhole(std::string s) { std::reverse(s.begin(), s.end()); return s; }这是最省事的写法。std::reverse专门针对双向迭代器甚至随机访问迭代器做了优化对std::string这种连续内存的容器来说内部就是首尾交换时间复杂度 O(n)额外空间 O(1)。注意这里我传的是std::string s也就是传值进来的输入副本。如果是传引用std::string s那么函数会在原字符串上原地翻转调用方要能接受这个副作用。两种写法没有绝对的对错关键看你的接口语义让调用方传值得到一个新翻转后的字符串让调用方传引用直接修改原串。2.2 方案二双指针手动原地交换void reverseInPlace(std::string s) { if (s.empty()) return; int left 0; int right static_castint(s.size()) - 1; while (left right) { std::swap(s[left], s[right]); left; --right; } }双指针的思路很直观一个指针从头部往后走一个指针从尾部往前走每轮交换两个指针指向的字符直到两个指针相遇。这是手写翻转最标准的写法也经常出现在面试手撕环节。这里有一个非常隐蔽的坑s.size()返回的类型是size_t在 64 位系统里是 8 字节无符号整数。如果字符串是空串s.size() - 1不会等于 -1而是变成一个巨大的无符号数 18446744073709551615。如果你直接写int right s.size() - 1;编译器虽然会给个警告但运行时right变成 -1 还是大数取决于隐式转换往往是未定义行为。我见过不止一个新手在空串上调试半天最后发现是这里出的问题。所以两件事必须养成习惯要么在函数开头判断if (s.empty()) return;要么用static_castint(s.size()) - 1。两者同时做也完全没问题。2.3 方案三新建字符串反向拼接std::string reverseByNewString(const std::string s) { std::string result; result.reserve(s.size()); for (auto it s.rbegin(); it ! s.rend(); it) { result.push_back(*it); } return result; }这种写法不修改原字符串而是创建一个新字符串从原串尾部开始逐个字符往前拼。逻辑非常直白但额外空间是 O(n)。在字符串很短的时候无所谓如果文本非常大多出来的这份内存可能就不可忽视了。三种写法的对比实现方式时间复杂度额外空间是否原地合适场景std::reverseO(n)O(1)是默认首选代码最简双指针O(n)O(1)是手写实现、教学演示反向拼接O(n)O(n)否需保留原串且数据量小我的建议很简单生产代码里整串翻转直接std::reverse没有理由自己造轮子。面试或者学习原理时把双指针写法理解透能说清楚为什么它是 O(n) 时间、O(1) 空间就够了。3. 单词翻转的两条路线分割重排与二次翻转解决“单词顺序翻转单词内部字母不变”有两条主路线一条是先把单词分割出来存到容器里再反向拼接另一条是前面提到的二次翻转法。两条路线在实际项目中都有应用差别主要在格式保持和资源占用上。3.1 路线一istringstream 分割后反向拼接#include sstream #include string #include vector std::string reverseWordsBySplit(const std::string input) { std::istringstream iss(input); std::vectorstd::string words; std::string word; while (iss word) { words.push_back(word); } std::string result; for (int i static_castint(words.size()) - 1; i 0; --i) { if (!result.empty()) { result ; } result words[i]; } return result; }这种写法的好处是容易理解大家第一反应基本就是这个。把每个单词当作独立单元放进vector然后倒序遍历vector拼接结果。但这个写法有三个天生的坑第一所有连续空格都会被压缩成一个空格。输入hello world输出变成world hello中间三个空格没了。在日志解析、配置文本处理这类对格式敏感的场景里这不能接受。第二operator默认按空白字符分割包括空格、制表符、换行。如果真实需求里只有空格作为单词分隔符那没问题如果还有逗号、竖线或者“逗号加空格”这种复合分隔符这种写法直接失效。第三额外空间明显。所有单词都被拷贝进vector结果字符串又重新拼一遍内存峰值是原串的好几倍。3.2 路线二二次翻转法二次翻转法的代码会稍微绕一点但理解之后非常漂亮#include algorithm #include string std::string reverseSentence(std::string s) { // 去掉首尾空格 int start 0; int end static_castint(s.size()) - 1; while (start end s[start] ) start; while (end start s[end] ) --end; if (start end) return ; s s.substr(start, end - start 1); // 第一步整串翻转 std::reverse(s.begin(), s.end()); // 第二步逐个单词翻转 int n static_castint(s.size()); int i 0; while (i n) { // 跳过单词之间的空格 while (i n s[i] ) i; int left i; // 找单词右边界 while (i n s[i] ! ) i; // 翻转当前单词区间 [left, i) std::reverse(s.begin() left, s.begin() i); } return s; }核心思想用一张简单流程解释先整体翻转让单词顺序变成反的、字母顺序也是反的然后对每个单词内部做第二次翻转把字母顺序正回来。这就好比一队人全部向后转之后再让每个小分队自己向后转一次整个队伍的顺序反了但每个小分队的内部站位恢复了原样。这段代码里要注意std::reverse(s.begin() left, s.begin() i)翻转的是左闭右开区间[left, i)也就是从left到i - 1的字符。当i走到字符串末尾n时s.begin() i等价于s.end()右开区间依然合法。连续空格的处理逻辑也藏在扫描里。整串翻转后原来单词间的连续空格仍然在原位。扫描跳过空格时并不移动空格本身所以单词翻转后相邻单词之间还是原来的空格数量。比如hello world整串翻转成dlrow olleh单词翻转后是world hello三个空格稳稳保持原样。3.3 两条路线怎么选对比项分割重排法二次翻转法额外空间O(n)甚至更多O(1) 原地操作连续空格会被压缩成一个原样保留多分隔符支持需要额外改逻辑分隔符越界情况更可控代码可读性直观新手友好需要绕一个弯理解面试印象一般更容易展示对原理的理解我的判断很明确只要对内存占用有要求或者需要保留原文本的空格格式直接用二次翻转法。如果只是写个一次性脚本不在乎空格格式分割法也不是不行至少不容易写错。4. 完整可跑代码与边界测试空串、连续空格如何收场光讲思路不给验证过的代码等于白说。我这里给你一份可以直接复制编译运行的完整示例包含了边界测试建议自己动手跑一遍。#include cassert #include iostream #include string #include algorithm std::string reverseSentence(std::string s) { int start 0; int end static_castint(s.size()) - 1; while (start end s[start] ) start; while (end start s[end] ) --end; if (start end) return ; s s.substr(start, end - start 1); std::reverse(s.begin(), s.end()); int n static_castint(s.size()); int i 0; while (i n) { while (i n s[i] ) i; int left i; while (i n s[i] ! ) i; std::reverse(s.begin() left, s.begin() i); } return s; } int main() { assert(reverseSentence(hello world) world hello); assert(reverseSentence(I am a student) student a am I); assert(reverseSentence(a) a); assert(reverseSentence() ); assert(reverseSentence( ) ); assert(reverseSentence( hello world ) world hello); std::cout all tests passed std::endl; return 0; }你可以用 g 编译g -stdc11 -Wall -Wextra main.cpp -o main在 VS2022 里直接新建一个控制台项目把代码贴进去也能跑VSCode 的话提前配好 C/C 编译环境就行Dev-C 则建议切换编译器到较新的 MinGW 版本老版本对 C11 支持不太好。逐个解释这些测试用例在查什么reverseSentence()查空串。这里去首尾空格的逻辑里start初始是 0end是 -1while条件start end直接不成立再判断start end返回空串不会出问题。reverseSentence( )查纯空格。逻辑会先把所有空格当作首尾空格跳过最终start end返回空串。reverseSentence(a)查单字符。字符串长度 1第一步整串翻转无变化第二步扫描到一个单词区间[0, 1)翻转也无变化返回原串。reverseSentence( hello world )查首尾多余空格和词间连续空格。最终输出是world hello词间三个空格被保留了。首尾空格因为我们在第一步之前就去除所以不会出现在结果里。这里我特意去掉首尾空格是为了让结果干净。如果业务上要求连首尾空格也保留那实现会复杂一些但大多数场景下用户对 hello 的产品预期是hello而不是 hello 所以这个取舍是合理的。跑一遍你会发现最让人揪心的不是主逻辑而是边界条件。我在这个函数上踩过的坑几乎都集中在size_t下溢、空串、以及单词扫描指针越界这三件事上。建议你在写之前先把你想到的输入列成一张表空串、纯空格、单字符、单词间一个空格、单词间多个空格、首尾带空格。把这六种情况想清楚代码基本就稳了。5. 这几个坑是这道题真正值钱的地方网上讲这道题的文章太多了但真正在工作中踩出来的坑反而很少被写进去。5.1 中文字符串不能直接整串翻转第一个大坑是编码。std::reverse是按字节翻转的对 UTF-8 编码的中文来说一个汉字往往占 3 个字节直接翻转会把字节顺序打乱输出成乱码。比如字符串“你好”UTF-8 字节是E4 BD A0 E5 A5 BD翻转后变成BD A5 E5 A0 BD E4完全不可读。如果你处理的是中文文本要么用std::wstring配合宽字符版本要么使用专门支持 UTF-8 字符边界的库。最怕的是不知道这一点测试用例全用英文上线处理中文日志才发现问题。5.2 分隔符不只是空格在实际业务里单词之间的分隔符经常是逗号、管道符、制表符甚至混合的。istringstream的operator只认空白字符这时候就不好使了。你可以改用std::getline配合字符参数逐次读取被指定分隔符切分的片段std::string segment; while (std::getline(iss, segment, ,)) { // 处理 segment }但注意getline不会自动跳过空片段所以相邻两个分隔符之间会生成空字符串你需要自行过滤。二次翻转法如果分隔符不再是空格扫描逻辑里判断s[i] ! 的地方也要对应改成其他分隔符。5.3 大文本场景下的内存差异分割法看起来只是把单词存进 vector原以为不会有太大开销但实际上一段几十 MB 的日志文本分词后的 vector 会存储大量std::string每个字符串对象本身就有对象头、堆指针等开销加上数据拷贝内存峰值很容易翻好几倍。二次翻转法则基本不产生额外大块内存只靠两个指针在原串上滑动。我建议性能敏感的场景优先二次翻转法。5.4 调试技巧打印中间状态这种带“两次翻转”的算法一旦结果不对很多人习惯盯着代码干瞪眼。我的经验是在整串翻转之后加一行打印看看第一次翻转后的字符串是否符合预期然后只对单个单词做翻转测试。把大问题拆成两个阶段定位效率高得多。比如你想确认某个诡异的用例是第一步出了问题还是第二步单词边界算错了直接在函数里临时加std::cout after whole reverse: [ s ] std::endl;看到中间状态问题往往一目了然。调试完记得删掉这行日志。5.5 多线程修改同一个字符串如果这道题跑到并发场景里比如多个线程同时翻转不同片段或者同时修改同一个字符串缓冲区一定要加锁或者保证每个线程操作独立副本。字符串翻转不是原子操作并发读写很容易把两个指针的位置搞乱。不过单机教学场景下通常不用考虑这个这里提一句只是提醒别把简单问题复杂化。6. 这套思路还能用在哪些文本处理场景学会了整串翻转和单词翻转你会发现它是一套可复用的思维模型绝不只是应付作业和面试。6.1 域名反转www.example.com想变成com.example.www可以把.当作分隔符沿用完全相同的两次翻转思路。先整个字符串翻转再以.为边界对每一段内部翻转就得到域名倒序。这个功能在分析访问日志、统计域名后缀时很实用。6.2 URL 路径重排后端处理带层级结构的路径时比如/api/v1/users需要变成/users/v1/api同样是先整串翻转再按/边界翻转每一段。规则和空格场景一模一样只是换了一个分隔符。6.3 固定格式日志的字段调换日志行往往是2025-01-01 12:00:00 [ERROR] message content这样的固定结构。如果想把消息体挪到时间戳前面可以先用分隔符把核心片段切出来再重新拼接或者如果格式足够规整用两次翻转法按空格边界处理。这种结构化的再排列本质上是“块级顺序调整”的变体。6.4 回文检测判断一个字符串是否是回文最简单的方法就是构造它的反转串然后与原始串比较。整串翻转在这里直接派上用场。如果忽略大小写和标点就先把字符串清洗一遍再翻转比较。6.5 游戏命令和脚本解析有人问“C 我的世界代码”相关的东西其实这类小游戏里的命令系统也经常需要处理参数重排。比如玩家输入/tp player x y z你想把坐标顺序变成z y x本质上就是对参数做分块翻转。实现思路和单词翻转一致按空格切块再按需求调整块顺序。遇到任何“块级顺序需要反转但块内内容不能变”的问题都可以先想三件事块的分隔符是什么需要保留原格式还是可以压缩内存是否紧张。想清楚这三点代码怎么写基本就定下来了。从我个人的经验来说这类看似简单的字符串题最考验人的不是语法而是对边界的敬畏。空串、纯空格、连续空格、中文字节、大文件内存这些才是真正决定代码能不能上生产的关卡。你可以先画一张翻转示意图把每个指针走到哪个位置标出来再动手写代码写完之后立刻补测试用例覆盖边界。这套方法在字符串处理上帮过我很多次希望你也能用上。
返回列表