携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情
题目描述
给你一个整数数组 nums ,设计算法来打乱一个没有重复元素的数组。打乱后,数组的所有排列应该是 等可能 的。
实现 Solution class:
Solution(int[] nums)使用整数数组nums初始化对象int[] reset()重设数组到它的初始状态并返回int[] shuffle()返回数组随机打乱后的结果
解题思路
我们在刷面经时经常看到这么一种解法。
const shuffle = (nums) => {
return nums.sort(()=>Math.random() - 0.5);
}
为什么这段代码能做到让数组“乱序” 呢。
我们看看 sort 的用法:
- 如果 sort 的回调返回的数小于0,则 a 会排到 b 的前面。
- 如果 sort 的回调返回的数等于0,则 a 和 b 的位置不变。
- 如果 sort 的回调返回的数大于0,则 b 会排到 a 的前面。
更多关于Sort()请参考:Array.prototype.sort() - JavaScript | MDN (mozilla.org)
那么我们都知道 Math.random() 返回一个 [0, 1) 的数。也就是说这个数减去0.5,那么最终的值就存在于上述三个情况之一,从而达到乱序的效果。
为什么有的地方会提到通过 Math.random 方法打乱的数组并不是真正的随机呢?我们来看看通过这个方法打乱的数组中,每个元素出现的概率吧。
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const locationMap = new Array(10).fill(0);
const shuffle = (nums) => {
const lens = nums.length;
nums.sort(() => Math.random() - 0.5);
locationMap[nums[lens - 1] - 1]++;
}
for (let i = 0; i < 100000; ++i) {
shuffle(arr.slice());
}
console.log(locationMap);
log:
[
10199, 2629, 8036,
7046, 9260, 12724,
16780, 9567, 10967,
12792
]
我们可以看出元素在各个位置出现的概率差距有可能是巨大的。因此通过 Math.random 打乱的数组并不是真正的随机。
那么有什么办法可以做到元素在各个位置的出现的概率是一样或者近似的呢?
接下来就介绍一下我们的主角——洗牌算法。
洗牌算法的思路就是将一个数组分为 未排序区域 和 已排序区域,每次从未排序区域中 随机 出一个下标来,和未排序区域的最后一个元素 交换,使得该轮交换后,未排序区域元素减少一个,已排序区域元素增加一个,n 位元素大小的数组经过 n 轮交换后,就实现了元素各个位置出现概率相同的洗牌算法了。
来看看通过洗牌算法打乱的数组概率情况是怎么样的:
[
9822, 10245, 9932,
10132, 10036, 9979,
10042, 10046, 9868,
9898
]
可以发现元素在各个位置出现的概率不会有很大的波动,都是 近似 的。
题解
/**
* @param {number[]} nums
*/
var Solution = function(nums) {
this.cards = nums;
this.beforeCards = nums.slice();
};
/**
* @return {number[]}
*/
Solution.prototype.reset = function() {
this.cards = this.beforeCards.slice();
return this.cards;
};
/**
* @return {number[]}
*/
Solution.prototype.shuffle = function() {
const lens = this.cards.length;
for(let i=0; i<lens; ++i) {
const idx = Math.floor(Math.random() * (lens - i));
[this.cards[idx], this.cards[lens - i - 1]] = [this.cards[lens - i - 1], this.cards[idx]];
}
return this.cards;
};