有效地选择随机数

Posted

技术标签:

【中文标题】有效地选择随机数【英文标题】:Choosing random numbers efficiently 【发布时间】:2011-02-01 04:23:37 【问题描述】:

我有一个方法,它使用随机样本来近似计算。这种方法被调用了数百万次,因此选择随机数的过程是否高效非常重要。

我不确定 javas Random().nextInt 到底有多快,但我的程序似乎并没有像我希望的那样受益。

在选择随机数时,我执行以下操作(半伪代码):

// Repeat this 300000 times
Set set = new Set();
while(set.length != 5)
    set.add(randomNumber(MIN,MAX));

现在,这显然有一个糟糕的最坏情况运行时间,因为理论上随机函数可以永远添加重复的数字,从而永远停留在 while 循环中。但是,这些数字是从 0..45 中选择的,因此在大多数情况下不太可能出现重复值。

当我使用上述方法时,它只比我的其他方法快 40%,这不是近似的,但会产生正确的结果。这运行了大约 100 万次,所以我希望这种新方法至少快 50%。

您对更快的方法有什么建议吗?或者,也许您知道生成一组随机数的更有效方法。

为了澄清,这里有两种方法:

// Run through all combinations (1 million). This takes 5 seconds
 for(int c1 = 0; c1 < deck.length; c1++)
    for(int c2 = c1+1; c2 < deck.length; c2++)
     for(int c3 = c2+1; c3 < deck.length; c3++)
        for(int c4 = c3+1; c4 < deck.length; c4++)
         for(int c5 = c4+1; c5 < deck.length; c5++)
             enumeration(hands, cards, deck, c1, c2, c3, c4, c5);
         
             
           
   
   

// Approximate (300000 combinations). This takes 3 seconds
Random rand = new Random();
HashSet<Integer> set = new HashSet<Integer>();
int[] numbers = new int[5];
while(enumerations < 300000)
set.clear();
while(set.size() != 5)
    set.add(rand.nextInt(deck.length));

Iterator<Integer> i = set.iterator();
int n = 0;
while(i.hasNext())
    numbers[n] = i.next();
    n++;

经过一些测试和分析,我发现这种方法是最有效的:

Random rand = new Random();
int[] numbers = new int[5];
ArrayList<Integer> list = new ArrayList<Integer>();
while(enumerations < 300000)
 while(list.size() != 5) 
     int i = rand.nextInt(deck.length);
        if(!list.contains(i)) list.add(i);
 
 int index = 0;
 for(int i : list) numbers[index] = i; index++; 
 enumeration(hands, cards, deck,numbers);

【问题讨论】:

您能重述您想要完成的任务吗?您是否尝试为每个方法调用生成一组 N 个不同的数字?您谈到将此方法与另一种“不近似”进行比较,而另一种方法更快 - 是真正的问题随机数生成还是您进行其他计算的方法(近似与非近似)? 问题是随机数生成。其他计算不相关,这就是为什么我没有在我的问题中提到它们。 【参考方案1】:

不要尝试开发您已知的随机数生成器。使用已知的 SecureRandom 代替:

http://www.owasp.org/index.php/Using_the_Java_Cryptographic_Extensions

【讨论】:

【参考方案2】:

如果您因必须跳过重复项而放慢速度,您可以通过创建所有卡片值的列表来解决该问题,然后在选择卡片时从列表中删除并选择随机下一次在较小的范围内编号。像这样的:

// Assuming we're just numbering all the cards 0 to 51. This could be more sophisticated, of course.
ArrayList cards=new ArrayList(52);
for (int x=0;x<52;++x)
  cards=new Integer(x);

Integer[] hand=new Integer[5];
for (int h=0;h<5;++h)

  // Pick a card from those remaining
  int n=random.nextInt(cards.size());
  hand[h]=cards.get(n);
  // Remove the picked card from the list
  cards.remove(n);

对于第一次抽奖,cards.get(n) 将返回 n,无论 n 是什么。但从那时起,值将被删除,因此 cards.get(3) 可能会返回 7,等等。

创建列表并从中删除会增加大量开销。我的猜测是,如果您一次只选择 5 张卡片,那么发生冲突的概率就足够小,以至于在找到重复项后消除它们比防止它们更快。即使在最后一次抽签中,重复的概率也只有 4/52=1/13,所以你很少会遇到重复,并且连续 2 次抽签都是重复的概率很小。这完全取决于生成随机数所需的时间与设置数组和执行删除所需的时间相比。最简单的判断方法是做一些实验和测量。 (或个人资料!)

【讨论】:

和我想的完全一样——重复的概率是如此之小,以至于防止它们所花费的时间比仅仅检查它们所花费的时间要长。我已经用我的结果更新了 OP。【参考方案3】:

看起来您想从集合 S 中选择一个 k-combination 而无需替换,其中 S 具有 n 个不同的值,k = 5 和 n = 52。您可以shuffle() 整个集合并选择 k 个元素(正如@Tesserex 建议的那样),或pick() k 元素,同时避免重复(如您所示)。您需要在您的特定环境和您选择的生成器中进行概要分析。我经常(但并非总是)看到pick() 的适度优势。

