
最近总有朋友问我“反转字符串这种题有什么好说的直接reverse一下不就行了”确实如果只是要一个结果C 里一行std::reverse就能搞定。但真正让你写这道题的时候考官想看的往往不是你会不会调库而是你有没有双指针的基本功。字符串反转看似简单背后却牵扯到原地修改、索引边界、停止条件这些非常基础但又容易翻车的细节尤其当你想用 C 把反转后的结果正确打印出来里面还有不少坑。这篇文章我会把“双指针法反转字符串”这件事拆开揉碎从为什么用双指针到手写 C 实现的每一步再到反转之后怎么打印、有哪些常见问题最后还会聊一下双指针思路在字符串其他题目里的延伸。不管你是刚学算法的同学还是写了好几年业务代码、偶尔刷题找回手感的老手这波内容应该都够你用。1. 双指针法到底在解决什么问题1.1 从“交换两个字符”到“左右夹逼”反转字符串的本质就是把第一个字符和最后一个字符交换第二个和倒数第二个交换一直往中间走。这个过程用两个下标来描述非常直观一个从左边出发叫left一个从右边出发叫right。每交换一次left往右走一步right往左走一步直到两个指针相遇或者交错。写成伪代码就是left 0 right n - 1 while(left right): swap(s[left], s[right]) left right--这里很多人会纠结为什么停止条件是left right而不是left right。你可以自己拿ab和abc试一下长度为奇数时中间那个字符不需要跟谁交换长度为偶数时两个指针会在中间“擦肩而过”。如果写成到中间那一步两个指针指向同一个字符时还会再交换一次结果等于没有交换白白多执行一轮。所以left right才是最干净的停止条件。1.2 为什么是双指针而不是栈或者直接调 reverse有的同学说反转字符串我可以用栈把所有字符压进栈再弹出来自然就反过来了。这种方法逻辑上没错但空间复杂度是 O(n)需要额外开一个跟原字符串一样大的存储空间。双指针法只需要两个临时变量或者连临时变量都不用直接用std::swap()空间复杂度是 O(1)是标准的原地算法。再说std::reverse。工程上我当然推荐直接用库函数但在算法学习阶段手写双指针的意义在于让你真正理解“索引移动”和“边界控制”。很多人在写二分查找、快排这类双指针相关的代码时边界总出错其实就是因为当初没有把这种左右夹逼的套路吃透。面试时如果题目明确写了“原地修改、不使用额外空间”你还能想起用双指针这就是你的竞争优势。1.3 时间复杂度与工程意义双指针反转的时间复杂度很好算每轮交换两个字符总共执行 n/2 次交换所以是 O(n)。对比压栈弹栈的方式栈操作还要多一次内存分配和一次遍历常数项明显更大。而且双指针访问的是数组的两个端点内存局部性好对 CPU 缓存非常友好。我在实际项目里需要手动处理一些大型文本缓冲区时也喜欢用双指针而不是重新分配一个新字符串一个重要的原因就是节省内存、减少拷贝。2. C 中的双指针实现与打印输出2.1 基础实现原地反转字符数组LeetCode 上经典的 344 题给的是vectorchar很多 C 版本的题解都默认在这个接口上写。其实用vectorchar还是string都无所谓算法逻辑完全一样只是类型不同。下面我写一个兼容性最好的版本#include vector #include algorithm // for std::swap void reverseString(std::vectorchar s) { if (s.size() 1) 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如果你直接写成int right s.size() - 1;当字符串为空时s.size() - 1会得到一个非常大的无符号数再赋给int甚至可能会变成-1虽然赋值结果不一定是灾难但这种隐式类型转换本身就是不定时炸弹。所以要么提前判空要么像我一样用static_castint强制转换。面试时你把这个细节写出来绝对加分。2.2 打印反转结果直接打印 vs 存储后打印热搜词里有“字符串反转怎么打印出来 c”我猜很多人的困惑在于反转到底做了什么、结果该怎么输出。这里我总结三种最常见的打印姿势。第一种原地反转后用循环遍历输出for (char c : s) { std::cout c; } std::cout std::endl;这种适合只关心终端输出、不需要保留反转结果的情况。第二种反转一个std::string对象然后直接std::cout str;#include iostream #include string #include algorithm int main() { std::string s hello; std::reverse(s.begin(), s.end()); std::cout s std::endl; return 0; }第三种比较取巧用反向迭代器直接构造一个新的字符串再输出原字符串保持不变std::string s hello; std::string reversed(s.rbegin(), s.rend()); std::cout reversed std::endl;面试或者写算法作业时我更推荐第一种或者第二种因为能体现你对字符串容器的理解。第三种更适合在业务代码里快速实现“只读反转输出”。2.3 完整可运行示例从输入到打印既然大家关心完整的 C 程序我直接给一个可以随便跑的例子。注意用getline而不是cin 否则遇到空格就会被截断。#include iostream #include string using namespace std; int main() { string s; cout 请输入一个字符串: ; getline(cin, s); int left 0; int right static_castint(s.size()) - 1; while (left right) { swap(s[left], s[right]); left; --right; } cout 反转后的字符串: s endl; return 0; }运行效果如下请输入一个字符串: hello world 反转后的字符串: dlrow olleh如果你把getline(cin, s)换成cin s再输入hello world你会发现输出只有olleh因为cin 在空格处就停住了。这个点虽然基础但我见过不少初学者在这里懵了半天。2.4 边界条件空串、单字符、奇数长度与偶数长度写反转函数的时候边界条件是最容易出问题的地方。我整理了一个速查表大家可以对照着看输入情况预期结果双指针执行过程注意点空字符串空字符串while 不执行必须先判空否则size()-1溢出单字符aaleft0, right0循环不执行直接返回即可奇数长度abccba第一轮交换 a/cleft1, right1循环结束中间字符 b 保持原位正确偶数长度abcddcba第一轮交换 a/dleft1, right2第二轮交换 b/cleft2, right1循环结束指针错过后自然停止你能发现只要停止条件写对所有边界情况都能被优雅处理。我见过有人为了处理奇数长度单独写 if 分支完全没有必要while (left right)已经把问题解决了。3. 实操中的细节与常见坑3.1 中文与多字节字符问题如果你只是在做 ASCII 字符串反转上面的代码都没毛病。但遇到中文问题就来了。C 的std::string本质上是一个字节容器而 UTF-8 编码下的一个汉字占 3 个字节。直接按char交换会把一个汉字的字节顺序打乱打印出来全是乱码。比如你好在 UTF-8 下是 6 个字节双指针交换字节后可能变成??这种不可读的字符。如果你要支持中文反转最稳妥的办法是改用宽字符#include iostream #include string using namespace std; int main() { wstring s L你好世界; int left 0; int right static_castint(s.size()) - 1; while (left right) { swap(s[left], s[right]); left; --right; } wcout s endl; return 0; }不过在 LeetCode 这种在线评测平台上输入都是 ASCII 字符所以大家刷题时不用担心这个问题。但做实际业务时我强烈建议先确认字符编码别把含有中文的字符串直接交给简单的双指针反转函数否则上线后就是事故。3.2 std::reverse 和手写双指针的区别很多人会问既然std::reverse已经封装好了为什么还要手写我的看法是分场景。如果是公司里的业务代码我会直接写std::reverse(s.begin(), s.end())因为它经过标准库优化可读性也高同事一看就懂。但如果是在学习算法、准备面试或者需要在循环过程中检查中间状态手写双指针才更有价值。你可以随时在循环里加一行代码把每一轮的交换结果打印出来方便排查问题。另外std::reverse内部也是用两个迭代器向中间移动并交换元素本质上和双指针是一样的。手写双指针相当于把库函数的底层实现重新走了一遍写完你会对迭代器、指针、索引有一个更直观的认识。3.3 打印陷阱cout 输出 vectorchar 或 char 数组这里要单独提一个 C 特有的坑很多人反转完vectorchar之后想用cout s.data()或者cout s[0]来打印结果出现乱码或者越界。原因是vectorchar并不保证以\0结尾虽然大多数实现会预留位置但直接当 C 风格字符串输出是完全不安全的——它会一直往后读到内存中的第一个 0 为止这是未定义行为。正确做法是用循环遍历输出或者先把vectorchar转成std::string再输出std::string str(s.begin(), s.end()); std::cout str std::endl;如果你用的是char[]比如char s[] hello;反转时只动前 5 个字符第 6 个\0还在原位置用printf(%s, s)没问题。但如果你自己手动在中间处理时不小心把\0移动了打印就会出问题。记住C 风格字符串的结尾标志是\0任何逐个字符的操作都要保证它不被破坏。3.4 面试和考试里的标准写法注意点我在看别人写的代码时发现几个高频错误这里统一说一下第一循环条件不要写成while (left ! right)。如果是偶数长度left 会直接超过 right循环永远不结束或者发生越界。必须用left right。第二左右下标类型尽量保持一致。left定义为intright也定义为int不要一个int一个size_t混着用。混用比较时会发生隐式转换轻则警告重则逻辑错误。第三如果面试官要求你手写交换过程不要只写一句swap就完事。可以先展示这样的写法然后再告诉他工程上可以直接用std::swap简化char tmp s[left]; s[left] s[right]; s[right] tmp;这能体现出你知道交换的底层原理而不是只会调用现成函数。4. 双指针思路的实战延伸4.1 反转字符串中的单词顺序双指针的价值不只是反转整个字符串它还能解决一类更复杂的问题反转句子中的单词顺序但要求单词内部的字母顺序不变。比如输入hello world c期望输出c world hello。解法很经典分两步先整体反转整个字符串得到c dlrow olleh然后再对每一个单词内部的字符做一次反转得到c world hello。实现起来双指针依然是好帮手void reverseSub(string s, int left, int right) { while (left right) { swap(s[left], s[right]); left; --right; } } string reverseWords(string s) { int n s.size(); reverseSub(s, 0, n - 1); int start 0; for (int end 0; end n; end) { if (end n || s[end] ) { reverseSub(s, start, end - 1); start end 1; } } return s; }这个代码片段几乎就是双指针的两个用法一个用于整段反转一个用于分段反转。如果你能把这一道题吃透双指针的核心就掌握得差不多了。4.2 双指针还能解决哪些字符串问题除了反转双指针在字符串算法里可以说是“万金油”。最常见的还有验证回文串左右指针同时往中间走逐个比较字符遇到非字母数字就跳过。反转字符串 II每隔 k 个字符反转前 k 个字符本质是双指针的循环版本。最长回文子串中心扩展法从一个中心向两边扩展也是双指针思想的一种变体。这些题目比我之前想象的要容易得多因为你只要记住一个套路一个指针从起点出发另一个指针从终点出发或者一个快指针一个慢指针通过控制两者的移动条件来解决问题。刷题时可以把它们归类到一起总结效率会高很多。4.3 性能实测与优化心得有人问双指针反转还能不能更快对于普通随机访问容器双指针已经是最优解了因为每个元素只访问一次总共 n/2 次交换复杂度上不可能再低了。但在工程里还有一些可以优化的点如果字符串特别大并且你只需要输出反转结果可以考虑直接用反向迭代器 cout避免额外空间和交换的开销。如果你在写嵌入式或者性能敏感的代码尽量少用std::swap改成位运算交换或者临时变量交换虽然编译器大概率会优化但手动控制有时候更安心。不过这个属于微观优化一般情况下不建议做。如果字符串存储在只读内存段比如const char*你不能原地修改指针指向的内容必须复制到可写缓冲区再反转。这个坑在真实项目中很常见很多段错误就是这么来的。5. 写在最后我的实操心得做了这么多年开发反转字符串我不敢说写过几百次但几十次肯定是有的。我最深的体会是这个题目难点从来不在“反转”本身而在“怎么把结果正确打印出来”。每一次换容器、换编码、换输出方式都可能出现新的问题。后来我养成了一个习惯拿到一个字符串操作需求先确认数据到底是什么类型、是否包含多字节字符、用什么方式输出再动手写算法。顺序反了后面全是坑。另外一个心得是双指针的核心不是“指针”这两个字而是“两个方向相反的下标控制权”。你可以把它想象成两个人在数组两端面对面走直到相遇。只要理解了这种左右夹逼的节奏反转字符串、判断回文、反转单词顺序全都是一套思路。最后分享一个小技巧调试双指针代码的时候别干瞪眼脑内执行。在循环里临时加一行代码把每一轮开始前的 left、right 和当前字符串打印出来比什么 debugger 都直观。等确认没问题了再把这行代码删掉。我用这个法子帮同事排查过好几回边界问题十分钟之内必出结果。