跳到主要内容

洗牌算法

如何等概率地打乱数组?​

  • 目标: n 个位置中, 每个元素落到任意位置的概率相同;
  • 从最后一个位置开始倒序遍历, 把位置 i 与 [0, i] 中的随机下标交换;
  • 交换后位置 i 的元素不再参与后续抽取;
function shuffle(nums: number[]): number[] {
for (let i = nums.length - 1; i >= 0; i--) {
const index = Math.floor(Math.random() * (i + 1));
[nums[i], nums[index]] = [nums[index], nums[i]];
}
return nums;
}

为什么每轮的随机范围必须缩小?​

  • 第 i 轮在 [0, i] 中随机, 保证位置 i 取到剩余任一元素的概率都是 1/(i+1);
  • 若每轮都在 [0, n-1] 中随机, 分布会不均匀;
  • 也可正序遍历, 让位置 i 与 [i, n-1] 中的随机下标交换, 效果相同;

如何在打乱的同时支持重置?​

  • 用两个数组: origin 保存初始状态, state 保存当前状态;
  • reset: 由 origin 复制回 state;
  • shuffle: 只修改 state;
class Solution {
private origin: number[];
private state: number[];

constructor(nums: number[]) {
this.origin = [...nums];
this.state = [...nums];
}

reset(): number[] {
this.state = [...this.origin];
return this.state;
}

shuffle(): number[] {
for (let i = this.state.length - 1; i >= 0; i--) {
const index = Math.floor(Math.random() * (i + 1));
[this.state[i], this.state[index]] = [this.state[index], this.state[i]];
}
return this.state;
}
}

洗牌算法的复杂度是多少?​

  • 时间复杂度: O(n), 每轮一次交换;
  • 空间复杂度: O(n), origin 与 state 各一份;