【码道初阶】Leetcode.189 轮转数组:不熟悉ArrayList时踩得坑,被Arraylist初始化骗了?

【LeetCode 踩坑日记】No.189 轮转数组:被 ArrayList 的初始化骗了?

在刷算法题的过程中,我们经常会遇到一种情况:思路明明是正确的,逻辑也很通顺,但代码一跑就崩,或者结果不对。

今天我在做 LeetCode 189. 轮转数组 时,就遭遇了一个经典的 Java 集合陷阱。这篇博客将记录我从"理直气壮"到"恍然大悟"的排错过程,并总结关于数组轮转的通用解法。

1. 题目回顾

题目 :给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置。
示例

输入: nums = [1,2,3,4,5,6,7], k = 3

输出: [5,6,7,1,2,3,4]

2. 我的"完美"思路与报错代码

我的第一反应非常直观:

  1. 创建一个新的列表 list
  2. 遍历原数组,计算每个元素移动 k 步后的新位置。
  3. 如果位置超出了数组长度,就绕回到开头;否则直接放入新位置。
  4. 使用 ArrayListset 方法填值。

于是我写出了以下代码:

java 复制代码
// ❌ 错误示范
class Solution {
    public void rotate(int[] nums, int k) {
        // 我以为这里创建了一个长度为 nums.length 的列表
        List<Integer> list1 = new ArrayList<>(nums.length); 
        
        for(int i = 0; i < nums.length; i++) {
            // 这里逻辑其实有点硬编码,把 k 当成了 3
            if(i + 3 > nums.length - 1) {
               list1.set(i + 3 - nums.length, nums[i]);
            } else {
               list1.set(i + 3, nums[i]);
            }
        }
        System.out.println(list1);
    }
}

运行结果

直接抛出 java.lang.IndexOutOfBoundsException: Index 2 out of bounds for length 0

3. 深度解析:为什么会报错?

看到报错我非常困惑:我在 new ArrayList<>(nums.length) 时不是已经指定长度了吗?为什么报错说 length 0

这里触及到了 Java ArrayList 的一个核心机制:Capacity(容量) vs Size(元素个数)

  • Capacity(容量):是你告诉底层数组"请预留多少空间"。
  • Size(实际大小):是列表里实际上装了多少个有效元素。

当我们执行 new ArrayList<>(10) 时,Java 只是在内存里开辟了一个能放10个东西的"空架子",但是架子上还没有书 ,此时 size 依然是 0。

  • add() 方法 :是在列表尾部追加元素,会使 size + 1
  • set(index, element) 方法 :是修改 现有位置的元素。它会检查 index 是否小于 size

因为我的列表是空的(Size = 0),任何 set(index, val) 操作都会被判定为越界,因为那个位置上根本还没有元素可以被"修改"。

修正思路

如果想用"指定位置填值"的逻辑,应该直接使用 数组 (int[]) ,因为数组在创建时(new int[n]),所有位置默认已经初始化为 0,长度是固定的,可以直接通过下标赋值。

4. 逻辑上的二次修正

除了报错,我的代码逻辑还有两个问题需要解决才能通过 LeetCode 的测试:

A. 变量 k 的处理

我之前的代码里写死了 i + 3,这是针对示例 1 的特解。题目中的 k 是变量,且 k 可能大于数组长度。
改进 :使用取模运算符 %
newIndex = (i + k) % n

这行公式完美解决了越界和绕回的问题,不需要写 if-else 判断。

B. 引用修改 vs 打印

题目方法的返回类型是 void。这意味着仅仅 System.out.println 是没用的,LeetCode 的判题系统会检查内存中 nums 数组的内容是否发生了变化。
改进 :必须把计算出的新结果,重新拷贝回原数组 nums

5. 最终正确代码(辅助数组法)

经过思考,我放弃了 ArrayList,改用辅助数组,代码变得清晰且高效:

java 复制代码
// ✅ 正确解答
class Solution {
    public void rotate(int[] nums, int k) {
        int n = nums.length;
        // 1. 创建一个同样长度的新数组
        int[] newArr = new int[n];
        
        // 2. 计算新位置并赋值
        for (int i = 0; i < n; i++) {
            // 使用 (i + k) % n 处理循环移动
            newArr[(i + k) % n] = nums[i];
        }
        
        // 3. 将新数组的值拷贝回原数组
        // System.arraycopy(newArr, 0, nums, 0, n); // 也可以用这个API
        for (int i = 0; i < n; i++) {
            nums[i] = newArr[i];
        }
    }
}

6. 进阶思考:能不能不费额外空间?

上面的解法使用了 O(N) 的额外空间(创建了 newArr)。如果面试官问:"能不能在 O(1) 空间复杂度下完成?即不使用额外数组?"

这需要用到**"三次翻转法"**(非常巧妙):

  1. 翻转整个数组。
  2. 翻转前 k % n 个元素。
  3. 翻转剩余的元素。

示例[1,2,3,4,5,6,7], k=3

  1. 全翻转:[7,6,5,4,3,2,1]
  2. 翻转前3个:[5,6,7, 4,3,2,1]
  3. 翻转后4个:[5,6,7, 1,2,3,4] -> 成功!

7. 总结

这次排错让我学到了三点:

  1. API 误区new ArrayList<>(capacity) 只是申请内存,不代表 List 里有元素。想按索引操作,优先选数组,或者先填充 List。
  2. 数学之美 :处理循环数组索引时,% 取模运算比 if-else 判断更优雅通用。
  3. 审题重要性:注意函数的返回类型和副作用要求(是返回新对象,还是原地修改)。

编程就是这样一个不断犯错、理解底层、然后优化的过程。希望能帮到遇到同样报错的你!


相关推荐
BBB努力学习程序设计2 小时前
Java设计模式实战指南:创建型模式深度解析
java
卡尔AI工坊2 小时前
万众瞩目的 GPT 5.2,连个火柴人游戏都做不明白?
后端·算法
BBB努力学习程序设计2 小时前
Java内存管理与JVM调优完全指南
java
fantasy_arch2 小时前
leetcode算法-最大乘积子数组
算法·leetcode·职场和发展
编程火箭车2 小时前
【Java SE 基础学习打卡】22 分支结构 - if
java·流程控制·编程基础·if语句·分支结构·条件判断·新手避坑
Ivy_belief2 小时前
C++新特性汇总:涵盖C++11到C++23
java·c++·c++11·c++23
玩具猴_wjh2 小时前
慧科讯业面试复盘
面试·职场和发展
dragoooon342 小时前
[hot100 NO.8~12]
算法
哈哈哈笑什么2 小时前
Spring Boot接口国际化异常信息方案
java·spring boot·后端