传送门
题目:打乱一个没有重复元素的数组。
示例:
// 以数字集合 1, 2 和 3 初始化数组。
int[] nums = {1,2,3};
Solution solution = new Solution(nums);
// 打乱数组 [1,2,3] 并返回结果。任何 [1,2,3]的排列返回的概率应该相同。
solution.shuffle();
// 重设数组到它的初始状态[1,2,3]。
solution.reset();
// 随机返回数组[1,2,3]打乱后的结果。
solution.shuffle();
一、使用:随机乱置算法(洗牌算法)。
最常用的洗牌算法。因为该算法的细节容易出错,且存在好几种变体,虽有细微差异但都是正确的,
所以这里只介绍一种简单的。
此类算法都是靠随机选取元素交换来获取随机性,直接看代码(伪码)
得到一个在在左开右闭区间 [min, max) 内的随机整数
int randNum(int min, int max)
void shuffle(int[] arr) {
int n = arr.length();
for (int i = 0 ; i < n; i++) {
int rand = randNum(i, n); // 从 i 到最后随机选一个元素
swap(arr[i], arr[rand]);
}
}
分析洗牌算法正确性的准则:产生的结果必须有 n! 种可能,否则就是错误的。
这个很好解释,因为一个长度为 n 的数组的全排列就有 n! 种,也就是说打乱结果总共有 n! 种。
洗牌算法 每次生成随机的左边界是i,每次都进1
i=0: 第1次选择 有 n次选择(0到n-1)
i=1: 第2次选择 有n-1次选择(1到n-1)
i=2: 第3次选择 有n-2次选择(2到n-1)…… 显然是: n!
所以下面这种做法是错的, 因为左边界一直没变
跟i交换的索引一直是n种选择 所以有n*n*n…n (n的n次方)种
错误的索引生成方法:int rand = randInt(0, n); // 每次都从区间 [0, n)中随机选取元素进行交换
二、蒙特卡罗方法验证正确性
洗牌算法,正确性衡量标准是:
对于每种可能的结果出现的概率必须相等,也就是说要足够随机。
如果不用数学严格证明概率相等,可以用蒙特卡罗方法近似地估计出概率是否相等,结果是否足够随机。
我们可以对同一个数组进行一百万次洗牌,统计各种结果出现的次数,把频率作为概率,可以很容易看出洗牌算法是否正确。整体思想很简单,不过实现起来也有些技巧的:
每次进行洗牌算法后,就把得到的打乱结果对应的频数加一,重复进行 100 万次,如果每种结果出现的总次数差不多,那就说明每种结果出现的概率应该是相等的。作为一种验证方法,我们不需要 n 太大,一般用长度为 5 或 6 的 arr 试下就差不多了: 这个思路的伪代码:
void shuffle(int[] arr);
// 蒙特卡罗
int N = 1000000;
HashMap count; // 作为直方图
for (i = 0; i < N; i++) {
int[] arr = {1,2,3};
shuffle(arr);
// 此时 arr 已被打乱
count[arr] += 1;
}
for (int feq : count.values())
print(feq / N + " "); // 频率
------------------------------------------ AC代码 --------------------------------------
int[] copyArray;// 用来交换的拷贝数组
int[] originalArray;// 原始数组
R andom random = new Random();
public Solution(int[] nums) {
copyArray = new int[nums.length];
originalArray = new int[nums.length];
for (int i = 0; i < nums.length; ++i) {
copyArray[i] = originalArray[i] = nums[i];
}
}
public int[] reset() {
return originalArray;
}
public int[] shuffle() {
for (int i = 0; i < copyArray.length; ++i) {
int randIndex = getRandIndex(i, copyArray.length);
// 交换两个位置数字
int tmp = copyArray[i];
copyArray[i] = copyArray[randIndex];
copyArray[randIndex] = tmp;
}
return copyArray;
}
int getRandIndex(int min, int max) {// 左闭右开
// nextInt(n) 返回值num 0<=num<n 注意右边开区间
return random.nextInt(max - min) + min;
}