问题描述
我试图通过通用的Java shuffle纸牌算法来更好地理解这段代码:
// Random for remaining positions.
int r = i + rand.nextInt(52 - i);
为什么必须“填充”或将i
索引添加到结果随机数中?看起来像在迭代并添加i
时,通过减去i
,可以使随机数的最大可能范围保持在0到51之间,但是为什么不这样做:
int r = rand.nextInt(52);
完整代码:
// Function which shuffle and print the array
public static void shuffle(int card[],int n)
{
Random rand = new Random();
for (int i = 0; i < n; i++)
{
// Random for remaining positions.
int r = i + rand.nextInt(52 - i);
//swapping the elements
int temp = card[r];
card[r] = card[i];
card[i] = temp;
}
}
解决方法
Fisher–Yates shuffle的工作方式如下:
- 从数组中获取一个随机元素,并将其交换到第一位
- 从剩余的 值中抽取一个随机元素,并将其交换到第二位
- 从剩余的 值中抽取一个随机元素,并将其交换到第三位
- 等等
这是您要询问的“ 剩余个值”部分。
例如经过10次迭代后,您已将10个随机值交换到数组的前10个位置中,因此对于下一次迭代,您需要在10端范围内的随机位置,因此与10个随机范围的偏移量比全范围小10个,也称为i + rand.nextInt(52 - i)
。
其他答案未解决您的“ 为什么不这样做:
int r = rand.nextInt(52);
”。这有时被称为“天真洗牌”,答案是因为这会导致有偏的洗牌。
理想情况下,您希望结果的数量能够反映同样可能的结果所涉及的概率计算。这意味着洗牌后的第一张牌可以是52张牌中的任何一张,第二张可以是剩余的51张牌中的任意一张,第三张可以是剩余的50张牌中的任意一张,...换句话说,有A = 52*51*50*...*3*2*1
(即52个阶乘)卡的可能排列。这就是Fisher-Yates改组及其变体所做的事情。但是,如果您按照建议的方式选择任何一张卡进行第i 次迭代,则会生成C = 52 52 个案例。结果是有偏差的洗牌。
为说明天真混洗中偏见的产生方式和原因,请考虑使用3张卡片的小得多的牌组。在这种情况下,A = {3*2*1
= 6个排列,而C = 3 3 = 27是到达最终排列的路径数。为什么这是个问题?由于鸽子洞原理。 C不是A的整数倍,因此,如果我们将C视为鸽子,而将A视为鸽子洞,则某些 洞必须比其他洞得到更多的鸽子。用改组的术语来说,达成某些协议的途径比其他途径多。因此,并非所有安排都会同样频繁地发生,因此结果不是“公平”的洗牌。
如果您进行分析得出结论并从初始化为['a','b','c']
的数组开始,则使用朴素的随机播放时,您会发现以下几率:
outcome probability
------- -----------
['a','c'] 4/27
['a','c','b'] 5/27
['b','a','c'] 5/27
['b','a'] 5/27
['c','b'] 4/27
['c','a'] 4/27
在无偏斜混洗的情况下,这6个可能排列的每个排列的概率为1/6。这就是创建Fisher-Yates算法的原因。
,您的代码所做的是创建卡座的混洗。在每次迭代中,它会随机抽取一张卡片,并将其放回牌组的前面,从i = 0到i = n。
如果您使用过int r = rand.nextInt(52);
,则意味着在每次迭代中您都可以拿回任何牌,即使是在牌组开始处的牌也已经是牌组新顺序的一部分。
通过减去i,您只选择剩余的52-i张卡中的一张,然后需要加+ i来获得它的实际位置,因为您已经在卡组的开始处设置了第一张i卡作为新的洗牌。
例如,假设您已经在新位置获得了前10张卡。现在,您需要获取第11张卡,以便从剩下的52-10 = 42张卡中随机抽取一张。假设我们取回了数字5,它不是card[4]
上的卡,而是card[10+4]