private static final Random rnd = new Random();
private static final int N = 52;
private static final int K = 5;
private static final List<Integer> S = new ArrayList<Integer>(N);
static 
    for (int i = 0; i < N; i++) 
        S.add(i + 1);
    

private final List<Integer> combination = new ArrayList<Integer>(K);

...

private void shuffle() 
    Collections.shuffle(S, rnd);
    combination.addAll(S.subList(0, K));


private void pick() 
    for (int i = 0; i < K; i++) 
        int v = 0;
        do 
            v = rnd.nextInt(N) + 1;
         while (combination.contains(v));
        combination.add(v);
    

【讨论】:

【参考方案4】:

我对您的实际问题没有任何意见,而且我对 Java 了解不多(只是四处寻找)。然而,在我看来,您正在尝试为扑克构建一个手部评估器,并且这个线程 http://pokerai.org/pf3/viewtopic.php?f=3&t=16 包含一些非常快速的 java 手部评估器。希望其中一些代码可以有所帮助。

【讨论】:

我实际上受到了这个线程中的一些算法的启发。不过,我正在实现一个 omaha 评估器,这个线程中的很多东西,比如抬头查找表,我都无法使用。【参考方案5】:

您可以使用线性同余作为随机生成器:http://en.wikipedia.org/wiki/Linear_congruential_generator [但要考虑它们的统计缺点]

您只需要为每个数字计算 (x + c) % m。然而,根据我的经验,创建对象(就像每次调用 new Set 和 add 时可能会做的那样,具体取决于您使用的实现)可能会比调用 nextInt() 花费更多的速度。也许您应该尝试使用探查器,例如这个:http://www.eclipse.org/tptp/

【讨论】:

我正在运行 os x,所以我不能使用 eclipse tptp 分析器!我真的很想念一个分析器! 我曾经在 Mac OS X 上使用过 JProfiler。Afaik 他们有 14 天的免费试用期。【参考方案6】:

永远不要猜测,永远衡量。

 long time = System.getCurrentMilliseconds();
 Random().nextInt()
 System.out.println(System.getCurrentMilliseconds() - time);

此外,您永远不应该依赖已知错误发生的频率有多低,只需编写代码防御即可。检测重复,如果是重复则不要添加,并使用continue 语句跳过迭代。

至于最快的方法和随机数... 您无法在 Java 的 Math.random() 中获取随机数。你只能得到伪随机数。您希望这有多快是牺牲了您对它们的看似随机性的表现。生成伪随机数的最快方法是基于 System.getCurrentMilliSeconds() 等种子值的位移和加法此外,伪随机数生成已经非常快,因为它只是原始 CPU 算术,所以你会一旦您看到使用Math.random() 生成一个需要多少毫秒,您可能会很高兴。

【讨论】:

@Yuval:如果你不测量,你不知道它什么时候足够快。分析通常是侵入性的。您应该测量 and 配置文件...尽管您当然不应该像这样测量 single 调用。 Math.random() 与 Random().nextInt() 相比有多快?我目前正在使用 Random 类。 @Frederik:Math.random() 在内部使用 Random.nextDouble(),所以它更慢,如果有的话。【参考方案7】:

您可以尝试使用existing Java implementation (or this one) 作为Mersenne Twister

请记住,大多数 MT 是加密安全的。

【讨论】:

您能否澄清一下,您所说的加密不安全是什么意思? 这意味着你不应该将它们用于加密,因为在给定一定数量的先验信息的情况下仍然可以预测下一个数字。【参考方案8】:

一种常见的技术是从所有可能输入的列表开始,然后从中随机选择,然后删除。这样一来,就没有选择重复项和必须循环未知时间的风险。当然,这种方法只适用于离散数据,但幸运的是整数。还请记住,如果可能,您的列表(或其他数据结构)选择和删除应该是 O(1),因为您关注的是速度。

【讨论】:

如果应用程序是它看起来的样子(扑克赔率计算器),那么有 52C5 == 2598960 个可能的输入,因此将使用少于 1/6 的输入。这是非常低效的内存使用,因为输入样本(在典型的扑克赔率计算器中)在评估后不需要保留在内存中。如果评估功能扩展到 7 张牌(52C7 == 133784560 种组合),情况可能会更糟 是的,你是对的 - 不幸的是,当我写答案时,实际方法的附加信息不在问题中。

以上是关于有效地选择随机数的主要内容,如果未能解决你的问题,请参考以下文章

从长(且合理)稀疏向量中选择随机元素的最有效方法是啥?

如何在大数据帧的每组中有效地随机标记行?

从包中选择随机元组

有效地从 lua 表中删除 nil 值

有效地使用多个 Numpy 切片进行随机图像裁剪

如何在 Python 中有效地生成具有随机斜率和截距的直线?