我看到这段代码来洗牌列表:
public static void shuffle(List<Integer> numbers) {
if(numbers == null || numbers.isEmpty()) return;
for(int i = 0; i < numbers.size(); ++i) {
int index = (int) (i + Math.random()*(numbers.size() - i));
swap(numbers, i, index);
}
}
代码似乎有效,但我不明白这个片段:
int index = (int) (i + Math.random()*(numbers.size() - i));
基本上它是i + R*(n-i)
但这如何确保:i)我们不会获得越界索引或ii)我不会更改相同的元素,即index == i
洗牌不会那么随机吗?
Math.random()
在区间[0, 1)
中返回一个统一的随机数,理想情况下,numbers.size() - i
将该数字缩放到区间[0, numbers.size() - i)
。 例如,如果i
为 2,列表大小为 5,则在理想情况下,以这种方式选择区间[0, 3)
中的随机数。 最后,i
被添加到数字中,(int)
铸造丢弃数字的小数部分。 因此,在此示例中,[2, 5)
中的随机整数(即 2、3 或 4)是随机生成的,因此在每次迭代中,索引 X 处的数字都会与自身或它后面的数字交换。
但是,这里有一个重要的微妙之处。 由于浮点数的性质和缩放数字时的舍入误差,在极少数情况下,。 舍入误差会导致习语Math.random()*(numbers.size() - i)
的输出可能等于numbers.size() - i
,即使Math.random()
输出的数字不包括 1Math.random()*(numbers.size() - i)
使某些结果偏向于其他结果。 例如,当 2^53 不能被numbers.size() - i
整除时,就会发生这种情况,因为Math.random()
在引擎盖下使用java.util.Random
,并且它的算法生成精度为 53 位的数字。 因此,Math.random()
不是编写此代码的最佳方式,并且代码可以使用专门用于生成随机整数的方法(例如java.util.Random
的nextInt
方法)。另请参阅此问题和此问题。
编辑:事实证明,Math.random() * integer
成语不会产生它可能返回integer
的问题,至少当integer
是任何正int
并且四舍五入到最接近的舍入模式使用Java时。 看到这个问题。
Math.random() 总是返回一个介于 0(含)和 1(不包括)之间的浮点数。因此,当您执行Math.random()*(numbers.size() - i)
时,结果将始终介于 0(含)和n-i
(不包括)之间。
然后你在i + Math.random()*(numbers.size() - i)
中添加 i 到它。
现在,如您所见,结果将介于 i(含)和 n(不含)之间。
之后,您将将其转换为整数。当你将双精度转换为 int 时,你会截断它,所以现在索引的值将从 ''ito
n - 1'''(包括两者)。
因此,您将不会有 ArrayIndexOutOfBoundsException,因为它总是至少比数组的大小小 1。
但是,索引的值可以等于 i,所以是的,你是对的,因为一个数字可以与自身交换并保持在那里。这完全没问题。
-
您有一个包含 1 到 50 个整数的列表。
-
因此,获取一个从 0 到 49 的随机值(包括 0 到 49)来索引它。 假设是30。
-
获取索引 30 处的项目。
-
现在将索引 30 中的项目替换为索引 49 中的项目。
-
下次生成一个介于 0 和 48 之间的数字(包括 0 和 48)。 永远不会达到 49,那里的数字占用最后一个使用的号码的插槽。
-
继续此过程,直到用尽列表。
注意:表达式(int)(Math.random() * n)
将生成一个介于0
和n-1
之间的随机数,因为Math.random
生成一个介于0
和1
排除之间的数字。
与其使用这样的自定义方法,我建议您使用 OOTB Collections.shuffle。检查此项以了解为Collections.shuffle
实现的逻辑。
代码分析:
Math.random() 返回一个带有正号、大于或等于0.0
且小于1.0
的double
值。
现在,让我们假设numbers.size() = 5
并试运行for
循环:
When i = 0, index = (int) (0 + Math.random()*(5 - 0)) = (int) (0 + 4.x) = 4
When i = 1, index = (int) (1 + Math.random()*(5 - 1)) = (int) (1 + 3.x) = 4
When i = 2, index = (int) (2 + Math.random()*(5 - 2)) = (int) (2 + 2.x) = 4
When i = 3, index = (int) (3 + Math.random()*(5 - 3)) = (int) (3 + 1.x) = 4
When i = 4, index = (int) (4 + Math.random()*(5 - 4)) = (int) (4 + 0.x) = 4
如您所见,当numbers.size() = 5
时,index
的值将在每次迭代中保持4
。
您的查询:
这如何确保:i)我们不会获得越界索引
正如上面已经解释的那样,使用试运行,它永远不会越界。
或 ii) 我不会更改相同的元素,即索引 == i 和 洗牌不会那么随机吗?
swap(numbers, i, index);
在索引处交换元素,i
与索引处的元素交换,4
每次numbers.size() = 5
时。以下示例对此进行了说明:
假设numbers
= [1, 2, 3, 4, 5]
When i = 0, numbers will become [5, 2, 3, 4, 1]
When i = 1, numbers will become [5, 1, 3, 4, 2]
When i = 2, numbers will become [5, 1, 2, 4, 3]
When i = 3, numbers will become [5, 1, 2, 3, 4]
When i = 4, numbers will become [5, 1, 2, 3, 4]
int index = (int) (i + Math.random()*(numbers.size() - i));
- 重要的是要注意 Math.random() 将生成一个属于 <0 的数字;所以它永远不会超过边界,因为排除最大值将是:i + 1*(number.size() -i) = number.size- 这一点是有效的,它可能发生。