数组极值交换这种题我第一次看到的时候觉得这就是个送分题找出最小值和最大值把最小值放到数组开头最大值放到数组末尾完事。结果真上手一写提交了三版才通过。后来我在模拟面试里也见过不少人在这道题上翻车而且翻车的方式几乎一模一样。这道题之所以值得单独拿出来讲是因为它并不是单纯考你会不会遍历数组而是把数组操作里最容易踩的三个坑一次性集中到了你面前极值的查找、下标的记录、交换后下标失效。尤其是最后一个坑很多写了好几年代码的人都会一时反应不过来。这篇文章我会从头到尾把我的实现过程、翻车现场、修正思路、边界处理都拆开来讲适合刚开始学数组的新手也适合准备基础算法面试的人还可以直接拿去当教学案例。1. 题干一句话背后三件事极值交换到底在考什么1.1 看起来只有“交换”实际要做的是三步题目通常长这样给定一个数组把数组中的最小值放到第一个位置最大值放到最后一个位置其余元素保持相对顺序不变。要求尽量高效地完成。光是把最小值放到第一个位置这句话就隐含了两个操作找出最小值是哪个、它现在在哪个下标把下标0的元素和它交换。最大值同理。所以完整拆开是三件事确定最小值以及它所在的下标确定最大值以及它所在的下标做两次交换把最小值换到arr[0]把最大值换到arr[n-1]。听上去平平无奇但先找出两个极值的下标再依次交换这个顺序里藏着一个非常隐蔽的陷阱第二次交换使用的下标可能已经被第一次交换破坏掉了。也就是说你最开始记住的max_idx在执行完第一步交换之后指向的不再是最大值了。很多人在这一步栽跟头不是因为不会写循环而是因为脑子里把数组当成了一张静态的纸忘了它其实是动态变化的。1.2 这道题为什么被当成算法启蒙题我认识一位算法老师特别喜欢拿这道题当课堂前十分钟的小测验。他的理由是这道题能把遍历、比较、记录状态、交换、边界判断这五样基本功一次性全考到而且代码量又短适合当场看人写。从数据结构的角度看数组本身就是最基础的数据容器所有更复杂的算法最终都要落到在数组里找东西、改东西。极值交换正好把找和改结合在了一起。如果你能一次性把这道题写对说明你对数组下标的敏感度是过关的。从面试的角度看这道题也是很好的压力测试题。面试官可以在你写完基础版本之后立刻追问几个变体如果最大值和最小值各重复出现怎么办如果数组长度为1怎么办如果交换后发现下标失效怎么办一环扣一环能快速判断你是背了答案还是真的理解了原理。我自己也是在这道题上吃了一次亏之后才开始养成一个习惯凡是要对数组做先记录下标、再操作的逻辑我一定会在心里多问一句——这次操作会不会让之前记录的下标变得无效这个习惯后面帮我避了不少坑。2. 我第一次实现的翻车现场先换最小值再找最大值2.1 一个四元数组如何被两次交换打回原形先看一个典型错误版本的伪代码思路min_idx 查找最小值的下标 max_idx 查找最大值的下标 交换 arr[0] 和 arr[min_idx] 交换 arr[n-1] 和 arr[max_idx]我当时就是这么写的逻辑看起来完全没问题先把最小值放到开头再把最大值放到结尾各干各的互不影响。但实际跑起来问题立刻就出来了。用一个最简单的四元数组来复现现场[9, 2, 3, 1]。手动走一遍最小值是1在index 3最大值是9在index 0。执行第一步交换arr[0]和arr[3]数组变成了[1, 2, 3, 9]。到这一步一切都很正常最小值确实去了开头。问题出在第二步。此时max_idx还记着0但这个位置上的值已经从9变成了1。真正的最大值9现在在index 3也就是数组末尾。执行交换arr[n-1]和arr[max_idx]实际交换的是arr[3]和arr[0]也就是把9和1又换回去了。数组变成了[9, 2, 3, 1]。对绕了一圈完全打回原形。我第一次跑出这个结果的时候还以为是编译器的问题。后来在纸上手动推演了一遍才意识到我的max_idx在第一步交换之后就成了一个过期下标。2.2 第二个常见翻车点极值下标被覆盖上面那种写法是先记录两个下标再依次交换。还有另一种写法更容易让人困惑先交换最小值再重新查找最大值。交换 arr[0] 和 arr[min_idx] max_idx 重新查找最大值的下标 交换 arr[n-1] 和 arr[max_idx]这种写法在部分情况下能碰巧蒙对但遇到最大值原本就在开头的数组一样会翻车。原因还是同一个第一步交换之后最大值已经不在原来的位置上了你在交换后的数组中重新找最大值找到的位置和最初的值已经对不上了。比如数组[9, 2, 3, 1]第一步后变成[1, 2, 3, 9]重新查找最大值的下标是3交换arr[3]和arr[3]看起来没出错。但换个例子[9, 5, 1, 3]第一步交换后得到[1, 5, 9, 3]重新找最大值下标是2交换arr[3]和arr[2]得到[1, 5, 3, 9]结果刚好正确。这两种写法让人很迷惑因为有些用例能通过有些不能属实战里最让人头痛的偶现Bug。真正的根因在于你无法保证第一步交换不影响最大值的位置除非提前做好判断。2.3 记住这个规律先操作后更新踩过这次坑之后我给自己总结了一条规律只要对数组做了修改操作所有在此之前记录的下标都必须重新审视一遍是否仍然有效。这个规律不只在极值交换里适用在数组删除、插入、排序等几乎所有场景下都成立。写成代码习惯就是涉及多个下标变量的操作要么一次性算好所有的目标位置要么在每次操作后立刻修正受影响的变量。在极值交换这道题里最经典的处理方式就是先记录min_idx和max_idx交换最小值到开头后立刻检查最大值是否因为这次交换而换了位置。如果max_idx 0说明最大值原本在开头被交换到了原来最小值的位置这时候就要把max_idx更新为min_idx。这就是下一节要讲的正确实现。3. 正确实现一次遍历定位极值解决下标失效3.1 C 语言版本与指针交换正确的思路其实不复杂先通过一次遍历同时找到最小值和最大值的下标然后做两次交换在两次交换之间插入下标修正逻辑。以 C 语言为例完整实现如下#include stdio.h void swap_extremes(int arr[], int n) { if (n 1) return; int min_idx 0, max_idx 0; for (int i 1; i n; i) { if (arr[i] arr[min_idx]) min_idx i; if (arr[i] arr[max_idx]) max_idx i; } // 先把最小值换到开头 int tmp arr[0]; arr[0] arr[min_idx]; arr[min_idx] tmp; // 如果最大值原本就在开头它已经被换到 min_idx 了 if (max_idx 0) { max_idx min_idx; } // 再把最大值换到末尾 tmp arr[n - 1]; arr[n - 1] arr[max_idx]; arr[max_idx] tmp; } int main() { int arr[] {9, 2, 3, 1}; int n 4; swap_extremes(arr, n); for (int i 0; i n; i) { printf(%d , arr[i]); } return 0; }运行结果是1 2 3 9符合预期。用之前翻车的例子再走一遍min_idx 3max_idx 0。第一步交换后数组变成[1, 2, 3, 9]此时发现max_idx 0将其更新为min_idx也就是3。第二步交换arr[3]和arr[3]也就是原地不动最终结果正确。3.2 Python 与 JavaScript 的写法对比同样的逻辑用 Python 写代码更加简洁def swap_extremes(arr): n len(arr) if n 1: return arr min_idx max_idx 0 for i in range(1, n): if arr[i] arr[min_idx]: min_idx i if arr[i] arr[max_idx]: max_idx i arr[0], arr[min_idx] arr[min_idx], arr[0] if max_idx 0: max_idx min_idx arr[-1], arr[max_idx] arr[max_idx], arr[-1] return arrPython 的元组解包交换写起来很自然但要注意arr[0], arr[min_idx] arr[min_idx], arr[0]这一行会同时读取两边的旧值再赋值不会出现先覆盖再丢失的问题。JavaScript 用解构赋值也能达到同样的效果function swapExtremes(arr) { const n arr.length; if (n 1) return arr; let minIdx 0; let maxIdx 0; for (let i 1; i n; i) { if (arr[i] arr[minIdx]) minIdx i; if (arr[i] arr[maxIdx]) maxIdx i; } [arr[0], arr[minIdx]] [arr[minIdx], arr[0]]; if (maxIdx 0) maxIdx minIdx; [arr[n - 1], arr[maxIdx]] [arr[maxIdx], arr[n - 1]]; return arr; }三个语言的核心逻辑完全一致差异只在交换语法上。C 语言用临时变量Python 和 JavaScript 用解构但底层都是三步交换的封装。理解原理的人切语言只是换层皮。3.3 为什么必须判断 max_idx 0关键就在这行判断。它是整个陷阱的出口也是最容易被忽略的一行代码。我的理解是第一步交换只涉及两个位置0和min_idx。被影响到的下标只有这两个。那么最大值原本的位置如果是其他任意位置它都不会受到这次交换的牵连max_idx自然不需要更新。唯一特殊的情况就是max_idx 0因为下标0是这次交换的参与者之一。当最大值原本在开头第一步交换会把它挤到原来最小值所在的位置也就是min_idx。所以必须把max_idx改成min_idx。如果你反过来写先交换最大值到末尾再处理最小值那么你需要判断的就是min_idx n - 1。两条路逻辑完全对称选择哪条都可以但要保持思路一致别写着写着混了。我个人习惯先处理最小值因为数组开头下标0的语义更直观判断起来不容易错。4. 边界条件与重复极值最容易扣分的地方4.1 数组长度 1 和 2 时的特殊表现边界值用得好不好是区分会写和写得好的分水岭。这道题最典型的两个边界是数组长度为1和长度为2。长度为1时数组中只有一个元素它既是最大值也是最小值交换没有任何意义。如果代码里不加判断直接进入交换逻辑你会发现自己跟自己交换虽然不会报错但属于无效操作。稳妥做法是在函数开头加一行if (n 1) return;既清晰又省事。长度为2时情况比较有意思。两个元素一个是最大值一个是最小值交换后刚好整体有序。比如[2, 1]会变成[1, 2][1, 2]会变成[1, 2]。用前面写的正确代码跑一遍会发现min_idx和max_idx分别指向两个位置先交换最小值到开头再判断max_idx 0是否成立最后交换最大值到末尾。整个流程依然正确。这里顺带提醒一句写边界处理时要区分不加判断也不报错和逻辑正确的区别。数组长度1时不做交换虽然结果一样但你的意图不明确别人读代码时还得猜测你是故意为之还是忘了处理。明确的if判断是最好的注释。4.2 多个最大值或多个最小值下标更新策略数组中可能出现多个相同的极值。这时候题目如果没有特别说明不同的人会有不同的约定取第一个出现的极值还是取最后一个出现的极值这个约定会影响查找代码的写法。在前面的实现里我用的是严格小于来更新min_idx也就是说我取的是第一个最小值最大值使用的是严格大于取的是第一个最大值。如果你希望取最后一个相同极值把和改成和即可。这道题本身通常不考这个细节但面试官可能会追问所以自己要心里有数。如果题目没有明确说明我建议在代码注释里写明取第一个最小值和第一个最大值避免歧义。在真实的工程代码里这种模糊的业务语义往往会造成线上问题养成明确约定的习惯很重要。4.3 一组可以直接跑通的测试用例这里整理一组测试用例方便你验证自己的实现是否正确。以取第一个最小值、第一个最大值为约定输入数组期望输出说明[9, 2, 3, 1][1, 2, 3, 9]极值在两头最容易触发下标失效的用例[1, 2, 3][1, 2, 3]极值本身就在目标位置交换后不变[2, 1][1, 2]长度为2的有序化场景[1][1]长度为1的边界[5, 4, 3, 2, 1][1, 4, 3, 2, 5]逆序数组[3, 1, 4, 1, 5][1, 3, 4, 1, 5]存在重复最小值1取第一个[7, 5, 6, 5, 8][5, 7, 6, 5, 8]存在重复最大值8和重复最小值5你会发现第三行[3, 1, 4, 1, 5]的结果是[1, 3, 4, 1, 5]而不是[1, 1, 4, 3, 5]因为约定取的是第一个最小值。如果你的实现取了最后一个最小值得到的是后者。两种结果都可能被题目接受但你必须知道自己在做哪种约定。5. 从单次交换到选择排序难度升级与真实应用5.1 把“找最小放前面”循环起来就是选择排序极值交换做一次是把整个数组的最小值和最大值放到两端。如果只保留找最小值放前面这个动作并且不再固定从头开始而是从i开始查找arr[i...n-1]区间内的最小值放到arr[i]的位置循环n-1次就得到了选择排序。这其实是我觉得这道题最值得玩味的地方它不是一个孤立的题目而是选择排序的微型原型。你和学习者从极值交换开始把一次交换扩展成循环交换自然就理解了选择排序的骨架。所以这道题也经常被用来引出排序话题。选择排序的时间复杂度是O(n^2)空间复杂度O(1)虽然不适合大规模数据但作为教学案例非常清晰。极值交换本身则是O(n)时间、O(1)空间因为它只需要一趟遍历。5.2 在真实项目里什么场景会用到极值交换有人可能会问这题除了面试和上课还有实际用途吗说实话你很少会在业务代码里直接写一个把最小值换到数组开头的函数但类似的思维在不少场景里都会用到。比如展示一组数据的排行时你希望把最高分和最低分高亮显示并且置顶或置底处理传感器采集的若干采样点时想先定位异常的最大最小值再做过滤做短视频帧抽帧时需要找到清晰度最高和最低的帧并做标记。这些场景背后的核心操作都是在数组里找到极值并且按位置做后续处理只是业务包裹了一层皮而已。此外很多排序算法或者快速选择算法的内部都会涉及根据值交换元素位置的步骤极值交换的代码虽然简单但它训练的是值、下标、位置变化三者之间的关系这个关系意识在更高阶的算法里非常关键。5.3 如果你非要调用现成函数怎么最短实现如果题目允许使用标准库很多人会想走捷径。Python 里确实可以写得很短def swap_extremes_v2(arr): if len(arr) 1: return arr mn min(arr) mx max(arr) min_idx arr.index(mn) max_idx arr.index(mx) arr[0], arr[min_idx] arr[min_idx], arr[0] if max_idx 0: max_idx min_idx arr[-1], arr[max_idx] arr[max_idx], arr[-1] return arr这里有一个容易踩的坑在 Python 中arr.index(mx)返回的是第一个最大值的位置。如果使用max_idx arr.index(mx)之后再交换仍然会遇到和手写版本相同的下标失效问题所以修正逻辑依然不能省。用内置函数只是省了遍历代码核心的算法思维一点都没少。至于用排序来偷懒比如直接arr.sort()确实能让首尾变成最小值和最大值但它会把所有元素重新排序而不是保持其他元素的相对顺序。很多题目的隐含要求正是只动极值其他元素位置不变所以排序方案在语义上是不成立的。算法题里最忌讳的就是跑得通但改写了题目的规则。就我个人来说现在再看到数组极值交换这类题已经不会纠结于怎么写能跑通了而是会下意识地去想交换操作之后我手上的下标还新不新鲜这个习惯帮我解决了不少比这道题复杂得多的数组处理问题。建议你在练习时也准备几个长度为 2 到 5 的测试用例先把代码写出来再手动推演一遍尤其是让极值一开始就位于首尾的情况。把这个流程走熟了类似的记录下标后再交换的题目你基本都不会再栽跟头。