java.util.Random真的是随机的吗?

Is java.util.Random really that random? How can I generate 52! (factorial) possible sequences?

我一直在用Random (java.util.Random)来洗牌52张牌。有52个!(8.0658175E+67)可能性。然而,我已经发现,java.util.Random的种子是long,比2^64(1.8446744e+19)小得多。

从这里,我怀疑java.util.Random是否真的是那么随机;它是否真的能够生成全部52个呢?可能性?

如果不是,我怎么能可靠地生成一个更好的随机序列,可以生成全部52个!可能性?


选择一个随机排列比你的问题意味着更多和更少的随机性。让我解释一下。

坏消息是:需要更多的随机性。

你的方法的根本缺陷是它试图使用64位熵(随机种子)在~2226种可能性之间进行选择。要在~2226种可能性之间做出公平的选择,你必须找到一种方法来产生226位的熵,而不是64位的熵。

有几种生成随机位的方法:专用硬件、CPU指令、操作系统接口、在线服务。在你的问题中已经有了一个隐含的假设,你可以以某种方式产生64比特,所以只要做你想做的任何事情,只做四次,然后把多余的比特捐给慈善机构。:)

好消息是:需要更少的随机性。

一旦你有了226个随机位,剩下的就可以确定地完成,因此可以使java.util.Random的属性不相关。以下是方法。

假设我们生成全部52个!排列(请听我说)并按字典顺序排序。

要选择一个排列,我们只需要在052!-1之间选择一个随机整数。这个整数就是我们的226位熵。我们将使用它作为排序排列列表的索引。如果随机指数是均匀分布的,那么不仅保证所有排列都能被选择,而且它们也将被等量地选择(这比问题所问的问题更有力地保证)。

现在,您实际上不需要生成所有这些排列。您可以直接生成一个,考虑到它在我们的假设排序列表中随机选择的位置。这可以使用Lehmer[1]代码在O(n2)时间内完成(另请参见编号排列和阶乘编号系统)。这里的n是你甲板的大小,即52。

在这个stackoverflow答案中有一个C实现。对于N=52,有几个整数变量会溢出,但幸运的是,在爪哇中,可以使用EDCOX1×3。其余的计算几乎可以转录为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static int[] shuffle(int n, BigInteger random_index) {
    int[] perm = new int[n];
    BigInteger[] fact = new BigInteger[n];
    fact[0] = BigInteger.ONE;
    for (int k = 1; k < n; ++k) {
        fact[k] = fact[k - 1].multiply(BigInteger.valueOf(k));
    }

    // compute factorial code
    for (int k = 0; k < n; ++k) {
        BigInteger[] divmod = random_index.divideAndRemainder(fact[n - 1 - k]);
        perm[k] = divmod[0].intValue();
        random_index = divmod[1];
    }

    // readjust values to obtain the permutation
    // start from the end and check if preceding values are lower
    for (int k = n - 1; k > 0; --k) {
        for (int j = k - 1; j >= 0; --j) {
            if (perm[j] <= perm[k]) {
                perm[k]++;
            }
        }
    }

    return perm;
}

public static void main (String[] args) {
    System.out.printf("%s
"
, Arrays.toString(
        shuffle(52, new BigInteger(
           "7890123456789012345678901234567890123456789012345678901234567890"))));
}

[1]不要与莱勒混淆。:)


你的分析是正确的:伪随机数发生器注入到与任何序列特异性种子必须是the same洗牌后,限the number of that to obtain你可以置换264。This is to verify experimentally assertion容易通过电话Collection.shuffle模式两次,在Random对象initialized with the same that the双J ^ Al和种子,identical shuffles是随机的。P></

在这一解决方案,然后,使用随机数发生器is to that for a空气日期2010年1月17 allows种子。"这可能是SecureRandomJava类提供了initialized virtually无限阵列与byte[]of size。然后你可以安审通SecureRandomto to the Collections.shuffle学院全面工作。P></

1
2
3
byte seed[] = new byte[...];
Random rnd = new SecureRandom(seed);
Collections.shuffle(deck, rnd);


通用伪随机数产生器中,choose from(PRNG)不要求在置换52 item list of a length is less than if(226位了。P></

西安java.util.Random算法与modulus implements of length is its 248只;thus了48位多,我知道的所有比特less than the 226。You will need to another PRNG的使用已与大长& mdash;专门开发周期,一个与52或大的综合大学。P></

"我也看到shuffling article number"的在线随机发电机组。P></

consideration this is of the Nature of the PRNG独立;applies equally to cryptographic和伪随机数发生器(OF)的noncryptographic发生器是不恰当的信息,当noncryptographic security is involved)。P></

尽管allows种子java.security.SecureRandom无限长度在passed to be of the implementation,能安下SecureRandom使用PRNG(例如,"sha1prng"或"drbg")。恩,prng'在线和depends周期(S和extent to a较小,无论是capable of length)52选择从置换的综合要求。(注我"被定义"最大长度"as the size of the can take to初始化PRNG种子在其被压缩,或没有shortening种子")。P></


