每天学习一点算法 2026/01/05
题目:打乱数组
给你一个整数数组 nums ,设计算法来打乱一个没有重复元素的数组。打乱后,数组的所有排列应该是 等可能 的。
实现 Solution class:
Solution(int[] nums) 使用整数数组 nums 初始化对象
int[] reset() 重设数组到它的初始状态并返回
int[] shuffle() 返回数组随机打乱后的结果
这个题目其实就是洗牌算法,我们分析一下,最核心的问题就是如何保证打乱后数组的所有排列是 等可能 的。
如果我们要把数组中的元素一个一个的取出来放到打乱后的数组中,就需要保证每次取到每个元素的概率是相同的。
-
很容易就可以想到的一种方式就是,根据数组长度生成随机数取出元素,然后长度减一生成随机数取出元素,直到取出所有的元素。感觉上是每个元素每次被取出的概率是相等的,来验证一下:
-
第 1 个:
1 l e n \frac{1}{len} len1 -
第 2 个:
l e n − 1 l e n × 1 l e n − 1 = 1 l e n \frac{len - 1}{len} × \frac{1}{len - 1} = \frac{1}{len} lenlen−1×len−11=len1 -
第 n 个:
l e n − n − 1 l e n × 1 l e n − n − 1 = 1 l e n \frac{len - n - 1}{len} × \frac{1}{len - n - 1} = \frac{1}{len} lenlen−n−1×len−n−11=len1
ok,概率是一样的。
然后就是代码实现了
typescriptclass Solution { readonly source: number[] // 存放初始数据 constructor(nums: number[]) { this.source = nums } // 重设数组 reset(): number[] { return this.source } // 打乱数组 shuffle(): number[] { const nums = [...this.source] // 克隆源数据 const len = this.source.length // 循环打乱数组 for (let i = 0; i < len; i++) { const random = Math.floor(Math.random() * (len - i)) // 生成 0 ~ len - i - 1 的随机整数作为随机下标 const num = nums.splice(random, 1)[0] // 截取随机位置的元素 nums.push(num) // 将随机元素放到数组末尾,随机下标范围会减少不用担心会选到重复元素 } return nums } } /** * Your Solution object will be instantiated and called as such: * var obj = new Solution(nums) * var param_1 = obj.reset() * var param_2 = obj.shuffle() */ -
-
我们看到上面循环打乱时,循环到最后一次时只有一个元素了,这个随机似乎没啥用了,有什么方法可以直接避免这种情况呢?
我们只遍历下标
0到下标len - 2的元素,每次生成[i, len - 1]范围的随机位置的元素跟i交换,这样依然可以保证每个数字打乱在每个位置上的概率一致,其实本质上跟上面方法的思想是一致的typescript// 打乱数组 const shuffle: () => number[] = () => { const nums = [...this.source] // 克隆源数据 const len = this.source.length // 循环打乱数组 for (let i = 0; i < len - 1; i++) { const random = i + Math.floor(Math.random() * (len - i)) // 生成 i ~ len - i - 1 的随机下标 ;[nums[i], nums[random]] = [nums[random], nums[i]] // 交换位置 } return nums }
题目来源:力扣(LeetCode)