为什么`array_diff_ukey`多次调用compare函数?

时间:2017-02-13 15:47:38

标签: php

我执行了以下代码,结果让我感到困惑!

我传递了两个数组和一个名为" myfunction"的函数。作为array_diff_ukey函数的参数。我看到 myfunction 被调用了13次(而它应该被调用最多9次)。更令人惊奇的是,它也比较了相同数组的键!在输出的两列中,我看到键" e",而只有第二个数组有它(对于其他一些键也是如此)。

function myfunction($a,$b) {
    echo $a . "   ".$b."<br>";
    if ($a===$b) {
        return 0;
    }
    return ($a>$b)?1:-1;
}

$a1=array("a"=>"green","b"=>"blue","c"=>"red");
$a2=array("d"=>"blue","e"=>"black","f"=>"blue");

$result=array_diff_ukey($a1,$a2,"myfunction");
print_r($result);

输出:

a   b
b   c
d   e
e   f
a   d
a   e
a   f
b   d
b   e
b   f
c   d
c   e
c   f
Array
(
    [a] => green
    [b] => blue
    [c] => red
)

eval.in上看到它。

为什么array_diff_ukey会对比较函数执行那么多不必要的调用?

2 个答案:

答案 0 :(得分:5)

好问题。实际上,实现的算法不是最有效的。

可以找到PHP数组函数的C源代码githubarray_diff_ukey的实施使用了C函数php_array_diffarray_udiffarray_diff_uassocarray_udiff_uassoc的实现也使用了该函数。{/ p>

正如你在那里看到的那样,该函数有这个C代码:

for (i = 0; i < arr_argc; i++) {
    //...
    zend_sort((void *) lists[i], hash->nNumOfElements,
              sizeof(Bucket), diff_key_compare_func, (swap_func_t)zend_hash_bucket_swap);
    //...
}

...这意味着每个输入数组都使用比较函数进行排序,解释了您获得的第一个输出系列,其中比较了相同数组的键,第一列可以列出除第一列以外的其他键阵列。

然后它在第一个数组的元素上有一个循环,在其他数组上有一个嵌套循环,并且 - 嵌套在其中 - 每个元素的元素上都有一个循环:

while (Z_TYPE(ptrs[0]->val) != IS_UNDEF) {
    //...
    for (i = 1; i < arr_argc; i++) {
        //...
        while (Z_TYPE(ptr->val) != IS_UNDEF && 
               (0 != (c = diff_key_compare_func(ptrs[0], ptr)))) {
            ptr++;
        }
        //...
    }
    //...
}

显然,对每个数组进行的排序实际上并没有对此算法中的任何内容做出贡献,因为仍然将第一个数组的所有键与其他数组中的所有键进行比较。 0 !=比较。因此算法 O(klogk + nm),其中 n 是第一个数组的大小, m 是大小的总和其他数组, k 是最大数组的大小。通常, nm 术语将是最重要的。

人们只能猜到为什么选择这种效率低下的算法,但看起来主要原因是代码可重用性:如上所述,这个C代码也被其他PHP函数使用,它可能更有意义。不过,这听起来并不是一个好借口。

PHP中这个(低效)array_diff_ukey算法的简单实现(不包括所有类型检查,边界条件等)可能看起来像这个mimic_array_diff_ukey函数:

function mimic_array_diff_ukey(...$args) {
    $key_compare_func = array_pop($args);
    foreach ($args as $arr) uksort($arr, $key_compare_func);
    $first = array_shift($args);
    return array_filter($first, function ($key) use($key_compare_func, $args) {
        foreach ($args as $arr) {
            foreach ($arr as $otherkey => $othervalue) {
                if ($key_compare_func($key, $otherkey) == 0) return false;
            }
        }
        return true;
    }, ARRAY_FILTER_USE_KEY);
}

一个更有效的算法使用排序,但是也可以从中受益并逐步通过第一个数组的键,同时逐步通过其他数组的键进行升序顺序,顺序 - 永远不必退后一步。这将使算法 O(nlogn + mlogm + n + m) = O(nlogn + mlogm)

以下是PHP中改进算法的可能实现:

function better_array_diff_ukey(...$args) {
    $key_compare_func = array_pop($args);
    $first = array_shift($args);
    $rest = [];
    foreach ($args as $arr) $rest = $rest + $arr;
    $rest = array_keys($rest);
    uksort($first, $key_compare_func);
    usort($rest, $key_compare_func);
    $i = 0;
    return array_filter($first, function ($key) use($key_compare_func, $rest, &$i) {
        while ($i < count($rest) && ($cmp = $key_compare_func($rest[$i], $key)) < 0) $i++;
        return $i >= count($rest) || $cmp > 0;
    }, ARRAY_FILTER_USE_KEY);
}

当然,如果采用这种算法来改进array_diff_ukey并且获得公平的运行时比较,则需要在C中实现该算法。

通过{{3}上的三个函数(array_diff_ukeymimic_array_diff_ukeybetter_array_diff_ukey)查看与您的问题略有不同的输入所做的比较}。

答案 1 :(得分:2)

array_diff_ukey分两个阶段运行:

  1. 对数组键进行排序
  2. 按键比较
  3. 这可能解释了为什么回调应该返回一个排序值而不是一个布尔值&#34;等于&#34;。

    我希望这可能是出于性能原因而做的,但如果是这样的话,我会认为可以用它来说'#34;这个键比其他数组中的所有键都大,所以我不应该费心去测试这些其他更大的键是否也更大,因为它们必须是&#34;,但似乎并非如此:它无论如何都要尽职尽责地对它们进行比较。

    我只能假设它是因为函数不能证明自己是确定性的(事实上在这种情况下会产生副作用)所以它不能像那样进行优化。也许array_diff_key(没有用户定义的函数)可以很好地进行这种优化。

    但无论如何,这就是幕后发生的事情,以及为什么你看到的不仅仅是9次比较。它可能会在核心中变得更好......