LeetCode 384. 打乱数组
问题描述
给你一个整数数组 nums,设计一个算法来打乱一个没有重复元素的数组。实现 Solution 类:
Solution(int[] nums)使用整数数组nums初始化对象int[] reset()重置数组到它的初始状态并返回int[] shuffle()返回数组随机打乱后的结果
示例:
// 以数组 [1,2,3] 为例
Solution solution = new Solution([1, 2, 3]);
// 重置数组到初始状态 [1, 2, 3] 并返回
solution.reset();
// 随机返回数组 [1, 2, 3] 打乱后的结果
// 例如,返回 [3, 1, 2]
solution.shuffle();
约束条件:
1 <= nums.length <= 50-10⁶ <= nums[i] <= 10⁶nums中的所有元素都是唯一的- 最多调用
10⁴次reset和shuffle
算法思路
核心思想:Fisher-Yates 洗牌算法
Fisher-Yates 算法(也称为 Knuth 洗牌算法)是生成均匀随机排列的标准算法。
算法原理:
- 从数组的最后一个元素开始,向前遍历
- 对于每个位置
i,随机选择一个索引j,其中0 <= j <= i - 交换
nums[i]和nums[j]
为什么能保证均匀分布?
- 对于长度为 n 的数组,总共有 n! 种排列
- Fisher-Yates 算法恰好执行 n 次选择:
- 第1次:n 种选择
- 第2次:n-1 种选择
- …
- 第n次:1 种选择
- 总共产生 n × (n-1) × … × 1 = n! 种结果,每种结果概率相等
步骤:
- 初始化:保存原始数组的副本
- 重置:返回原始数组的副本
- 洗牌:
- 复制当前数组(或原始数组)
- 从后往前遍历数组
- 对每个位置 i,生成随机索引 j ∈ [0, i]
- 交换位置 i 和 j 的元素
代码实现
import java.util.*;
class Solution {
/**
* 原始数组的备份,用于reset操作
*/
private int[] original;
/**
* 随机数生成器
*/
private Random random;
/**
* 构造函数:初始化Solution对象
*
* @param nums 输入的整数数组
*/
public Solution(int[] nums) {
// 保存原始数组的副本,避免外部修改影响
this.original = nums.clone();
this.random = new Random();
}
/**
* 重置数组到初始状态
*
* @return 原始数组的副本
*
* 时间复杂度: O(n)
* 空间复杂度: O(n)
*/
public int[] reset() {
// 返回原始数组的副本,确保外部无法修改内部状态
return original.clone();
}
/**
* 返回数组随机打乱后的结果(使用Fisher-Yates洗牌算法)
*
* @return 打乱后的数组
*
* 时间复杂度: O(n)
* 空间复杂度: O(n)
*/
public int[] shuffle() {
// 创建当前数组的副本用于洗牌
int[] shuffled = original.clone();
int n = shuffled.length;
// Fisher-Yates 洗牌算法
// 从最后一个元素开始,向前遍历
for (int i = n - 1; i > 0; i--) {
// 生成随机索引 j,范围 [0, i](包含i)
int j = random.nextInt(i + 1);
// 交换 shuffled[i] 和 shuffled[j]
swap(shuffled, i, j);
}
return shuffled;
}
/**
* 交换数组中两个位置的元素
*
* @param arr 数组
* @param i 第一个位置
* @param j 第二个位置
*/
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
算法分析
-
时间复杂度:
reset(): O(n) - 需要克隆数组shuffle(): O(n) - 需要遍历整个数组进行洗牌
-
空间复杂度:
- 构造函数: O(n) - 存储原始数组副本
reset()和shuffle(): O(n) - 返回新的数组副本
-
随机性:
- Fisher-Yates 算法确保每种排列的概率都是 1/n!
- 使用
Random.nextInt(i + 1)生成均匀分布的随机数
-
正确性:
- 不变性:
reset()总是返回原始数组 - 独立性:每次
shuffle()调用都是独立的,不受之前调用影响 - 完整性:所有元素都会被重新排列,不会丢失或重复
- 不变性:
算法过程
nums = [1, 2, 3] :
Fisher-Yates 洗牌过程:
初始: [1, 2, 3]
i = 2 (最后一个元素):
- 生成 j ∈ [0, 2],假设 j = 1
- 交换位置 2 和 1: [1, 3, 2]
i = 1 (倒数第二个元素):
- 生成 j ∈ [0, 1],假设 j = 0
- 交换位置 1 和 0: [3, 1, 2]
i = 0 (第一个元素):
- 循环结束(i > 0 条件不满足)
最终结果: [3, 1, 2]
测试用例
public static void main(String[] args) {
// 测试用例1:标准示例
int[] nums1 = {1, 2, 3};
Solution solution1 = new Solution(nums1);
System.out.println("Original: " + Arrays.toString(nums1));
System.out.println("Reset: " + Arrays.toString(solution1.reset()));
// 多次洗牌,结果不同
System.out.println("Shuffle 1: " + Arrays.toString(solution1.shuffle()));
System.out.println("Shuffle 2: " + Arrays.toString(solution1.shuffle()));
System.out.println("Shuffle 3: " + Arrays.toString(solution1.shuffle()));
// 测试用例2:单元素数组
int[] nums2 = {42};
Solution solution2 = new Solution(nums2);
System.out.println("Original: " + Arrays.toString(nums2));
System.out.println("Shuffle: " + Arrays.toString(solution2.shuffle()));
System.out.println("Reset: " + Arrays.toString(solution2.reset()));
// 测试用例3:两个元素
int[] nums3 = {10, 20};
Solution solution3 = new Solution(nums3);
System.out.println("Original: " + Arrays.toString(nums3));
System.out.println("Shuffle 1: " + Arrays.toString(solution3.shuffle()));
System.out.println("Shuffle 2: " + Arrays.toString(solution3.shuffle()));
// 测试用例4:包含负数
int[] nums4 = {-1, 0, 1, 2};
Solution solution4 = new Solution(nums4);
System.out.println("Original: " + Arrays.toString(nums4));
System.out.println("Shuffle: " + Arrays.toString(solution4.shuffle()));
// 测试用例5:reset
int[] nums5 = {1, 2, 3, 4, 5};
Solution solution5 = new Solution(nums5);
int[] reset1 = solution5.reset();
int[] shuffle1 = solution5.shuffle();
int[] reset2 = solution5.reset();
}
关键点
-
Fisher-Yates 算法:
- 从后往前遍历(
i = n-1到i = 1) - 随机索引范围是
[0, i],包含i - 使用
random.nextInt(i + 1)生成正确范围的随机数
- 从后往前遍历(
-
数组克隆:
- 构造函数中克隆原始数组,防止外部修改影响
reset()和shuffle()返回新数组
-
随机数生成器:
- 使用
java.util.Random而不是Math.random() Random提供更好的随机性和性能
- 使用
-
边界情况处理:
- 单元素数组:洗牌后仍然是自身
-
时间复杂度:
- 每次洗牌恰好执行 n-1 次交换
常见问题
-
为什么不能从前向后洗牌?
- 从前向后会导致某些排列的概率更高
- Fisher-Yates 必须从后向前才能保证均匀分布
-
随机索引为什么包含 i 本身?
- 允许元素保持在原位置,这是合法的排列
- 如果排除 i,会减少可能的排列数量
-
为什么不直接用 Collections.shuffle()?
Collections.shuffle()内部也是使用 Fisher-Yates
612

被折叠的 条评论
为什么被折叠?



