【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. 我的"完美"思路与报错代码
我的第一反应非常直观:
- 创建一个新的列表
list。 - 遍历原数组,计算每个元素移动
k步后的新位置。 - 如果位置超出了数组长度,就绕回到开头;否则直接放入新位置。
- 使用
ArrayList的set方法填值。
于是我写出了以下代码:
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) 空间复杂度下完成?即不使用额外数组?"
这需要用到**"三次翻转法"**(非常巧妙):
- 翻转整个数组。
- 翻转前
k % n个元素。 - 翻转剩余的元素。
示例 :[1,2,3,4,5,6,7], k=3
- 全翻转:
[7,6,5,4,3,2,1] - 翻转前3个:
[5,6,7, 4,3,2,1] - 翻转后4个:
[5,6,7, 1,2,3,4]-> 成功!
7. 总结
这次排错让我学到了三点:
- API 误区 :
new ArrayList<>(capacity)只是申请内存,不代表 List 里有元素。想按索引操作,优先选数组,或者先填充 List。 - 数学之美 :处理循环数组索引时,
%取模运算比if-else判断更优雅通用。 - 审题重要性:注意函数的返回类型和副作用要求(是返回新对象,还是原地修改)。
编程就是这样一个不断犯错、理解底层、然后优化的过程。希望能帮到遇到同样报错的你!