
“将一个数组逆序输出”这大概是Java初学者遇到的第一个“看着简单但总写不对”的题目。说它简单是因为需求一句话就能讲清楚给一个数组要么倒着打印要么把顺序真正调转过来说它总写不对是因为我看了太多初学者的代码问题根本不是语法不过关而是对数组下标和算法边界的理解是糊的。这个题目适合谁零基础的Java入门者、准备实习笔试的学生还有想快速复习基础数据结构的开发者。把它吃透你能建立两个非常关键的能力一是从索引角度理解数组结构二是掌握“双指针”这种处理对称操作的基本套路。后面你再碰链表反转、字符串反转、回文判断、快排分区这些题会发现它们骨子里都是同一套思维。这篇文章我就把这道题从题目拆解、三种实现方案、完整实操到高频报错一次性讲透。1. 拆题之前先搞清楚“逆序输出”到底在问什么1.1 两种语义是倒着打印还是真正逆置很多同学拿到题就开始写代码写完发现结果不对回头一看题目都没读懂。这里有个最容易忽略的区别。假设有一个数组int[] arr {3, 1, 4, 1, 5, 9, 2, 6};需求A在屏幕上依次打印 “6 2 9 5 1 4 1 3”但数组本身还是原来的顺序。需求B把数组元素真实翻转让 arr 变成 {6, 2, 9, 5, 1, 4, 1, 3}然后输出这个新数组的内容。前者只是遍历方向变了叫“逆序输出”原数组纹丝不动后者是数据位置整体调换叫“逆置”或“反转”。标准的“将一个数组逆序输出”如果没有额外说明通常指的是后者——把数组本身改造成逆序状态然后再遍历打印。但也经常有题目只要求前者所以动手前第一件事是把题目读清楚确认它要的是打印效果还是实际修改。1.2 数组的特性和“下标从0开始”这件事数组是同一类型元素的连续存储结构它天生就是通过下标来定位数据的。任何一个下标 i 对应元素 arr[i]数组长度为 n 时有效下标范围永远是 0 到 n-1。这个结论听起来是废话但实际写循环的时候不知道多少人栽在这上面。如果你理解了“逆置”的本质是对称翻转那核心逻辑就一句话下标为 i 的元素要和下标为 n-1-i 的元素交换位置。比如长度为5的数组arr[0] 跟 arr[4] 换arr[1] 跟 arr[3] 换arr[2] 保持不动。生活里可以这么类比一排椅子编号1到10要求把10号的人挪到1号位9号的人挪到2号位……本质上就是两个端点往中间靠拢每靠拢一步交换一对人。这不涉及什么高深算法就是对称操作。2. 三种主流实现方式每一种都值得亲手写一遍2.1 新建数组法思路最直白适合第一次接触第一种方案非常直观我重新创建一个同样长度的数组然后从原数组的末尾开始逐个取元素放进新数组最后返回新数组。public static int[] reverseByNewArray(int[] arr) { int n arr.length; int[] result new int[n]; for (int i 0; i n; i) { result[i] arr[n - 1 - i]; } return result; }这段代码的关键在于映射关系新数组下标 i 对应的值来自原数组下标 n-1-i。当 i 是 0 时取的是 arr[n-1]当 i 是 n-1 时取的是 arr[0]。一点弯路都没有就是两排数一对一对上。这个方法的时间复杂度是 O(n)因为循环里每个元素只会被处理一次空间复杂度也是 O(n)因为它额外创建了一个新数组。优点是代码清晰、逻辑简单、不影响原数组的数据适合在“不能修改原数组”的场景里用。缺点是白白多占了一份内存数据量大时不划算。2.2 双指针原地交换法面试官最想看到的解法第二种方案不创建新数组直接在原数组上做交换用一个 while 循环让两个指针往中间走。public static void reverseInPlace(int[] arr) { int left 0; int right arr.length - 1; while (left right) { int tmp arr[left]; arr[left] arr[right]; arr[right] tmp; left; right--; } }这段代码的逻辑分三步初始化左右指针、交换两端元素、向中间收紧。循环什么时候结束当 left right 时说明要么已经走到了数组中心要么两边已经交错交换也该停了。时间复杂度同样是 O(n)因为每个元素最多参与一次交换空间复杂度只有 O(1)只借助了一个临时变量不依赖数据规模。这是面试中最推荐的解法因为它在时间和空间两个维度都做到了最优。这里有两个细节值得展开。第一为什么交换必须要临时变量你试一下不加 tmp 会怎样arr[left] arr[right]; arr[right] arr[left];第一句已经用 arr[right] 覆盖了 arr[left] 原来的值等执行第二句时arr[left] 已经不是原来的值了。结果到最后两个位置存的是同一个数另一个元素彻底丢了。这就像两杯饮料要互换必须先把一杯倒进第三个空杯才能腾出原来的容器。临时变量就是这个“第三个空杯”。第二为什么循环条件是 left right 而不是 left right如果数组长度是奇数比如5个元素left 和 right 最终会在中间元素相遇。这时候 left right元素跟自身交换没有任何意义结果虽然一样但逻辑不够干净如果数组长度是偶数left 和 right 根本不会相等它们会交错而过。更重要的是如果条件写成 left right在 left right 之后还会继续执行一次交换等交换完再退出虽然结果没变但白白多了一次无效操作。所以 是更严谨的选择。2.3 递归法换个角度看同一个问题递归版本的核心思想和双指针一样都是“两端交换向中间靠拢”只是把迭代换成了函数调用。public static void reverseByRecursion(int[] arr, int left, int right) { if (left right) { return; } int tmp arr[left]; arr[left] arr[right]; arr[right] tmp; reverseByRecursion(arr, left 1, right - 1); }调用时从外部传入起始边界reverseByRecursion(arr, 0, arr.length - 1)。递归的优势在于代码结构非常清晰地反映了“分治”的思想当前问题拆成“交换一对元素”和“把剩下的子数组继续逆序”两个子问题。递归出口是 left right表示子数组为空或只剩一个元素。但这里必须说清楚递归的时间复杂度是 O(n)空间复杂度却是 O(n)因为每次递归调用都会占用一层调用栈数组越大栈越深。对一个几百万元素的数组执行递归反转大概率会碰到栈溢出。所以在实际开发里我不推荐用递归做这件事它更适合用来帮助理解分治思想生产环境还是用双指针迭代最靠谱。2.4 工具库方法Collections.reverse 和它的坑Java 自带了一个现成工具java.util.Collections.reverse但它只能接收 List不能直接接收基本类型数组。有的人会写int[] arr {1, 2, 3, 4, 5}; Listint[] list Arrays.asList(arr); Collections.reverse(list);看起来没问题运行也不报错但结果是完全错误的。因为 Arrays.asList 接收的是 int[] 这个整体泛型会被推断成 Listint[]集合里只有一个元素——整个数组本身。反转一个只有一个元素的列表等于什么都没干。正确做法是先手动把基本类型数组装进一个 ArrayListListInteger list new ArrayList(); for (int value : arr) { list.add(value); } Collections.reverse(list); System.out.println(list);或者用 Java 8 的 Stream 一行搞定ListInteger list Arrays.stream(arr) .boxed() .collect(Collectors.toList()); Collections.reverse(list);从这个坑能看出来一个道理工具方法虽然方便但它有自己的适用边界。你在实际项目中当然可以放心用 Collections.reverse但如果是在笔试或作业里遇到这道题直接用工具库等于放弃了展示算法能力的机会改卷子的人多半不会给你分。3. 手把手实操从零写一段能跑的 Demo3.1 环境准备JDK 和编辑器怎么选做这个 Demo 不需要复杂的工程结构只要本机装了 JDK 8 或更高版本就行。打开命令行敲一下java -version能看到版本号就说明环境没问题。编辑器方面新手我建议用社区版的 IDEA图形化调试功能非常直观可以看着变量一步步变化老手用 VS Code 或者记事本都能写。这个题只需要一个 .java 文件完全不用建 Maven 或 Gradle 工程从 IDE 里新建一个项目反而多余直接新建一个名为 ReverseArrayDemo.java 的文件就够了。3.2 完整代码逐行拆解我把三种方案放进同一个 Demo 里方便对照验证。完整代码如下import java.util.Arrays; public class ReverseArrayDemo { public static void main(String[] args) { int[] arr {1, 2, 3, 4, 5}; System.out.println(原始数组: Arrays.toString(arr)); // 方案一新建数组法 int[] reversed1 reverseByNewArray(arr); System.out.println(新建数组法结果: Arrays.toString(reversed1)); // 方案二双指针原地交换 reverseInPlace(arr); System.out.println(原地逆置结果: Arrays.toString(arr)); // 方案三递归法先把数组逆回去再演示递归逆置 reverseByRecursion(arr, 0, arr.length - 1); System.out.println(递归还原结果: Arrays.toString(arr)); } public static int[] reverseByNewArray(int[] arr) { int n arr.length; int[] result new int[n]; for (int i 0; i n; i) { result[i] arr[n - 1 - i]; } return result; } public static void reverseInPlace(int[] arr) { int left 0; int right arr.length - 1; while (left right) { int tmp arr[left]; arr[left] arr[right]; arr[right] tmp; left; right--; } } public static void reverseByRecursion(int[] arr, int left, int right) { if (left right) { return; } int tmp arr[left]; arr[left] arr[right]; arr[right] tmp; reverseByRecursion(arr, left 1, right - 1); } }main 方法里的执行顺序是有讲究的先用新建数组法逆置原数组 arr 保持不变接着用原地交换法把 arr 真正逆置最后为了让递归法也能独立演示我先把 arr 递归逆回去恢复成原始顺序。注意函数设计的两个细节第一个方法有返回值因为它创建了新数组需要把结果传回给调用方第二和第三个方法是 void因为它们直接修改传入数组的内容不需要返回值。这也是一个值得养成的习惯方法是否会修改入参决定了它的返回值设计。3.3 编译、运行和用调试器观察文件编译运行的命令如下javac ReverseArrayDemo.java java ReverseArrayDemo运行输出应该是原始数组: [1, 2, 3, 4, 5] 新建数组法结果: [5, 4, 3, 2, 1] 原地逆置结果: [5, 4, 3, 2, 1] 递归还原结果: [1, 2, 3, 4, 5]注意这里有一个很妙的验证点把数组逆置后再逆一次能还原回原来的顺序。你可以利用这一点来验证自己的反转函数写没写错。如果你用的是 IDEA我强烈建议你在原地交换法的 while 循环里打一个断点然后以 Debug 模式运行。每次停下来观察左侧调试面板里 left、right、arr[left]、arr[right] 四个值的变化你会亲眼看到两个指针从两端往中间靠近数据一对一对换过去。这个过程比看十遍代码都管用。4. 高频翻车现场与排查速查表4.1 数组越界想从最后一个开始结果从 arr.length 开始了这是最典型的新手错误。想倒序遍历数组结果写出了这样的代码for (int i arr.length; i 0; i--) { System.out.print(arr[i] ); }运行直接抛出Exception in thread main java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5问题在于 arr.length 是数组长度不是最大下标。长度为5的数组下标只有 0、1、2、3、4根本没有 5。正确写法是把起点设为 arr.length - 1for (int i arr.length - 1; i 0; i--) { System.out.print(arr[i] ); }这里顺便提醒一句终止条件写 i 0 会多执行一次 i 等于 0 的循环然后 i 变成 -1 退出如果你写成 i 0就会漏掉第一个元素。把这个边界捋清楚比背十遍代码都重要。4.2 交换数据时没有用临时变量前面已经演示过这个错误但我要再说一遍它的症状因为它太常见了。错误代码是arr[left] arr[right]; arr[right] arr[left];对数组 {1, 2, 3, 4, 5} 执行第一轮时arr[0] 会被覆盖成 5然后 arr[4] 也被赋成 5原来的 1 永远消失了。等到 left 和 right 走到中间数组变成 {5, 4, 3, 4, 5}两头都丢了数据。记住交换三步走存值、赋右、赋左。不要贪图省变量。4.3 需求没看清只打印 vs 真正逆置“逆序输出”这四个字在实际上机或笔试中出现时经常有两个版本如果题目说“从后往前输出数组元素”那是遍历方向问题用上面的倒序 for 循环打印即可数组不动。如果题目说“将数组逆序”或“逆置数组”那是在要求改变数据的位置。我在帮人看代码时见过不少同学把第一种做成了第二种或者反过来。前者改了不该改的数据后者输出结果不变。解决办法只有一个下笔前把题目中的动词圈出来是用“输出”还是用“逆序/倒置/反转”。动词不同代码完全不同。4.4 高频问题排查速查表我把这道题常见的报错和症状整理成一张表遇到问题直接查比你去论坛翻帖子快得多。症状可能原因解决办法ArrayIndexOutOfBoundsException下标写成了 arr.length而不是 arr.length - 1把循环起点改为 length - 1数组内容变成 5 4 3 4 5 这种对称重复交换时没有用临时变量数据被覆盖了用三步交换法输出结果和原数组一模一样循环条件用了 或者逆置之后又被逆置了一次使用 条件检查调用顺序Arrays.asList 之后集合 size 为 1基本类型数组被整体当成了 List 的一个元素手动遍历装箱或用 Stream 的 boxed()大数组执行递归反转时 StackOverflowError递归调用太多层栈空间不够改用双指针迭代方案编译报错 “找不到符号”文件名与 public 类名不一致让 public 类名等于文件名这张表基本上覆盖了初学者在这个题上能遇到的所有问题。它不是让死记硬背而是让你知道报错信息里带 Index 的八成是边界问题数据重复或丢失的八成是覆盖问题答案顺序不对的八成是循环条件问题。排查方向对了十分钟内一定能解决。5. 别停留在题上逆序背后藏着的算法思想5.1 双指针让对称操作变得极其简单当你把双指针原地交换法写完一遍再看其他题会发现“咦这个套路我见过”。比如判断一个字符串是不是回文public static boolean isPalindrome(char[] s) { int left 0; int right s.length - 1; while (left right) { if (s[left] ! s[right]) { return false; } left; right--; } return true; }这段代码的结构跟数组逆置几乎一模一样唯一的区别是原题的“交换”变成了“比较”。这就是双指针思想的威力它把很多对称性操作统一成了一个套路两端往中间靠拢边靠拢边处理逻辑。快排的 partition 阶段、有序数组的两数之和、链表求中点底层都有这个影子。5.2 从数组延伸到字符串和链表字符串反转和数组反转非常像但因为 String 是不可变的你不能直接修改字符串内部的字符序列只能先把它转成 char[] 再逆置或者用 StringBuilderString str hello; String reversed new StringBuilder(str).reverse().toString();如果自己实现char[] chars str.toCharArray(); reverseInPlace(chars); String reversed new String(chars);链表反转的思路又有点不同它没有一个物理上的“数组下标”可以操作需要用一个 prev 指针记录上一个节点ListNode prev null; ListNode curr head; while (curr ! null) { ListNode next curr.next; curr.next prev; prev curr; curr next; } return prev;链表反转是面试高频题看它跟数组逆置的区别会发现双指针在数组里是“下标锚点”在链表里是“节点引用”。思想同源实现不同。所以你把这个数组题吃透对后面理解链表反转的 prev 赋值顺序会很有帮助。5.3 真实项目里哪里用得上有人会觉得“我以后不写算法这题除了应付考试还有什么用”我举几个我在实际开发中见过的场景。图片轮播组件里有一个“上一张/下一张”的切换能力。当用户从最后一张点“下一张”回到第一张、从第一张点“上一张”跳到最后一张时本质上就是在做环形数组的逆序跳跃。排行榜如果要切换升序和降序展示最快的做法之一就是先把有序数组逆置一遍再交给前端渲染。还有撤销功能很多实现用栈来记录历史操作撤销就是把栈里的内容逆序弹出来执行“逆操作”。这些场景算法都不难但都是以“逆序/反转”为核心逻辑的。所以这道题的价值不在于它的代码量而在于它是一道“入口题”。理解它你就摸到了双指针、分治这些更高级思想的边缘。我个人实际带过不少刚开始学 Java 的朋友最深的感受是这个题难不在语法难在边界意识。下标从哪开始、到哪里结束、什么时候退出循环这些才是算法里真正决定成败的东西。我建议大家学这个题的时候别急着敲代码先拿纸画一个长度5的数组下标标0到4再用两个箭头模拟 left 和 right 一步步移动。画两遍以后你会发现自己对数组的理解突然扎实了后面写循环、边界条件、双指针都会从容很多。