目录 打乱数组 暴力法 官方代码 复杂度分析 Fisher-Yates 洗牌算法 官方代码 复杂度分析 0/1 发生器 打乱数组 给你一个整数数组 nums ,设计算法来打乱一个没有重复元素的数组,支持两个操作,reset
和shuffle
分别表示将数组到它的初始状态并返回,以及将数组随机打乱
官方题解 给出了两种解法:暴力法以及 Fisher-Yates 洗牌算法
暴力法 其中暴力法就是通过调用系统的Random.nextInt()
API 来获得随机抽取到的元素(不放回采样),将所有元素遍历一遍后,随机化完成
官方代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution { int [] nums; int [] original; public Solution (int [] nums) { this .nums = nums; this .original = new int [nums.length]; System.arraycopy(nums, 0 , original, 0 , nums.length); } public int [] reset() { System.arraycopy(original, 0 , nums, 0 , nums.length); return nums; } public int [] shuffle() { int [] shuffled = new int [nums.length]; List<Integer> list = new ArrayList <Integer>(); for (int i = 0 ; i < nums.length; ++i) { list.add(nums[i]); } Random random = new Random (); for (int i = 0 ; i < nums.length; ++i) { int j = random.nextInt(list.size()); shuffled[i] = list.remove(j); } System.arraycopy(shuffled, 0 , nums, 0 , nums.length); return nums; } }
复杂度分析 时间复杂度:
初始化:$O(n)$,其中 n 为数组中的元素数量。我们需要 $O(n)$ 来初始化原本数组,方便后续重置操作 reset()
:$O(n)$,需要 $O(n)$ 进行数组的拷贝shuffle()
:$O(n^2)$,需要遍历 $n$ 个元素,每个元素需要 $O(n-k)$ 的时间从 nums 中移除第 $k$ 个元素空间复杂度:${O(n)}$,记录初始状态和临时的乱序数组均需要存储 $n$ 个元素
Fisher-Yates 洗牌算法 上述算法的复杂度在于,每次随机化采样之后,需要删除原本位于 k 位置的元素,随后在新的数组进行重新随机采样,可以通过优化这种删除步骤做到提高时间复杂度的方法。比如,每次将删除的元素与数组的最后一个元素(第一个元素也可以)交换,随后重新在除去最后一个元素(第一个元素)的子数组重新采样,也可以达到最终效果,最终甚至做到了就地乱序
⛵具体的,实现算法如下(设需要原地乱序的数组为 nums ):
循环 n 次(数组长度为 $n$)在第 $i$ 次循环中($0 \le i < n$): 在 $[i,n)$ 中随机抽取一个下标 $j$ 将第 $i$ 个元素与第 $j$ 元素交换 如果完成 $n$ 次采样结束,否则返回第 1 步 官方代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution { int [] nums; int [] original; public Solution (int [] nums) { this .nums = nums; this .original = new int [nums.length]; System.arraycopy(nums, 0 , original, 0 , nums.length); } public int [] reset() { System.arraycopy(original, 0 , nums, 0 , nums.length); return nums; } public int [] shuffle() { Random random = new Random (); for (int i = 0 ; i < nums.length; ++i) { int j = i + random.nextInt(nums.length - i); int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } return nums; } }
复杂度分析 时间复杂度:
初始化:$O(n)$,其中 n 为数组中的元素数量。我们需要 $O(n)$ 来初始化原本数组,方便后续重置操作 reset()
:$O(n)$,需要 $O(n)$ 进行数组的拷贝shuffle()
:$O(n)$,只需要遍历 $n$ 个元素即可完成就地乱序空间复杂度:${O(n)}$,记录初始状态需要存储 $n$ 个元素
0/1 发生器 已有方法 rand7 可生成 1 到 7 范围内的均匀随机整数,试写一个方法 rand10 生成 1 到 10 范围内的均匀随机整数
简单粗暴的方法就是:将任意已知的随机数发生器,变为 0~1 发生器,随后通过 0~1 发生器产生出任意的随机数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public int rand10 () { int res = 0 ; do { res = (zeroOne()<<3 )+(zeroOne()<<2 )+(zeroOne()<<1 )+(zeroOne()); }while (res>=10 ); return res+1 ; } public int zeroOne () { int temp = 0 ; do { temp = rand7(); }while (temp==7 ); return temp>3 ?0 :1 ; }
也可以使用力扣官方的题解,使用拒绝采样的方法,每次随机是相互独立的,具体如下图: