
1. 先搞清楚这个疑问从哪来的标题里的问题我当年学C时也产生过一模一样的疑惑。第一次写数组逆置脑子里的想法很简单遍历整个数组把第0个和最后一个交换第1个和倒数第二个交换直到全部换完。代码写出来大概长这样void reverse(int arr[], int n) { for (int i 0; i n; i) { int temp arr[i]; arr[i] arr[n - 1 - i]; arr[n - 1 - i] temp; } }跑一遍一看数组纹丝不动就像什么都没发生过。那一刻本能地怀疑难道数组元素不能直接交换当然不是。问题出在循环范围上——我遍历了全部n个元素把每对对称位置交换了两次第一次是i从前往后走时换的第二次是i走到对称位置时又换了一次等于白忙一场。这就是不能直接交换元素值这个说法的真正来源不是换不得是你换重复了。这篇文章就把这件事彻底讲透。我会从逆置的本质、边界条件、各种写法、排查技巧一路展开最后附上我实际写过的一段可复现代码。内容不要求你有多少C基础只要知道数组是什么、for循环怎么写就能跟上。看完你不仅可以秒写逆置还能把背后的索引规律和边界思维用到其他算法题里。2. 逆置的本质与整体设计思路2.1 逆置到底在做什么数组逆置也叫反转、翻转就是把数组里的元素顺序完全倒过来。比如{1,2,3,4,5}变成{5,4,3,2,1}{1,2,3,4}变成{4,3,2,1}。这里有个隐藏规律逆置是成对进行的。无论数组多长最终每个位置上的元素都来自它的对称位置。数学上如果数组长度为n那么索引i的对称位置就是n - 1 - i。校验一下n5时0的对称位置是41的对称位置是32的对称位置是2——中间那个元素跟自己对称所以它不需要动。生活里也有类似的场景一横排人面对面站好老师喊向后转最左边和最右边的人同时转身换位第二左和第二右换位一直换到中间为止。注意这个行为里的两个关键动作第一两个人是一起交换的第二一旦换过就不能再换第二次。如果老师喊了两次交换第一排和最后一排大家又站回原位了。数组逆置失败的原因往往就是多喊了那两次。2.2 为什么遍历整个数组交换必然失败回到开头的错误代码。设n5i从0到4i0交换 arr[0] 和 arr[4]数组变成{5,2,3,4,1}i1交换 arr[1] 和 arr[3]数组变成{5,4,3,2,1}i2交换 arr[2] 和 arr[2]自己换自己数组不变i3交换 arr[3] 和 arr[1]数组又变回{5,2,3,4,1}i4交换 arr[4] 和 arr[0]数组完全回到{1,2,3,4,5}关键在于i3和i4时又碰了之前已经换过的位置。每一对对称元素被完整地分两次处理每次动作方向相反、相互抵消。如果是偶数长度比如n4i3时交换arr[3]和arr[0]同样是把i0时换过的又换回来。所以直接交换元素值这个动作本身没有任何问题问题出在循环次数和范围上。我们需要让每一对只被处理一次这就自然引出了双指针方案。2.3 双指针法为什么是标准答案双指针法是解决这类问题最直观的方案思路是维护两个游标left从数组头部开始right从尾部开始只要left right就交换这两个位置上的元素然后 left 向右移动一格right 向左移动一格。直到两个指针相遇或者交错循环结束。用代码表示void reverse(int arr[], int n) { int left 0, right n - 1; while (left right) { int temp arr[left]; arr[left] arr[right]; arr[right] temp; left; --right; } }这个方案能成功是因为它的循环条件left right精确保证了每一对对称元素只被交换一次。对于奇数长度left和right最终会指向同一个中间元素此时left right为假循环自然退出中间元素不会被碰。对于偶数长度left和right会在越过中点时交错同样终止。还有一种等价写法for (int i 0; i n / 2; i)。它的意思是只处理前半段的元素让它和后半段对称位置交换。n5时n/22处理i0和i1正好覆盖了除中间元素外的所有位置。两种写法本质上控制的是同一个边界只是视角不同一个用双指针从两端逼近一个用数学下标精确定位前半段。我个人更推荐双指针写法因为它对边界条件的表达更加形象不容易算出错误的对称索引。3. 核心细节拆分交换操作与边界条件3.1 交换的三个动作为什么缺一不可交换两个变量的值是C里最基本的操作之一标准三连int temp arr[left]; arr[left] arr[right]; arr[right] temp;第一行用临时变量保存arr[left]的原值第二行让arr[left]接收arr[right]的值第三行再把临时变量里保存的值给arr[right]。如果少了临时变量直接写arr[left] arr[right];那么arr[left]的原始值就被覆盖丢失了第三行根本没有可用的来源数据。这就好比两个人交换座位如果一个人先坐过去不给他原来的位置占住另一个人就不知道该坐哪里。在实际项目里我见过有人用加减法或者异或运算来炫技交换比如a a b; b a - b; a a - b;这种做法虽然能省一个变量但可读性差极端情况下整数还可能溢出虽然C无符号整数溢出是良定义的但有符号溢出就是未定义行为。除非是在性能极度敏感且明确知道溢出不影响的场景否则一律用临时变量。C标准库也提供了std::swap它内部帮你处理了这些细节后面我会单独讲。3.2 边界条件两种写法怎么选写逆置最容易翻车的地方就是边界条件我分三种情况仔细展开。第一种情况双指针循环条件left right这是我最推荐的写法。它不关心数组长度是奇数还是偶数也不需要做n / 2的整数除法运算。循环每执行一次left和right各向中间移动一步直到相遇或交错。有个我自己踩过的坑第一次写双指针时循环变量忘了让 right 自减只写了left结果left快到数组末尾时和后面的right发生交错循环条件变成right left早就跳出循环后半段元素全没有处理。所以双指针的关键不光是循环条件的写法还有每一轮循环后两个指针都必须移动。第二种情况下标循环for (int i 0; i n / 2; i)这个写法把对称关系直接表达成arr[i]和arr[n - 1 - i]的交换。优点是不需要额外的变量缺点是容易在n / 2的边界上算错。比如把条件误写成i n / 2那么n5时i会取0、1、2i2恰好是中间元素自己换自己不影响结果但n6时i取0、1、2、3i3的对称索引是2这等于把i2那组对换又重复处理了一次奇数长度的代码白写一半。第三种情况不知道数组长度如果拿到的不是数组而是指针或者容器只有begin()和end()那直接使用标准库的std::reverse是最省事的方案后面会专门讲。3.3 对称索引的规律所有对称位置的和等于 n-1这是一个非常实用的小技巧我能记一辈子arr[i]的对称位置一定是arr[n - 1 - i]。所有对称位置的索引之和都等于n - 1。你可以拿任意数组验证数组长度 n索引对 (i, n-1-i)索引和5(0,4), (1,3), (2,2)4, 4, 46(0,5), (1,4), (2,3)5, 5, 58(0,7), (1,6), (2,5), (3,4)7, 7, 7, 7调试逆置代码时如果怀疑对称索引算错了最快的检查方法就是打印所有待交换的索引对验证它们的和是不是等于n - 1。这个规律在字符串反转、矩阵转置、回文判断等一系列问题里都会反复出现值得专门记住。4. 实操从零写一个可靠的逆置函数4.1 基于C风格数组的实现先来最传统、最贴近底层的写法。注意C风格数组作为函数参数时会退化为指针所以必须在参数里显式传入长度n否则函数内部拿不到数组边界信息。#include iostream void reverseArray(int arr[], int n) { int left 0; int right n - 1; while (left right) { // 三行经典交换 int temp arr[left]; arr[left] arr[right]; arr[right] temp; left; --right; } } int main() { int data[] {1, 2, 3, 4, 5}; int n sizeof(data) / sizeof(data[0]); reverseArray(data, n); for (int i 0; i n; i) { std::cout data[i] ; } std::cout std::endl; return 0; }这里有个细节值得单独说sizeof(data) / sizeof(data[0])是在 main 函数里计算数组长度。一旦数组被传入函数sizeof(arr)返回的是指针大小通常是8字节不再是整个数组的大小。所以千万别在函数内部写int n sizeof(arr) / sizeof(arr[0])这是C/C初学者最常见的第一个坑我一共见过它至少十几次。运行上面代码输出是5 4 3 2 14.2 基于 std::vector 的安全写法C风格数组要手动传长度忘了传或者传错就出问题。更好的做法是使用std::vector它自带size()而且可以按引用传递函数内修改直接反映到外部#include iostream #include vector void reverseVector(std::vectorint vec) { int left 0; int right static_castint(vec.size()) - 1; while (left right) { // 可以用标准库的 swap也可以手写三行 int temp vec[left]; vec[left] vec[right]; vec[right] temp; left; --right; } } int main() { std::vectorint data {1, 2, 3, 4, 5, 6}; reverseVector(data); for (int num : data) { std::cout num ; } std::cout std::endl; return 0; }输出6 5 4 3 2 1注意我把vec.size()用static_castint转成了 int因为size()返回的是size_t无符号类型如果直接int right vec.size() - 1在vec为空时size() - 1会变成巨大的正整数导致越界访问。这是无符号类型的经典陷阱在LeetCode和实际项目里都坑过不少人。解决方式要么转换成有符号类型要么写int right static_castint(vec.size() - 1);。最稳妥的方法是单独处理空容器if (vec.empty()) { return; }4.3 一行代码搞定std::reverse如果你不是在练习基本功而是在实际工程里需要逆置数组最优雅的方案是直接调用标准库算法std::reverse。它在头文件algorithm中接受一对迭代器#include iostream #include vector #include algorithm int main() { std::vectorint data {9, 8, 7, 6, 5}; std::reverse(data.begin(), data.end()); for (int num : data) { std::cout num ; } std::cout std::endl; // C风格数组也可以用 std::begin / std::end int arr[] {1, 3, 5, 7, 9}; std::reverse(std::begin(arr), std::end(arr)); for (int n : arr) { std::cout n ; } std::cout std::endl; return 0; }std::reverse内部也是用双指针思路实现的效率是O(n)空间O(1)。我个人的建议是初学阶段务必手写一遍逆置理解边界和交换的原理等工作后用std::reverse就够了没必要重复造轮子。这两件事不矛盾前者是理解后者是效率。4.4 测试用例清单如何证明你的逆置是对的写任何算法函数都要建立一套测试用例来验证。逆置函数至少应该测这几种情况测试输入期望输出验证要点{}空数组{}不能崩溃不能越界{1}单元素{1}中间元素不处理{1,2}双元素{2,1}最基本的成对交换{1,2,3}奇数长度{3,2,1}中间元素保持原位{1,2,3,4,5,6}偶数长度{6,5,4,3,2,1}所有元素都成对交换{1,1,1,1}重复元素{1,1,1,1}重复值下交换是否安全尤其是空数组和单元素数组最容易漏。很多初版代码在n 1时没问题但n 0时会因为n - 1变成负数而出问题。双指针写法里n 0时right -1此时left right是0 -1为 false循环根本不进反而天然安全。如果是下标写法for (int i 0; i 0 / 2; i)也不会执行同样安全。但如果你在函数里先计算n - 1做了别的事情就要格外小心。5. 常见问题与排查技巧实录5.1 高频错误速查表我把平时指导别人排查逆置问题时遇到的典型错误整理成一张表方便你对照检查现象常见原因解决办法数组完全没变化循环遍历了全部元素交换了两次只遍历前半段或改用left right只反转了前半段后半段乱掉双指针只有 left 移动right 没递减每轮同时left和--right程序崩溃/访问越界未处理空数组或 n0right 为 -1增加判空或确保边界条件在循环前安全结果是对的但顺序很奇怪对称索引写错比如用了n - i而不是n - 1 - i用索引和等于 n-1规则验证swap之后中间结果被覆盖忘记使用临时变量直接ab补上临时变量或改用std::swap函数改了数组但外部没生效C风格数组传参时正确定义长度或者错误地按值传入了 vector数组天然按指针传vector 需要引用5.2 排查思路从现象倒推根因实际调试时我一般按三段倒推法来查第一段确认有没有交换。在关键位置加打印语句输出每次交换前后的数组状态看元素到底有没有移动。如果一轮循环下来数组完全没变八成是交换了两次而不仅仅是没交换。第二段确认交换的是不是正确位置。打印出每次交换的索引对比如left0, right4然后手动心算left right是否等于n - 1。如果不等于说明索引计算有问题。最常见的错误是把n - 1 - i写成n - i差了恰好一位结果就会出现某几个边界元素不动的诡异现象。第三段确认循环是否提前终止。如果发现只处理了一部分元素检查循环变量是否在每轮都正确更新。用while (left right)时最容易忘的是操作完没更新造成死循环或者提前退出。5.3 我踩过的一个真实坑有一年我在给某个模拟项目写图像数据预处理需要把像素数组的行顺序翻转也就是上下反转。当年我刚从数组逆置转过思路心想这还不简单吗直接对每一行调用std::reverse就是了。测出来发现图像不但上下没变反而整体错位了。排查很久才发现问题不是逆置函数本身而是图像数据的存储格式是行优先的我错误地把行号数组当成了行指针数组reverse 之后每一行内部的像素顺序也被反转了。这事给我的教训是逆置数组的思路用在任何线性序列上都行但必须搞清楚你操作的对象是数据本身还是数据的地址。在C里数组元素可能是int、是结构体、是指针元素值交换的逻辑都一样但语义完全不同。万一哪天你在业务代码里看到数组逆置后结果不对先别怀疑交换逻辑检查一下数组里存的是什么类型的数据。5.4 新的需求逆置的常见变体理解了数组逆置之后很多看似不同的题目其实都是它的变体。比如逆置字符串本质上就是字符数组的逆置std::reverse直接支持。逆置单链表不能随机访问尾节点需要三指针就地反转思路类似但手法不同。原地移动数组中的一部分比如把前k个元素移到末尾可以通过三次局部逆置组合完成这在某些题目里叫旋转数组。判断回文把数组逆置后再逐位比较或者用双指针从两端同步向中间比较。掌握双指针从两端向中间移动这个核心模板后这些变体都能快速理解因为它们的共同底层逻辑都是对序列端点进行操作并逐步收窄范围。6. 最后再分享一个小技巧回到开头的问题数组逆置到底能不能直接交换元素值能但前提是你得保证每一对元素只交换一次。任何让你产生不能交换错觉的代码都值得把所有交换的索引对逐轮打印出来看看。我个人在实际调试这类小程序时的习惯是先写一个print辅助函数每次交换后打印当前数组然后手动跟着演算一遍。一旦看到某个索引对重复出现就说明循环边界多了半轮。这个方法看似笨却比考眼力debug快得多。如果你想把逆置练到肌肉记忆推荐一个自测题目随机生成长度为 0 到 100 之间的数组反复逆置再逆置每次都检查最终是否回到原数组。这个操作本质上是逆置两次等于恒等变换也是逆置运算最重要的性质之一。跑通这个测试你才算真正把边界条件吃透了。数组逆置看起来简单但它牵涉到的变量交换、无符号整数陷阱、边界控制、指针退化每一个都是日常开发里高频出现的问题。把这个基础点打牢后面再面对更复杂的算法题你会感觉地基稳得多。