【问题标题】:Efficiently pick n random elements from PHP array (without shuffle)有效地从 PHP 数组中挑选 n 个随机元素(无随机播放)
【发布时间】:2015-11-09 05:09:54
【问题描述】:

我有以下代码在 PHP 中从数组 $array 中选择 $n 元素:

shuffle($array);
$result = array_splice($array, 0, $n);

给定一个大数组,但只有几个元素(例如5 中的10000),这相对较慢,所以我想对其进行优化,以便不必对所有元素进行混洗。值必须是唯一的。

我正在寻找性能最高的替代方案。我们可以假设$array 没有重复并且是0-indexed。

【问题讨论】:

  • 也读过,但我有点担心array_flip 在大型阵列上的性能。
  • @FabianSchmengler 感谢博文和基准测试。我认为您应该编辑您的问题,以简要解释哪种解决方案(两个有争议的解决方案)最适合未来读者在哪种情况下使用。哦!并且,在您的博客文章中添加所有详细信息的链接。该页面已存档于Internet Archive

标签: php arrays performance random shuffle


【解决方案1】:
$randomArray = [];
while (count($randomArray) < 5) {
  $randomKey = mt_rand(0, count($array)-1);
  $randomArray[$randomKey] = $array[$randomKey];
}

这将提供恰好 5 个没有重复的元素,而且速度非常快。密钥将被保留。

注意:您必须确保 $array 有 5 个或更多元素,或者添加某种检查以防止无限循环。

【讨论】:

  • 我不得不说这个解决方案更好。
  • 随着 n 接近数组长度,我担心这会花费很长时间......在您选择它们​​之后,有没有一种快速的方法来重新索引它们?
  • @PaulS。这一切都取决于数组的大小。如果n 接近数组长度,那么 shuffle() 或其他类似的解决方案会更好。
  • 如果效率真的是个问题,你也可以缓存$array的长度(在while之外计算),而不是每次调用mt_rand函数时计算它。跨度>
  • 这将在输出数组中生成大间隙,而不是连续键(如$n 随机选取的元素),因为输出数组的大小应为$n ,但示例代码生成具有原始数组索引的数组,例如array(0=&gt;$a1, 100=&gt;$a2,..)
【解决方案2】:

此函数仅对$n 元素执行随机播放,其中$n 是您要选择的随机元素的数量。它也适用于关联数组和稀疏数组。 $array 是要处理的数组,$n 是要检索的随机元素的数量。

如果我们将$max_index 定义为count($array) - 1 - $iteration

它通过生成一个介于 0 和 $max_index 之间的随机数来工作。选择该索引处的键,并将其索引替换为 $max_index 处的值,这样它就永远不会被再次选择,因为 $max_index 将在下一次迭代中减少一个并且无法访问。

总结这是Richard Durstenfeld's Fisher-Yates shuffle,但只对$n元素而不是整个数组进行操作。

function rand_pluck($array, $n) {
    $array_keys = array_keys($array);
    $array_length = count($array_keys);
    $max_index = $array_length -1;
    $iterations = min($n, $array_length);
    $random_array = array();
    while($iterations--) {
        $index = mt_rand(0, $max_index);
        $value = $array_keys[$index];
        $array_keys[$index] = $array_keys[$max_index];
        array_push($random_array, $array[$value]);
        $max_index--;
    }
    return $random_array;
}

【讨论】:

  • 是的,shuffle 算法的变化是最好的(类似于我的答案),无论是在性能方面还是在统计方面,即无偏抽样,+1
  • 严格来说这个解决方案不是O(n),而是O(N),因为必须使用array_keys等等..当然比原来的@快987654336@ 解决方案且无偏见(因为它是shuffle 的变体),我的解决方案严格来说是O(n),但还有一些其他问题..
  • @NikosM。确实,但实际上array_keys 在大规模数组(数十万个元素)上速度非常快。区分时间复杂度和实际花费的时间很重要。虽然我不怀疑没有它你的方法可能会更快,但我认为在任何数组上工作的好处比每 100k 元素可能产生的 10 毫秒惩罚更重要。
  • 是的,我们似乎在这里进行了权衡,我正在考虑如何使用另一种变体来优化我发布的答案,否则您的答案似乎应该是最佳解决方案
【解决方案3】:

诀窍是使用shuffle 的变体,或者换句话说,部分洗牌。

性能不是唯一的标准,统计效率,即无偏抽样同样重要(就像原来的shuffle解决方案一样)

