ChatGPT解决这个技术问题 Extra ChatGPT

PHP 函数的 Big-O 列表

使用 PHP 一段时间后,我注意到并非所有内置的 PHP 函数都像预期的那样快。考虑使用缓存的素数数组来查找数字是否为素数的函数的这两种可能实现。

//very slow for large $prime_array
$prime_array = array( 2, 3, 5, 7, 11, 13, .... 104729, ... );
$result_array = array();
foreach( $prime_array => $number ) {
    $result_array[$number] = in_array( $number, $large_prime_array );
}

//speed is much less dependent on size of $prime_array, and runs much faster.
$prime_array => array( 2 => NULL, 3 => NULL, 5 => NULL, 7 => NULL,
                       11 => NULL, 13 => NULL, .... 104729 => NULL, ... );
foreach( $prime_array => $number ) {
    $result_array[$number] = array_key_exists( $number, $large_prime_array );
}

这是因为 in_array 是通过线性搜索 O(n) 实现的,它会随着 $prime_array 的增长而线性减慢。 array_key_exists 函数是通过哈希查找 O(1) 实现的,除非哈希表被大量填充(在这种情况下它只有 O(n)),否则它不会减慢速度。

到目前为止,我不得不通过反复试验来发现大 O,偶尔还会发现 looking at the source code。现在的问题...

是否有所有*内置 PHP 函数的理论(或实际)大 O 时间列表?

*或者至少是有趣的

例如,我发现很难预测列出的函数的大 O,因为可能的实现取决于 PHP 的未知核心数据结构:array_mergearray_merge_recursivearray_reversearray_intersectarray_combine、{ 6}(带有数组输入)等。

