
【LeetCode 384】打乱数组(Java 详细题解 + Fisher--Yates 洗牌算法讲解)
一、题目描述
给定一个没有重复元素的整数数组 nums,设计一个算法实现数组的随机打乱,使得所有排列出现的概率相等。
你需要实现一个类 Solution,包含以下三个方法:
java
class Solution {
public Solution(int[] nums) { } // 初始化对象
public int[] reset() { } // 重置数组并返回
public int[] shuffle() { } // 随机打乱数组并返回
}
二、示例
输入:
["Solution", "shuffle", "reset", "shuffle"]
[[[1,2,3]], [], [], []]
输出:
[null, [3,1,2], [1,2,3], [1,3,2]]
解释:
java
Solution solution = new Solution([1, 2, 3]);
solution.shuffle(); // 返回任意 [1,2,3] 的随机排列
solution.reset(); // 返回初始状态 [1, 2, 3]
solution.shuffle(); // 再次打乱
三、题目要求分析
我们需要支持三种操作:
-
初始化(Solution)
保存原始数组,以便后续可以重置。
-
reset()
返回数组的初始状态。
-
shuffle()
返回数组的随机排列,并保证所有排列的概率相同。
四、核心思路:Fisher--Yates 洗牌算法
Fisher--Yates 是一种经典的等概率随机打乱算法 。
它保证每个元素在任意位置的概率完全相等。
算法过程:
设数组长度为 n:
- 从后向前遍历数组;
- 对于每个位置
i,在[0, i]范围内随机选择一个索引j; - 交换
nums[i]和nums[j]。
伪代码如下:
for i from n-1 to 1:
j = random(0, i)
swap(nums[i], nums[j])
这能确保:
- 每个元素有 1/n 的概率出现在每个位置;
- 所有 n! 种排列出现概率相等。
五、代码实现(Java)
java
import java.util.Random;
class Solution {
private int[] original; // 保存初始数组
private int[] array; // 当前数组
private Random rand; // 随机数生成器
// 构造函数:初始化数组
public Solution(int[] nums) {
original = nums.clone();
array = nums.clone();
rand = new Random();
}
// 重置数组到原始状态
public int[] reset() {
array = original.clone();
return array;
}
// 打乱数组
public int[] shuffle() {
for (int i = array.length - 1; i > 0; i--) {
int j = rand.nextInt(i + 1); // 生成 [0, i] 范围内的随机索引
swap(array, i, j);
}
return array;
}
// 交换两个元素
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

六、执行流程示例
以 nums = [1, 2, 3] 为例:
-
初始化:
original = [1, 2, 3] array = [1, 2, 3] -
打乱过程:
- i = 2, j = random(0, 2) → 假设 j = 0 → [3, 2, 1]
- i = 1, j = random(0, 1) → 假设 j = 1 → [3, 2, 1]
→ 打乱结果可能为 [3,2,1] 或其他排列。
-
reset:
返回 [1, 2, 3]
七、复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| reset() | O(n) | O(n) |
| shuffle() | O(n) | O(1)(原地打乱) |
| 构造函数 | O(n) | O(n) |
八、为什么要使用 Fisher--Yates 算法?
许多初学者会尝试使用以下错误做法:
java
Collections.shuffle(Arrays.asList(nums));
这虽然能打乱数组,但并不适用于原生 int[],而且在算法面试中,你需要展示算法设计能力,而不仅是调用现成 API。
Fisher--Yates 洗牌算法是:
- 数学上证明等概率;
- 时间复杂度 O(n);
- 空间复杂度 O(1);
- 面试官非常喜欢考察的随机算法经典题。
九、进阶思考
-
如果数组中存在重复元素,如何保证"等概率"?
- Fisher--Yates 仍然适用,只是最终排列会有重复结果。
-
如果想要每次 shuffle 结果都不重复,该如何实现?
- 可以用一个集合记录已出现过的排列,但复杂度会非常高(O(n!)),通常不可行。
-
若需要部分打乱前 k 个元素 ,可以在洗牌时只迭代到
i = k - 1。
十、总结
| 要点 | 内容 |
|---|---|
| 核心算法 | Fisher--Yates 洗牌 |
| 实现要点 | 从后向前遍历,每次随机交换 |
| 保证等概率 | 每个元素独立均匀地分布在任意位置 |
| 时间复杂度 | O(n) |
| 空间复杂度 | O(1) |