function random_pick( $a, $n ) 
{
  $N = count($a);
  $n = min($n, $N);
  $picked = array_fill(0, $n, 0); $backup = array_fill(0, $n, 0);
  // partially shuffle the array, and generate unbiased selection simultaneously
  // this is a variation on fisher-yates-knuth shuffle
  for ($i=0; $i<$n; $i++) // O(n) times
  { 
    $selected = mt_rand( 0, --$N ); // unbiased sampling N * N-1 * N-2 * .. * N-n+1
    $value = $a[ $selected ];
    $a[ $selected ] = $a[ $N ];
    $a[ $N ] = $value;
    $backup[ $i ] = $selected;
    $picked[ $i ] = $value;
  }
  // restore partially shuffled input array from backup
  // optional step, if needed it can be ignored, e.g $a is passed by value, hence copied
  for ($i=$n-1; $i>=0; $i--) // O(n) times
  { 
    $selected = $backup[ $i ];
    $value = $a[ $N ];
    $a[ $N ] = $a[ $selected ];
    $a[ $selected ] = $value;
    $N++;
  }
  return $picked;
}

注意该算法在时间和空间上都是严格的O(n),产生无偏选择(它是一个部分无偏改组)并产生输出是具有连续键的正确数组(不需要额外的array_values等)

使用示例:

$randomly_picked = random_pick($my_array, 5);
// or if an associative array is used
$randomly_picked_keys = random_pick(array_keys($my_array), 5);
$randomly_picked = array_intersect_key($my_array, array_flip($randomly_picked_keys));

关于 PHP 改组的更多变体和扩展:

  1. PHP - shuffle only part of an array
  2. PHP shuffle with seed
  3. How can I take n elements at random from a Perl array?

【讨论】:

  • 看来我们已经发布了相同算法的变体。 +1 出于您在我的回答中提到的原因。
  • 就像我在下面所说的,我的算法要快很多。这大约慢了 25 倍并且不再是随机的:sandbox.onlinephpfunctions.com/code/…
  • @Devon,玩弄测试用例,你会感到惊讶,这样做:注释掉我的代码的可选部分(涉及备份)并使用值 10、100、1000 的测试用例,尤其是10 你会很惊讶,而且我的代码在所有方面都有统一的性能;这些情况 and 是公正的(产生真正的组合)sandbox.onlinephpfunctions.com/code/…
  • 没有什么比你更公正的了。 mt_rand 使用统计上准确的随机分布。您非常关心执行 n 个循环,您并没有从逻辑上考虑在每个循环中必须进行多少处理。即使 100000 个元素中有 10000 个,我的速度也快 3 倍。除此之外,这个问题毫无意义。
  • @Devon,您将无偏洗牌与最佳随机数生成器混淆,这是不同的,您提交了测试用例,但您没有玩弄它们,也没有研究整体特征
【解决方案4】:

与数组洗牌相比,这只会显示小型 n 的好处,但您可以

  1. 选择一个随机索引rn次,每次减少限制1
  2. 针对以前使用的索引进行调整
  3. 看值
  4. 存储使用的索引

伪代码

arr = []
used = []
for i = 0..n-1:
    r = rand 0..len-i
    d = 0
    for j = 0..used.length-1:
        if r >= used[j]:
            d += 1
    arr.append($array[r + d])
    used.append(r)
return arr

【讨论】:

    【解决方案5】:

    您可以使用mt_rand() 生成n 次随机数,然后将这些值填充到新数组中。为了解决相同索引被返回两次的情况,我们使用实际返回的索引来填充新数组并始终检查索引是否存在于新数组中,如果存在,我们使用 while 循环遍历它,只要我们得到一个重复索引。最后我们使用array_values() 得到一个0索引的数组。

    $count = count($array) - 1;
    $new_array = array();
    for($i = 0; $i < $n; $i++) {
        $index = mt_rand(0, $count);
        while(isset($new_array[$index])) {
            $index = mt_rand(0, $count);
        }
    
        $new_array[$index] = $array[$index];
    }
    $new_array = array_values($new_array);
    

    【讨论】:

    • 如果 mt_rand 两次给你相同的索引怎么办?
    • @Endijs 范围为 10000 的可能性极小,但我们可以检查它是否已经返回,如果返回则重新生成。
    • 投了反对票,但再次阅读代码后发现是错误的,可以重新投票,如果进行了小修改,则投票被解锁
    • @NikosM。给你。
    猜你喜欢
    • 1970-01-01
    • 2012-01-27
    • 2013-11-26
    • 1970-01-01
    • 1970-01-01
    • 2010-09-08
    • 2016-07-16
    • 2016-01-28
    相关资源
    最近更新 更多