多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

数组元素交换最大值最小值:索引修正与边界条件详解

数组元素交换最大值最小值:索引修正与边界条件详解 1. 题目到底在考什么先别急着写代码。这道题看起来只是“交换数组首尾元素”但它其实埋了不少坑尤其是“最大的与第一个元素交换最小的与最后一个元素交换”这句话很多人第一次做都会掉进顺序陷阱里。题目本身很简单输入一个数组找到最大值和最小值分别和第一个元素、最后一个元素交换然后输出结果。但这里有一个非常经典的问题如果最大值本身就在最后一个位置或者最小值本身就在第一个位置你先交换哪一个会直接影响结果。我当年第一次做这道题时直接先交换最大值结果把最小值的位置搞乱了最后输出完全不对。后来才意识到这类题的核心不是“会不会遍历找最大最小”而是“交换顺序会不会覆盖你还没处理的索引”。这道题适合谁适合刚学完数组、准备开始刷简单题的人。它能把循环、索引、临时变量、边界条件这些基础概念一次性串起来。尤其是如果你在准备面试或考试搞清楚这类题背后的“索引失效”问题后面很多中等难度题都会轻松很多。2. 先拆解两种常规解法2.1 做法一先找下标再统一交换这是最直观的做法先遍历一遍数组记录最大值和最小值的下标遍历结束后再统一做交换。public static void swapFirstWithMaxAndLastWithMin(int[] arr) { int maxIndex 0; int minIndex 0; for (int i 1; i arr.length; i) { if (arr[i] arr[maxIndex]) { maxIndex i; } if (arr[i] arr[minIndex]) { minIndex i; } } // 先交换最大值和第一个元素 int temp arr[0]; arr[0] arr[maxIndex]; arr[maxIndex] temp; // 再交换最小值和最后一个元素 temp arr[arr.length - 1]; arr[arr.length - 1] arr[minIndex]; arr[minIndex] temp; }这个写法看起来很漂亮但你要是直接跑就会发现问题。举个具体例子输入[20, 30, 10, 40, 50]最大值50在最后一个位置也就是maxIndex 4最小值10在中间minIndex 2。第一次交换最大值和第一个元素后数组变成[50, 30, 10, 40, 20]这一步没问题。但第二次交换最小值时minIndex仍然等于2而数组的第2个位置此时还是10所以也没问题。那问题出在哪出在最大值位于maxIndex且刚好和最后一个元素重叠或者最小值刚好在第一个元素的位置时。举另一个例子输入[10, 20, 30, 40, 5]最大值40的maxIndex 3最小值5的minIndex 4。先交换最大值和第一个元素结果[40, 20, 30, 10, 5]注意最小值5还在最后没问题。再交换最小值和最后一个元素得到[40, 20, 30, 10, 5]看起来还是不对其实第一次交换后数组变成[40, 20, 30, 10, 5]最小值在最后一个位置minIndex4和最后一个元素是同一个位置交换等于没交换最终结果没问题。关键是minIndex有没有被第一次交换影响。2.2 临界情形最大值在末尾最小值在开头这才是真正考验人的情况。看这个数组输入[5, 20, 30, 40, 50]最大值50在末尾最小值5在开头。按刚才的做法先交换最大值和第一个元素数组变成[50, 20, 30, 40, 5]此时原来的最小值5被换到了末尾minIndex还停留在0。接下来交换最小值和最后一个元素时minIndex0也就是把arr[0]和arr[4]交换结果又变回了[5, 20, 30, 40, 50]完美地回到了原样。这个问题不是偶发而是必现的只要maxIndex和minIndex之间存在“你动了我还没处理的位置”就会互相破坏。解决思路有两个方向交换前判断索引是否重叠或者交换后修正受影响的minIndex。2.3 做法二交换后修正索引推荐最常见的修正方案是先交换最大值然后检查最小值索引是否受影响。public static void swapMaxMin(int[] arr) { if (arr null || arr.length 2) { return; } int maxIndex 0; int minIndex 0; for (int i 1; i arr.length; i) { if (arr[i] arr[maxIndex]) { maxIndex i; } if (arr[i] arr[minIndex]) { minIndex i; } } // 先把最大值和第一个元素交换 int temp arr[0]; arr[0] arr[maxIndex]; arr[maxIndex] temp; // 如果最小值原本在第一个位置它已经被换到 maxIndex 处了 if (minIndex 0) { minIndex maxIndex; } // 再把最小值和最后一个元素交换 temp arr[arr.length - 1]; arr[arr.length - 1] arr[minIndex]; arr[minIndex] temp; }这段代码的关键就一句话if (minIndex 0) minIndex maxIndex;。意思是如果最小值一开始就在第一个位置经过第一次交换后它已经被移动到maxIndex的位置了所以我们要把minIndex修正为原来的maxIndex。这个写法能覆盖所有情况不光是最大值和最小值重叠的问题还包括最大值在末尾、最小值在末尾等各种边界组合。但这里也有一个隐藏细节如果minIndex 0的同时maxIndex arr.length - 1那么第一次交换后原来的最大值到了开头原来的最小值到了末尾两个目标同时完成修正minIndex maxIndex也不会出错。3. 从需求到实现完整流程拆解3.1 输入阶段如何正确读取数组题目说的是“输入数组”但具体怎么输入通常由评测环境决定。常见的有两种控制台输入或者接口参数直接传入。这里我给你一个控制台版本的完整代码方便你本地跑import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner scanner new Scanner(System.in); System.out.print(请输入数组长度 n: ); int n scanner.nextInt(); int[] arr new int[n]; System.out.println(请输入 n 个整数:); for (int i 0; i n; i) { arr[i] scanner.nextInt(); } swapMaxMin(arr); System.out.println(交换后的数组:); for (int num : arr) { System.out.print(num ); } System.out.println(); } public static void swapMaxMin(int[] arr) { if (arr null || arr.length 2) { return; } int maxIndex 0; int minIndex 0; for (int i 1; i arr.length; i) { if (arr[i] arr[maxIndex]) { maxIndex i; } if (arr[i] arr[minIndex]) { minIndex i; } } int temp arr[0]; arr[0] arr[maxIndex]; arr[maxIndex] temp; if (minIndex 0) { minIndex maxIndex; } temp arr[arr.length - 1]; arr[arr.length - 1] arr[minIndex]; arr[minIndex] temp; } }这个main方法没什么技术含量就是最基本的Scanner读取。不过有一点值得提醒nextInt()遇到非数字输入会抛异常如果测试环境给的数据格式不干净建议改用hasNextInt()做一下判断。实战中很多简单题不是算法难而是输入解析没处理好导致运行时错误。3.2 边界条件处理为什么length 2直接返回如果数组长度是0或1根本没有交换的意义。长度为1时第一个元素和最后一个元素是同一个元素最大值和最小值也是同一个元素交换等于没交换强行处理反而可能出现索引越界。所以开头判断arr.length 2直接返回是一种防御性写法能避免后续无谓的麻烦。还有一种情况数组元素全部相等。比如[7, 7, 7, 7]maxIndex和minIndex都是0第一次交换arr[0]和arr[0]数组不变第二次交换arr[3]和arr[3]也不变。没有报错也没有 bug。但从需求角度讲这是符合预期的因为没有“最大”和“最小”的明显区分时交换与否无所谓。3.3 相等元素的处理顺序还有一个细节容易忽略当数组里有多个最大值或多个最小值时我们应该选择哪一个进行交换比如[10, 50, 20, 50, 5]最大值50出现了两次分别在下标1和下标3。题目说“最大的与第一个元素交换”到底交换哪一个不同的遍历写法会产生不同结果。如果代码是if (arr[i] arr[maxIndex]) { maxIndex i; }那么只有遇到严格大于当前最大值时才会更新下标最终保留的是“第一个出现的最大值下标”。反之如果用就会保留“最后一个出现的最大值下标”。这两种选择在多数情况下都不影响最终效果因为值和第一个元素交换后数组中剩下的最大值位置虽然不同但数组整体内容一致。但有一种特殊情况会让结果产生差异最大值在数组末尾且最大值只出现一次时和没有区别如果最大值在末尾且还有其他相等的最大值那么交换哪个下标会影响另一个相同值的位置。这里我的建议是采用和也就是保留“第一次出现的最值下标”。原因很简单人的直觉就是从左往右找最值第一次遇到最大值时记下来后面如果碰到更大的再更新。这样代码可读性最好也最容易向别人解释。如果题目明确要求交换“第一个”最大值那就更得用了。4. 交换的三种实现方式对比4.1 临时变量交换int temp arr[0]; arr[0] arr[maxIndex]; arr[maxIndex] temp;这个方式最稳不挑数据类型哪怕元素是对象也能用。缺点是引入了一个局部变量不过对于基本类型数组这个成本可以忽略。4.2 加减法交换arr[0] arr[0] arr[maxIndex]; arr[maxIndex] arr[0] - arr[maxIndex]; arr[0] arr[0] - arr[maxIndex];这个看起来节省了临时变量实际上有严重隐患两个整数相加可能溢出。比如arr[0]和arr[maxIndex]都是接近Integer.MAX_VALUE的数相加后变成负的交换结果完全错误。所以我从不推荐在普通业务代码里用这种技巧除非你确定数值范围很小且代码可读性优先级别较低。面试时如果你敢写这个面试官大概率会追问溢出问题。4.3 异或交换arr[0] arr[0] ^ arr[maxIndex]; arr[maxIndex] arr[0] ^ arr[maxIndex]; arr[0] arr[0] ^ arr[maxIndex];异或交换的原理是利用了异或运算的对称性。它不会溢出也不依赖临时变量但你得小心一点如果arr[0]和arr[maxIndex]是同一个索引也就是maxIndex 0那连续三次异或会把这个元素变成0。比如一个数组[5, 2]如果最大值在第一个位置用异或交换arr[0]和arr[0]最终arr[0]会被清零整个数组就错了。那你说我可以先判断一下maxIndex ! 0再交换确实可以但代码就变啰嗦了。对比来看临时变量方案没有任何这些幺蛾子所以我建议你在刷题和日常开发中统一用临时变量。它看起来最朴素但最不容易出错。5. 算法复杂度与分析这个算法只需要一趟遍历所以时间复杂度是O(n)。空间复杂度是O(1)因为只用了常数个额外的变量。O(n)意味着什么就是数组越大耗时按比例增长。但这类题目里的 n 一般来说不会很大哪怕是十万级别这个遍历也毫秒级完成。关键是你要养成“先找下标再交换”的思路不要把交换放在遍历过程中。有人会想着在遍历时同时做交换结果越写越乱不仅效率没有提升正确性还难保证。简单题的价值不在于让你优化常数时间而在于训练你处理“多个操作之间的互相影响”。如果你能把这道题的索引修正逻辑搞清楚以后写多指针、滑动窗口、排序算法的交换逻辑时就不会那么容易被边界条件坑到。6. 常见错误与排查逻辑我总结了做这道题时最常见的几类错误基本都是方向性陷阱你可以对照着排查。错误类型具体表现原因分析解决方式先换最小值后换最大值最大值没换到开头第一次交换动了最大值位置第二次用旧下标交换出错要么先换最大值再修正最小值要么先换最小值再修正最大值没有修正 minIndex数组变回原样或部分错乱最大值在首位时最小值被换走但索引没更新交换后加if (minIndex 0) minIndex maxIndex;和混用最值下标选取不稳定多个重复值时判定条件决定了取第一个还是最后一个统一使用严格大于/小于未处理数组长度边界越界异常空数组或长度为1时执行交换逻辑开头判断 arr null交换时用同一索引元素被置零异或或不变maxIndex 0或minIndex arr.length - 1时发生自身交换使用临时变量方案天然规避我记得有一次给一个朋友讲这道题他写的代码逻辑完全没问题但就是输出不对。我让他把每一步打印出来才发现他写的是if (arr[i] arr[max]) { max i; } else if (arr[i] arr[min]) { min i; }这里用了else if导致每次循环只能判断一个分支。如果当前元素既是新的最大值又是新的最小值比如数组长度大于1但所有元素都相等就会出现逻辑漏洞。但更重要的是else if会让判断不充分——一个元素不可能同时大于最大值又小于最小值所以else if在这个场景下其实不会漏掉正确结果。真正的问题是有人会把else写成和if平级的另一段逻辑导致重复判断。建议还是分开两个if写逻辑清楚也不容易误改。还有一次某个同学写完交换后在循环里更新了maxIndex和minIndex的判定条件结果因为交换后数组内容变了后续的遍历被已交换的数据干扰得出的最值位置完全错了。这也是一个很重要的点找最值和交换要分阶段不要在遍历的同时做交换。7. 进阶如果要交换最大值到第一个、最小值到最后一个但只允许一次遍历怎么办有些面试官会在你写完基础版本后加一句“能不能优化到一次遍历完成”。仔细想想一次遍历里同时完成“找最值”和“交换”是做不到的因为交换需要知道最终的最值位置而最终位置是遍历结束后才能确定的。但可以做到的是在遍历过程中记录最大值和最小值的下标遍历结束后一次性完成两次交换。这已经是理论最优了。如果面试官说的是一次遍历就完成所有交换那他实际上是在考察你是否理解“依赖关系”。你完全可以回答两次交换之间存在索引依赖但找最值的过程只需要一次遍历真正交换必须等最值确定后执行因此整体是“一次扫描 两次交换”。如果你想把代码进一步封装成可复用的工具方法可以把swapMaxMin变成一个静态工具类接收数组并直接修改原数组。这也是一种约定函数名暗示了它会改变传入参数的状态。在真实开发中这种“原地修改”的写法要格外谨慎至少要在注释里说明副作用避免使用者误以为传入后原数组不变。很多资深开发者看到这里会心一笑因为这类“看似简单实则巨坑”的题恰恰是面试中最容易翻车的地方。8. 我踩过的坑与调试技巧8.1 打印数组的两种方式调试这类题最直接的技巧是在交换操作后打印数组。Java 里直接System.out.println(arr)打印出来的是[I1b6d3586这种对象地址不是内容。你需要Arrays.toString(arr)System.out.println(Arrays.toString(arr));这个细节几乎每个新手都会踩一次。输出格式是[1, 2, 3]非常直观。8.2 手动模拟 vs 代码调试如果你在纸上手动模拟一遍交换过程比直接打断点更快。我会习惯写一个简短的测试方法覆盖几个边界用例普通乱序数组最大值在末尾最小值在开头最大值和最小值都在首尾位置所有元素相等数组长度为1把这几种情况跑一遍基本上这段代码的健壮性就稳了。你不需要引入测试框架直接在main里多调用几次就够用。public static void testCase(int[] testArr) { System.out.println(交换前: Arrays.toString(testArr)); swapMaxMin(testArr); System.out.println(交换后: Arrays.toString(testArr)); System.out.println(----); }你会发现最大问题真的都是索引修正不到位。一旦加上if (minIndex 0) minIndex maxIndex;所有用例都通过。8.3 一段代码在多个 JDK 版本下的表现这道题基本都是纯 Java 语法没有涉及任何高级特性所以在 JDK 8 到最新版本下表现一致。唯一需要注意的可能是如果你用var关键字声明局部变量在某些在线评测系统上会编译失败因为它们的 JDK 版本过低。我建议刷题时老老实实写完整类型名不要炫技。9. 扩展场景如果题目变成交换最大值和最小值怎么办有些变体题会把要求改为“只交换最大值和最小值的位置”不强制放到首尾。这时候其实更简单因为你不需要关心和固定位置的覆盖问题两次交换操作互不干扰直接用临时变量分别交换即可。int temp arr[maxIndex]; arr[maxIndex] arr[minIndex]; arr[minIndex] temp;但如果你要在原题基础上做变体比如“最大值和第一个元素交换次小值和最后一个元素交换”那就要记录次小值下标逻辑会复杂一点。这时就要考虑如果次小值的位置被最大值交换影响了怎么办。这类变体在面试题中偶尔出现核心思想仍然是“操作顺序对索引的影响”。我个人很推荐在刷完这道题之后顺手把“只交换最大值和最小值”“交换最大值和最后一个元素”“交换最小值和第一个元素”这几个变体也写一遍。每个变体都只是改一两行但能帮你把交换类的数组操作系统地过一遍。等你以后遇到旋转数组、链表反转、排序算法里的交换操作就能很自然地判断哪些索引需要修正哪些不需要。在真实项目里虽然很少会遇到这种“把最大和第一个交换”的业务需求但数组元素交换是排序算法、加密算法、数据清洗中经常用到的基础操作。搞清楚这道题其实是在帮你建立对“原地修改数组”这一行为的肌肉记忆。后面不管是用冒泡排序还是快速排序的分区逻辑你都会感谢当年认真折腾过这道题的自己。如果你现在在本地运行记得输入时要确保数组长度和元素个数一致。输入5然后输入5个数字回车。如果输入过程中多按了一个空格或者换行Scanner会正常跳过空白符所以不需要担心换行符问题。唯一要留意的是不要把数字输成带逗号的形式比如1,2,3,4Scanner会解析失败。保持每行一个或者空格分隔就行。
返回列表