完全偏离主题,但 1 不是素数。
PHP 中的数组是哈希表。这应该告诉你你需要知道的一切。在哈希表中搜索一个键是 O(1)。搜索一个值是 O(n) - 你无法在未排序的集合上击败它。您好奇的大多数函数可能都是 O(n)。当然,如果你真的想知道,可以阅读源码:cvs.php.net/viewvc.cgi/php-src/ext/standard/…
作为记录,您尝试做的最快实现是(而不是使用 NULL 作为您的值)使用 true,然后使用 isset($large_prime_array[$number]) 测试是否存在。如果我没记错的话,它比 in_array 函数快数百倍。
大 O 符号与速度无关。这是关于限制行为。
@Kendall 我不是在比较 array_key_exists,我是在比较 in_arrayin_array 迭代数组中的每个项目,并将值与传递给它的针进行比较。如果您将值翻转到键(并且只需将每个值替换为像 true 这样的虚拟值,使用 isset 会快很多倍。这是因为数组的键由 PHP 索引(如hashtable). 因此,以这种方式搜索数组可以显着提高速度。

D
Derek Pollard

因为在我认为将它放在某个地方供参考之前似乎没有人这样做过。我已经通过基准测试或代码略读来描述 array_* 函数。我试图将更有趣的 Big-O 放在顶部附近。此列表不完整。

注意:假设哈希查找计算的所有 Big-O 都是 O(1),即使它实际上是 O(n)。 n 的系数是如此之低,存储足够大的数组的内存开销会在查找 Big-O 的特性开始生效之前伤害你。例如,在 N=1 和 N=1,000,000 时对 array_key_exists 的调用之间的差异是增加了约 50% 的时间。

有趣的点:

isset/array_key_exists 比 in_array 快得多,并且 array_search +(union) 比 array_merge 快一点(看起来更好)。但它的工作方式确实不同,所以请记住这一点。 shuffle 与 array_rand 在同一 Big-O 层上 array_pop/array_push 由于重新索引惩罚而比 array_shift/array_unshift 快

查找:

array_key_exists O(n) 但实际上接近 O(1) - 这是因为碰撞中的线性轮询,但因为碰撞的机会非常小,所以系数也非常小。我发现您将哈希查找视为 O(1) 以提供更真实的大 O。例如,N=1000 和 N=100000 之间的差异只有大约 50% 的减速。

isset( $array[$index] ) O(n) 但非常接近 O(1) - 它使用与 array_key_exists 相同的查找。由于它是语言结构,如果键是硬编码的,将缓存查找,从而在重复使用相同键的情况下加快速度。

in_array O(n) - 这是因为它对数组进行线性搜索,直到找到值。

array_search O(n) - 它使用与 in_array 相同的核心函数,但返回值。

队列功能:

array_push O(∑ var_i, 对于所有 i)

array_pop O(1)

array_shift O(n) - 它必须重新索引所有键

array_unshift O(n + ∑ var_i, for all i) - 它必须重新索引所有键

数组交集、并集、减法:

array_intersect_key 如果相交 100% 做 O(Max(param_i_size)*∑param_i_count, for all i), if相交 0% intersect O(∑param_i_size, for all i)

array_intersect 如果相交 100% 做 O(n^2*∑param_i_count, 对于所有 i),如果相交 0% 相交 O(n^2)

array_intersect_assoc 如果相交 100% 做 O(Max(param_i_size)*∑param_i_count, for all i), if相交 0% intersect O(∑param_i_size, for all i)

array_diff O(π param_i_size, for all i) - 这是所有 param_sizes 的乘积

array_diff_key O(∑ param_i_size, for i != 1) - 这是因为我们不需要遍历第一个数组。

array_merge O( ∑ array_i, i != 1 ) - 不需要遍历第一个数组

+ (union) O(n),其中 n 是第二个数组的大小(即 array_first + array_second) - 比 array_merge 更少的开销,因为它不必重新编号

array_replace O( ∑ array_i, 对于所有 i )

随机的:

shuffle O(n)

array_rand O(n) - 需要线性轮询。

明显的大O:

array_fill O(n)

array_fill_keys O(n)

range O(n)

array_splice O(偏移 + 长度)

array_slice O(offset + length) 或 O(n) 如果长度 = NULL

array_keys O(n)

array_values O(n)

array_reverse O(n)

array_pad O(pad_size)

array_flip O(n)

array_sum O(n)

array_product O(n)

array_reduce O(n)

array_filter O(n)

array_map O(n)

array_chunk O(n)

array_combine O(n)

我要感谢 Eureqa 使查找函数的 Big-O 变得容易。这是一个了不起的免费程序,可以找到任意数据的最佳拟合函数。

编辑:

对于那些怀疑 PHP 数组查找是 O(N) 的人,我编写了一个基准来测试它(对于大多数实际值,它们仍然是有效的 O(1))。

https://i.stack.imgur.com/PgRQd.png

$tests = 1000000;
$max = 5000001;


for( $i = 1; $i <= $max; $i += 10000 ) {
    //create lookup array
    $array = array_fill( 0, $i, NULL );

    //build test indexes
    $test_indexes = array();
    for( $j = 0; $j < $tests; $j++ ) {
        $test_indexes[] = rand( 0, $i-1 );
    }

    //benchmark array lookups
    $start = microtime( TRUE );
    foreach( $test_indexes as $test_index ) {
        $value = $array[ $test_index ];
        unset( $value );
    }
    $stop = microtime( TRUE );
    unset( $array, $test_indexes, $test_index );

    printf( "%d,%1.15f\n", $i, $stop - $start ); //time per 1mil lookups
    unset( $stop, $start );
}

@肯德尔:谢谢!我做了一些阅读,结果发现 PHP 使用“嵌套”哈希表进行冲突。也就是说,它不是使用 logn 结构来处理冲突,而是使用另一个哈希表。而且我确实理解实际上说 PHP 哈希表提供 O(1) 性能,或者平均至少 O(1) - 这就是哈希表的用途。我只是好奇你为什么说它们“真的是 O(n)”而不是“真的是 O(logn)”。好的文章!
时间复杂度应该包含在文档中!选择正确的功能可以为您节省大量时间,或者告诉您避免做您计划做的事情:p 已经感谢您提供此列表!
我知道这是旧的......但是什么?该曲线根本没有显示 O(n),它显示了 O(log n),en.wikipedia.org/wiki/Logarithm。这与您对嵌套哈希映射的期望也是准确的。
数组元素上未设置的 Big-O 是什么?
虽然哈希表确实具有最坏情况的 O(n) 查找复杂度,但平均情况是 O(1),而您的基准测试的特定情况甚至可以保证 O(1),因为它是一个从零开始的连续数字索引数组,它永远不会有哈希冲突。您仍然看到对数组大小的依赖的原因与算法复杂性无关,它是由 CPU 缓存效应引起的。数组越大,随机访问查找越有可能导致缓存未命中(以及层次结构中更高的缓存未命中)。
D
Dathan

您具体描述的情况的解释是关联数组被实现为哈希表 - 所以按键查找(相应地,array_key_exists)是O(1)。但是,数组不是按值索引的,因此在一般情况下,发现数组中是否存在值的唯一方法是线性搜索。那里没有惊喜。

我认为没有关于 PHP 方法算法复杂性的具体综合文档。但是,如果这是一个足够大的问题,值得付出努力,您可以随时查看 the source code


这不是一个真正的答案。正如我在问题中所述,我已经尝试查看 PHP 源代码。由于 PHP 是用 C 语言编写的,使用了复杂的宏,因此有时很难“看到”函数的底层大 O。
@Kendall 我忽略了您对深入研究源代码的引用。但是,我的回复中有一个答案:“我认为没有关于 PHP 方法的算法复杂性的具体全面的文档。” “否”是一个完全有效的答案。 (C:
r
ryeguy

您几乎总是希望使用 isset 而不是 array_key_exists。我没有看内部,但我很确定 array_key_exists 是 O(N),因为它遍历数组的每个键,而 isset 尝试使用相同的哈希算法访问元素访问数组索引时使用。那应该是O(1)。

需要注意的一个“问题”是:

$search_array = array('first' => null, 'second' => 4);

// returns false
isset($search_array['first']);

// returns true
array_key_exists('first', $search_array);

我很好奇,所以我对差异进行了基准测试:

<?php

$bigArray = range(1,100000);

$iterations = 1000000;
$start = microtime(true);
while ($iterations--)
{
    isset($bigArray[50000]);
}

echo 'is_set:', microtime(true) - $start, ' seconds', '<br>';

$iterations = 1000000;
$start = microtime(true);
while ($iterations--)
{
    array_key_exists(50000, $bigArray);
}

echo 'array_key_exists:', microtime(true) - $start, ' seconds';
?>

is_set: 0.132308959961 秒
array_key_exists: 2.33202195168 秒

当然,这并没有显示时间复杂度,但它确实显示了 2 个函数如何相互比较。

要测试时间复杂度,请比较在第一个键和最后一个键上运行其中一个函数所需的时间。


这是错误的。我 100% 确定 array_key_exists 不必遍历每个键。如果您不相信,请查看下面的链接。 isset 速度如此之快的原因是它是一种语言结构。这意味着它没有进行函数调用的开销。另外,我认为它可能正在缓存查找,因此。此外,这不是问题的答案!我想要一个 PHP 函数的 Big(O) 列表(如问题所述)。我的例子没有一个基准。 svn.php.net/repository/php/php-src/branches/PHP_5_3/ext/…
如果你仍然不相信我,我已经创建了一个小基准来证明这一点。 pastebin.com/BdKpNvkE
您的基准测试有什么问题是您必须禁用 xdebug。 =)
为什么要使用 isset 而不是 array_key_exists 有两个关键原因。首先,isset 是一种降低函数调用成本的语言结构。这类似于 $arrray[] = $append vs array_push($array, $append) 参数。其次,array_key_exists 还区分非设置值和空值。对于 $a = array('fred' => null);array_key_exists('fred', $a) 将返回 true,而 isset($['fred']) 将返回 false。这个额外的步骤很重要,并且会大大增加执行时间。
J
Josh Stern

如果人们在实践中遇到关键冲突问题,他们会使用辅助哈希查找或平衡树来实现容器。平衡树将给出 O(log n) 最坏情况行为和 O(1) avg。大小写(哈希本身)。在内存应用程序的大多数实际应用中,开销是不值得的,但也许有些数据库将这种形式的混合策略实现为默认情况。


你能分享更多关于这个的细节吗?这与 PHP 有什么关系?