为什么这个数字生成模式如此频繁?

时间:2015-10-26 02:48:13

标签: php unit-testing random

我只是在PHP闲逛,我决定使用PHP_INT_MIN( - 9223372036854775808)和PHP_INT_MAX(9223372036854775807)生成一些随机数字。我只是echo编辑了以下内容:

echo rand(-9223372036854775808, 9223372036854775807);

我不断刷新以查看生成的数字并查看数字的随机性,结果我开始注意到一种模式正在出现。每出2-4次刷新0就会出现,这种情况一定会发生,在一个阶段,我甚至连0都连续出现4次。

我想进一步尝试,所以我创建了以下代码段:

<?php
$countedZero = 0;
$totalGen = 250;

for ($i = 1; $i <= $totalGen; $i++) {
    $rand = rand(-9223372036854775808, 9223372036854775807);

    if ($rand == 0) {
        echo $i . ": <font color='red'>" . $rand . "</font><br/>";

        $countedZero++;
    } else {
        echo $i . ": " . $rand . "<br/>";
    }
}
echo "0 was generated " . $countedZero . "/" . $totalGen . " times which is " . (($countedZero / $totalGen) * 100) . "%."
?>
这会让我清楚地了解发电率是多少。我跑了8次测试:

  • 前3个测试使用的是$totalGen 250个。(总共3次测试)。

  • 第二次3次测试使用的是$totalGen 1000次。(共计6次测试)。

  • 第三个测试只是为了看看结果会对更大的数字产生什么影响,我选择了10,000。 (总共7次测试​​)。

  • 第四次测试是最后的测试,我对此感兴趣,因为最后一次(大量)测试得到了如此高的结果,所以我提高了赌注并将$totalGen设置为500,000。 (第8次测试总计)。

结果

我截取了结果截图。我拿了第一个输出,我没有继续测试它试图让它适合某种模式:

测试1(250)

(1)。

(2)。

(3)。

测试2(1000)

(1)。

(2)。

(3)。

测试3(10,000)

(1)。

测试4(500,000)

(1)。

根据上述结果,可以安全地假设即使可能数的范围达到最大值,0也很有可能出现。所以我的问题是:

为什么会发生这种情况有合理的理由吗?

考虑可以选择多少个数字为什么0是一个重复的数字?

注意测试8最初将是1,000,000,但是它已经非常糟糕,所以如果有人可以测试1,000,000并通过编辑OP来显示结果,我会将其减少到500,000,这将非常感激。

修改1

根据@ maiorano84的要求,我使用了mt_rand代替rand,结果就是这些。

测试1(250)

(1)。

(2)。

(3)。

测试2(1000)

(1)。

(2)。

(3)。

测试3(10,000)

(1)。

测试4(500,000)

(1)。

您可以看到的结果显示0仍然很有可能出现。同样使用函数rand提供了最低的结果。

更新

似乎在PHP7使用新功能random_int时,它解决了问题。

示例 PHP7 random_int

https://3v4l.org/76aEH

2 个答案:

答案 0 :(得分:4)

这基本上是某人如何编写错误的rand()函数的示例。当您在min中指定max / rand()范围时,您会遇到PHP的一部分内容,这会导致PRNG中的分发不完善。

在php-src中具体为lines 44-45 of php_rand.h,这是以下宏:

#define RAND_RANGE(__n, __min, __max, __tmax) \
    (__n) = (__min) + (zend_long) ((double) ( (double) (__max) - (__min) + 1.0) * ((__n) / ((__tmax) + 1.0)))

从较高的调用堆栈(lines 300-302 in rand.c of php-src):

if (argc == 2) {
    RAND_RANGE(number, min, max, PHP_RAND_MAX);
}

RAND_RANGE是上面定义的宏。只需拨打rand()而不是rand(-9223372036854775808, 9223372036854775807)即可删除范围参数,您将再次获得均匀分布。

这是一个演示效果的脚本......

function unevenRandDist() {

    $r = [];
    for ($i = 0; $i < 10000; $i++) {
            $n = rand(-9223372036854775808,9223372036854775807);
            if (isset($r[$n])) {
                    $r[$n]++;
            } else {
                    $r[$n] = 1;
            }
    }
    arsort($r);
    // you should see 0 well above average in the top 10 here
    var_dump(array_slice($r, 0, 10));

}

function evenRandDist() {

    $r = [];
    for ($i = 0; $i < 10000; $i++) {
            $n = rand();
            if (isset($r[$n])) {
                    $r[$n]++;
            } else {
                    $r[$n] = 1;
            }
    }
    arsort($r);
    // you should see the top 10 are about identical
    var_dump(array_slice($r, 0, 10)); //

}

unevenRandDist();
evenRandDist();

我得到的样本输出

array(10) {
  [0]=>
  int(5005)
  [1]=>
  int(1)
  [2]=>
  int(1)
  [3]=>
  int(1)
  [4]=>
  int(1)
  [5]=>
  int(1)
  [6]=>
  int(1)
  [7]=>
  int(1)
  [8]=>
  int(1)
  [9]=>
  int(1)
}
array(10) {
  [0]=>
  int(1)
  [1]=>
  int(1)
  [2]=>
  int(1)
  [3]=>
  int(1)
  [4]=>
  int(1)
  [5]=>
  int(1)
  [6]=>
  int(1)
  [7]=>
  int(1)
  [8]=>
  int(1)
  [9]=>
  int(1)
}

请注意,在第一个数组和第二个数组中出现0次的次数存在过分差异。即使从技术上讲,它们都会在PHP_INT_MINPHP_INT_MAX的同一确切范围内生成随机数。

我想你可以为此归咎于PHP,但重要的是要注意glibc rand并不知道生成好的随机数(而不管加密)。 This problem is known in glibc's implementation of rand as pointed out by this SO answer

答案 1 :(得分:3)

我快速查看了您的脚本并通过命令行运行它。我注意到的第一件事是因为我运行的是32位版本的PHP,我的整数最小值和最大值与你的不同。

因为我使用的是原始值,所以我实际上是100%的时间。我通过修改脚本解决了这个问题:

$countedZero = 0;
$totalGen = 1000000;

for ($i = 1; $i <= $totalGen; $i++) {
    $rand = rand(~PHP_INT_MAX, PHP_INT_MAX);

    if ($rand === 0) {
        //echo $i . ": <font color='red'>" . $rand . "</font><br/>";

        $countedZero++;
    } else {
        //echo $i . ": " . $rand . "<br/>";
    }
}
echo "0 was generated " . $countedZero . "/" . $totalGen . " times which is " . (($countedZero / $totalGen) * 100) . "%.";

我能够确认每次测试只会产生50%的命中率。

但这是有趣的部分:

$rand = rand(~PHP_INT_MAX+1, PHP_INT_MAX-1);

将范围改变为这些值会导致零的可能性下降到平均0.003%(8次测试后)。奇怪的是,在检查了零的$ rand的值之后,我看到很多值为1,并且有很多随机的负数。没有出现大于1的正数。

将范围更改为以下后,我能够看到一致的行为和更多随机化:

$rand = rand(~PHP_INT_MAX/2, PHP_INT_MAX/2);

以下是我非常确定的事情:

因为你在这里处理一个范围,你必须考虑最小值和最大值之间的差异,以及PHP是否可以支持该值。

在我的情况下,PHP能够支持的最小值是-2147483648,最大值2147483647,但它们之间的差异实际上最终为4294967295 - 比PHP可以存储的数字大得多,因此它按顺序截断最大值试图管理这个价值。

最终,如果您的最小值和最大值之差超过了PHP_INT_MAX常量,那么您将看到意外行为。