其他分享
首页 > 其他分享> > 【数学】力扣384:打乱数组

【数学】力扣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:暴力解法

  1. 首先考虑如何随机打乱一个数组
    设数组 nums的长度为 n。可以使用如下方法打乱:
    • 将数组中所有的数都放到数据结构 waiting 中,并初始化打乱后的数组 shuffle;
    • 循环 n 次,在第 i 次循环中(0≤i<n):
      • 在 waiting 中随机抽取一个数 num,将其作为打乱后的数组 shuffle 的第 i 个元素;
      • 从 waiting 中移除 num。

对于原数组 nums 中的数 num 来说,被移动到打乱后的数组的第 i 个位置的概率为:
image
因此,对于原数组 nums 中的任意一个数,被移动到打乱后的数组的任意一个位置的概率都是相同的。
2. 在算法的实现中考虑两个问题

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 个元素。

方法2:Fisher-Yates 洗牌算法
是方法1的优化,通过调整 waiting 的实现方式来减少shuffle阶段的时间复杂度。
原理是通过随机交换位置来实现随机打乱,有正向和反向两种写法,且实现非常方便。
注意这里“reset”函数以及类的构造函数的实现细节。
针对方法1的shuffle阶段,可以在移除 waiting 的第 k 个元素时,将第 k 个元素与数组的最后 1 个元素交换,然后移除交换后数组的最后 1 个元素,这样我们只需要 O(1) 的时间复杂度即可完成移除第 k 个元素的操作。此时,被移除的交换后数组的最后 1 个元素即为我们根据随机下标获取的元素。
在此基础上,也可以不移除最后 1 个元素,而直接将其作为乱序后的结果,并更新待乱序数组的长度,从而实现数组的原地乱序。因为实际上不再需要从数组中移除元素,所以也可以将第 k 个元素与第 1 个元素交换。
!!!所以思路是:
等概率选择每个位置应该填哪个数。

  1. 先在 0 ~ n-1 中随机选一个坐标,将它作为随机排序数组的第一个数,和原数组的第一个数交换位置(每个数被选到的概率是 $ \frac{1}{n} $);
  2. 剩下的 n-1 个数里,继续随机一个 1 ~ n-1 的坐标,将它作为随机排序数组的第二个数,和原数组的第二个数交换位置(每个数被选到的概率为第一次没被选到且第二次被选到 $ \frac{n-1}{n} * \frac{1}{n-1}=\frac{1}{n} $;
  3. 以此类推

因此,每个数填到每个位置是等概率的,都是$ \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