让我提前道歉,因为这有点难理解…

首先,你已经知道java.util.Random并不是完全随机的。它从种子中以完全可预测的方式生成序列。您完全正确,因为种子只有64位长,所以它只能生成2^64个不同的序列。如果您要以某种方式生成64个真正的随机位,并使用它们来选择一个种子,那么您就不能使用该种子在52个种子中进行随机选择!概率相等的可能序列。

但是,只要您实际生成的序列不超过2^64个,只要它可以生成的2^64个序列没有"特殊"或"明显特殊"的话,这个事实就没有任何意义。

假设您有一个更好的prng,使用1000位种子。假设您有两种方法来初始化它——一种方法是使用整个种子对其进行初始化,另一种方法是在初始化之前将种子散列到64位。

如果你不知道哪个初始值设定项是哪个,你能写任何测试来区分它们吗?除非你足够幸运地用相同的64位初始化了坏的初始值,否则答案是否定的。如果你不知道具体的prng实现中的一些弱点,你就无法区分这两个初始值。

或者,假设Random类有一个由2^64个序列组成的数组,这些序列在遥远的过去的某个时间被完全随机选择,并且种子只是这个数组的一个索引。

因此,事实上,Random只使用64位作为其种子并不一定是一个统计上的问题,只要您不太可能使用相同的种子两次。

当然,出于加密目的,64位种子是不够的,因为让系统使用相同的种子两次是计算上可行的。

编辑:

我要补充的是,尽管上述所有内容都是正确的,但java.util.Random的实际实现并不令人敬畏。如果你正在写一个纸牌游戏,可以使用MessageDigestAPI生成"MyGameName"+System.currentTimeMillis()的sha-256散列,并使用这些位来洗牌。根据上述论点,只要你的用户不是真正的赌博者,你就不必担心currentTimeMillis会返回很长时间。如果你的用户真的在赌博,那么使用没有种子的SecureRandom


我要在这个问题上采取不同的策略。你的假设是正确的-你的prng不能达到全部52个!可能性。

问题是:你的纸牌游戏的规模是多少?

如果你在做一个简单的克朗代克风格的游戏?那你肯定不需要全部52个!可能性。相反,这样看:一个玩家将有18个五分之一的不同游戏。即使考虑到"生日问题",他们也必须在遇到第一个重复游戏之前玩数十亿只手。

如果你在做蒙特卡洛模拟?那你可能没事了。你可能需要处理由于prng中的"p"而产生的工件,但是你可能不会仅仅因为种子空间太小而遇到问题(同样,你也会看到无数种独特的可能性)。另一方面,如果你正在处理大量的迭代,那么,是的,你的种子空间太小可能会破坏交易。

如果你在玩多人纸牌游戏,特别是在网上有钱的情况下?然后你需要谷歌搜索一下在线扑克网站是如何处理你所问的问题的。因为对于普通玩家来说,低种子空间的问题并不明显,但是如果值得花时间投资的话,它是可以利用的。(扑克网站都经历了一个阶段,他们的prng被"黑客",让某人看到所有其他玩家的洞牌,只需从暴露的牌中推断种子。)如果这是你所处的情况,不要简单地找到一个更好的prng-你需要把它作为一个加密问题认真对待。


与dasbinklenlight基本相同的简短解决方案:

1
2
3
4
5
6
// Java 7
SecureRandom random = new SecureRandom();
// Java 8
SecureRandom random = SecureRandom.getInstanceStrong();

Collections.shuffle(deck, random);

你不需要担心内部状态。详细解释原因:

通过这种方式创建SecureRandom实例时,它访问特定于操作系统的真随机数发生器。这要么是一个熵池,其中的值是包含随机位的存取(例如,对于纳秒计时器,纳秒精度基本上是随机的)或内部硬件编号生成器。

此输入!!)它可能仍然包含虚假的痕迹加密的强散列,用于删除这些跟踪。这就是使用这些CSPRNgs的原因,而不是为了自己创建这些数字!SecureRandom有一个计数器,跟踪使用了多少位(getBytes()getLong()等),必要时用熵位重新填充SecureRandom

简而言之:简单地忽略反对意见,使用SecureRandom作为真正的随机数生成器。


如果你只是考虑as the number of an位阵列(或字节),那么也许你可以用the Random.nextBytes(安全)问题解决方案在这suggested栈溢出,然后进入new BigInteger(byte[])地图数组。P></


一个非常简单的算法是将sha-256应用于从0向上递增的整数序列。(如果想要"得到不同的序列",可以添加一个盐)。)如果我们假设sha-256的输出是"和"0到2256-1之间的均匀分布整数一样好,那么我们就有足够的熵来完成任务。

要从sha256的输出中得到排列(当表示为整数时),只需将其模化为52、51、50…在这个伪代码中:

1
2
3
4
5
6
7
8
9
10
deck = [0..52]
shuffled = []
r = SHA256(i)

while deck.size > 0:
    pick = r % deck.size
    r = floor(r / deck.size)

    shuffled.append(deck[pick])
    delete deck[pick]