洗牌算法,近年来已经逐渐成为互联网公司面试题中常见的一类,今天我们就来聊一聊洗牌算法中经典且简单的一种 - Fisher–Yates shuffle Algorithm。
给定一个数组,编写一个程序来生成数组元素的随机排列。这个问题也被称为“洗牌”或“随机化给定的数组”。这里的 shuffle 意味着数组元素的每一种可能的排列都应该有相同的发生概率。
设给定的数组为arr[]。一个简单的解决方案是创建一个辅助数组temp[],它最初是arr[]的副本。从temp[]中随机选择一个元素,将随机选择的元素复制到arr[0],然后从temp[]中删除选择的元素。重复相同的过程 n 次并继续将元素复制到arr[1]、arr[2]、...。该解决方案的时间复杂度为 O(n^2)。
上述的方法虽然简单且容易理解,但O(n^2)的时间复杂度显然不是我们想要的。
Fisher–Yates洗牌算法的工作时间复杂度为 O(n)。我们需要做一个假设,那就是我们有一个随机函数 rand(),它可以在 O(1)的时间内生成一个随机数。算法的思想是从数组的最后一个元素开始,并将其与整个数组(包括最后一个)中随机选择的元素交换,之后在 0 到 n-2 的数组(大小减 1),重复之前的操作直到我们到达第一个元素。
具体算法如下
图解
算法原理
第 i 个元素(包括最后一个)到达最后一个位置的概率是 1/n,因为我们在第一次迭代中随机选择了一个元素。
将第 i 个元素分为两种情况,可以证明第 i 个元素到达倒数第二个位置的概率也为 1/n。
1:i = n-1(最后一个元素的索引):
最后一个元素到达倒数第二个位置的概率是=(最后一个元素不停留在其原始位置的概率)x(上一个索引选择的概率再次选择 step 以便交换最后一个元素)
所以概率 = ((n-1)/n) x (1/(n-1)) = 1/n
情况 2:0 < i < n-1 (非最后一个索引):
第 i 个元素到达倒数第二个位置的概率 = (第 i 个元素在前一次迭代中未被选中的概率) x (第 i 个元素在本次迭代中被选中的概率)
所以概率 = ((n-1)/n) x (1 /(n-1)) = 1/n
我们可以很容易地将上述证明推广到任何其他位置。
注意:该算法是建立在每个位置的元素被选中的概率都为1/n的假设上的,也就是rand()抽中每个元素的概率必须相等
Fisher–Yates shuffle Algorithm的Python实现
from random import randint
def randomize (arr, n):
for i in range(n-1,0,-1):
# Pick a random index from 0 to i
j = randint(0,i+1)
# Swap arr[i] with the element at random index
arr[i],arr[j] = arr[j],arr[i]
return arr
arr = [1, 2, 3, 4, 5, 6, 7, 8]
n = len(arr)
print(randomize(arr, n))
其他语言实现下载链接:
(包含各种语言:C、Python、Java、C++、C#、PHP等均有示例)
免费资源下载:Fisher–Yates