【数学】力扣384:打乱数组
作者:互联网
给你一个整数数组 nums ,设计算法来打乱一个没有重复元素的数组。打乱后,数组的所有排列应该是 等可能 的。
实现 Solution class:
Solution(int[] nums) 使用整数数组 nums 初始化对象
int[] reset() 重设数组到它的初始状态并返回
int[] shuffle() 返回数组随机打乱后的结果
示例:
输入
["Solution", "shuffle", "reset", "shuffle"]
[[[1, 2, 3]], [], [], []]
输出
[null, [3, 1, 2], [1, 2, 3], [1, 3, 2]]
解释
Solution solution = new Solution([1, 2, 3]);
solution.shuffle(); // 打乱数组 [1,2,3] 并返回结果。任何 [1,2,3]的排列返回的概率应该相同。例如,返回 [3, 1, 2]
solution.reset(); // 重设数组到它的初始状态 [1, 2, 3] 。返回 [1, 2, 3]
solution.shuffle(); // 随机返回数组 [1, 2, 3] 打乱后的结果。例如,返回 [1, 3, 2]
方法1:暴力解法
- 首先考虑如何随机打乱一个数组
设数组 nums的长度为 n。可以使用如下方法打乱:- 将数组中所有的数都放到数据结构 waiting 中,并初始化打乱后的数组 shuffle;
- 循环 n 次,在第 i 次循环中(0≤i<n):
- 在 waiting 中随机抽取一个数 num,将其作为打乱后的数组 shuffle 的第 i 个元素;
- 从 waiting 中移除 num。
对于原数组 nums 中的数 num 来说,被移动到打乱后的数组的第 i 个位置的概率为:
因此,对于原数组 nums 中的任意一个数,被移动到打乱后的数组的任意一个位置的概率都是相同的。
2. 在算法的实现中考虑两个问题
- 如何实现重设数组到它的初始状态?
使用 nums 来存储当前数组,并用 original 来存储数组的初始状态。在需要重设数组到它的初始状态时,只需要将 original 复制到 nums 并返回即可。 - 如何实现 waiting?
要求 waiting 既支持根据随机计算的下标获取元素,又支持根据该下标移除元素。可以使用数组来实现 waiting。
class Solution:
def __init__(self, nums: List[int]):
self.nums = nums # 存储当前数组
self.original = nums.copy() # 存储数组的初始状态
def reset(self) -> List[int]:
self.nums = self.original.copy()
return self.nums
def shuffle(self) -> List[int]:
n = len(self.nums)
shuffled = [0] * n # 初始化数组
for i in range(n):
index = random.randrange(n) # 随机生成数字index
shuffled[i] = self.nums.pop(index) # 提取数组nums中的元素index到数组shuffled中,并删除nums中的该元素,循环结束时shuffled即为将nums随机打乱顺序的结果
# self.nums = shuffled
return shuffled
# Your Solution object will be instantiated and called as such:
# obj = Solution(nums)
# param_1 = obj.reset()
# param_2 = obj.shuffle()
第三个函数的代码是错误的,提示pop index out of range。为什么索引会超出范围呢?index不就是在len(self.nums)范围里的吗?而 n = len(self.nums),哪里不对呢?
!!!问题就出在n = len(self.nums)上啦!定义 n 之后,n是一个定值,那么数组 shuffled 的长度是 n ,这没有问题;变量 i 表示shuffled 中的某一个值的索引,所以范围是range(n) = [0, n - 1],也没有问题。因此就只有index = random.randrange(n)
这个部分了。pop()
的原理是先提取再删除,不止pop一个的时候就是删除之后再继续提取、再删除……因此!在第二次循环时,pop应用的长度只有【n - 1】,而不是【n】,所以其实j的随机数范围应当是动态的len(self.nums),而非 n 这个定值。
class Solution:
def __init__(self, nums: List[int]):
self.nums = nums # 存储当前数组
self.original = nums.copy() # 存储数组的初始状态
def reset(self) -> List[int]:
self.nums = self.original.copy()
return self.nums
def shuffle(self) -> List[int]:
n = len(self.nums)
shuffled = [0] * n # 初始化数组
for i in range(n):
index = random.randrange(len(self.nums)) # 随机生成数字index作为索引,范围为[0, len(self.nums) - 1],这里的len(self.nums)是变化的
shuffled[i] = self.nums.pop(index) # 提取数组nums中的元素index到数组shuffled中,并删除nums中的该元素,循环结束时shuffled即为将nums随机打乱顺序的结果
# self.nums = shuffled
return shuffled
# Your Solution object will be instantiated and called as such:
# obj = Solution(nums)
# param_1 = obj.reset()
# param_2 = obj.shuffle()
时间复杂度:
- 初始化:O(n),其中 n 为数组中的元素数量。需要 O(n) 来初始化 original。
- reset:O(n)。需要 O(n) 将 original 复制到 nums。
- shuffle:O(n^2)。需要遍历 n 个元素,每个元素需要 O(n−k) 的时间从 nums 中移除第 k 个元素。
空间复杂度:O(n)。记录初始状态和临时的乱序数组均需要存储 n 个元素。
方法2:Fisher-Yates 洗牌算法
是方法1的优化,通过调整 waiting 的实现方式来减少shuffle阶段的时间复杂度。
原理是通过随机交换位置来实现随机打乱,有正向和反向两种写法,且实现非常方便。
注意这里“reset”函数以及类的构造函数的实现细节。
针对方法1的shuffle阶段,可以在移除 waiting 的第 k 个元素时,将第 k 个元素与数组的最后 1 个元素交换,然后移除交换后数组的最后 1 个元素,这样我们只需要 O(1) 的时间复杂度即可完成移除第 k 个元素的操作。此时,被移除的交换后数组的最后 1 个元素即为我们根据随机下标获取的元素。
在此基础上,也可以不移除最后 1 个元素,而直接将其作为乱序后的结果,并更新待乱序数组的长度,从而实现数组的原地乱序。因为实际上不再需要从数组中移除元素,所以也可以将第 k 个元素与第 1 个元素交换。
!!!所以思路是:
等概率选择每个位置应该填哪个数。
- 先在 0 ~ n-1 中随机选一个坐标,将它作为随机排序数组的第一个数,和原数组的第一个数交换位置(每个数被选到的概率是 $ \frac{1}{n} $);
- 剩下的 n-1 个数里,继续随机一个 1 ~ n-1 的坐标,将它作为随机排序数组的第二个数,和原数组的第二个数交换位置(每个数被选到的概率为第一次没被选到且第二次被选到 $ \frac{n-1}{n} * \frac{1}{n-1}=\frac{1}{n} $;
- 以此类推
因此,每个数填到每个位置是等概率的,都是$ \frac{1}{n} $。
class Solution:
def __init__(self, nums: List[int]):
self.nums = nums
def reset(self) -> List[int]:
return self.nums
def shuffle(self) -> List[int]:
# self.temp = self.nums.copy()
self.temp = list(self.nums) # 保存原数组,新建一个数组进行排序操作并返回。方法1是新建一个新数组保存原数组,对原数组直接进行操作。两种方法思路不同、方式相同、结果相同
for i in range(len(self.nums)):
index = random.randrange(i, len(self.nums)) # 随机生成索引值
self.temp[i], self.temp[index] = self.temp[index], self.temp[i] # 交换
return self.temp
# Your Solution object will be instantiated and called as such:
# obj = Solution(nums)
# param_1 = obj.reset()
# param_2 = obj.shuffle()
标签:index,shuffle,shuffled,nums,打乱,力扣,384,数组,self 来源: https://www.cnblogs.com/Jojo-L/p/16248900.html