【问题标题】:PHP Arrays - Remove duplicates ( Time complexity )PHP 数组 - 删除重复项(时间复杂度)
【发布时间】:2009-01-25 18:05:36
【问题描述】:

好的,这不是“如何获取所有唯一性”或“如何在 php 中从我的数组中删除重复项”的问题。这是一个关于时间复杂度的问题。

我认为array_unique 有点 O(n^2 - n),这是我的实现:

function array_unique2($array) 
{ 
    $to_return = array(); 
    $current_index = 0;

    for ( $i = 0 ; $i < count($array); $i++ ) 
    { 
        $current_is_unique = true; 

        for ( $a = $i+1; $a < count($array); $a++ ) 
        { 
            if ( $array[$i] == $array[$a] ) 
            { 
                $current_is_unique = false; 
                break; 
            } 
        } 
        if ( $current_is_unique ) 
        { 
            $to_return[$current_index] = $array[$i];
        } 

    } 

    return $to_return; 
}

但是,当针对 array_unique 进行基准测试时,我得到了以下结果:

正在测试 (array_unique2)... 操作耗时 0.52146291732788 秒。

正在测试 (array_unique)... 操作耗时 0.28323101997375 秒。

这使 array_unique 速度提高了一倍,我的问题是,为什么(两者都有相同的随机数据)?

我的一个朋友写道:

function array_unique2($a)
{
    $n = array();
    foreach ($a as $k=>$v)
        if (!in_array($v,$n))
            $n[$k]=$v;
    return $n;
}

比php内置的快一倍。

我想知道,为什么?

array_unique 和 in_array 的时间复杂度是多少?

编辑 我从两个循环中删除了 count($array) 并在函数顶部使用了一个变量,它在 100 000 个元素上增加了 2 秒!

【问题讨论】:

  • 如果您关心的是高效执行,PHP 真的是一个不错的选择吗?
  • 哈哈,别猜了,但这不是重点。我一般不使用PHP。但是我在玩的时候发现这很有趣。
  • 你的第一个函数不正确:array_unique2(array(1, 2)) 只是返回 array(2)。
  • 哦,做到了.. 我测试了它:$array_with_multiple = array("Filip", "Jenny", "Filip", "Tarzan");
  • 你在基准测试中表现如何?

标签: php algorithm time-complexity


【解决方案1】:

虽然我不能代表原生 array_unique 函数,但我可以告诉你,你的朋友算法更快,因为:

  1. 他使用单个 foreach 循环,而不是您的双 for() 循环。
  2. 在 PHP 中,Foreach 循环的执行速度往往比 for 循环快。
  3. 他使用了一个 if(!) 比较,而您使用了两个 if() 结构
  4. 您朋友调用的唯一附加函数是 in_array,而您调用了 count() 两次。
  5. 你做了三个你朋友不需要的变量声明($a、$current_is_unique、$current_index)

虽然这些因素都不是很大,但我可以看到累积效应会使您的算法比您的朋友花费更长的时间。

【讨论】:

  • 实际上,count() 被调用了很多次——每个循环每次循环迭代一次。
  • 谢谢,通过从这两个地方删除 count(),我在 100 000 个元素上获得了 2 秒的时间。
  • 您的第 1 点无效:in_array() 似乎也在循环遍历条目,因此在 Filip 朋友的代码中也有一个隐藏的 for 循环!
  • @Christoph:你能提供一个参考吗?我会非常有兴趣自己阅读。
  • 在我的测试中,“朋友”函数在大型数组情况下表现不佳。
【解决方案2】:

in_array() 的时间复杂度是O(n)。要了解这一点,我们将查看PHP source code

in_array() 函数在ext/standard/array.c 中实现。它所做的只是调用php_search_array(),其中包含以下循环:

while (zend_hash_get_current_data_ex(target_hash, (void **)&entry, &pos) == SUCCESS) {

    // checking the value...

    zend_hash_move_forward_ex(target_hash, &pos);
}

这就是线性特征的来源。

这是算法的整体特征,因为zend_hash_move_forward_ex() 具有恒定的行为:查看Zend/zend_hash.c,我们看到它基本上只是

*current = (*current)->pListNext;

至于array_unique()的时间复杂度:

  • 首先,将创建数组的副本,这是一个具有线性特性的操作
  • 然后,将创建一个 struct bucketindex 的 C 数组,并将指向我们数组副本的指针放入这些存储桶中 - 线性 特性再次
  • 然后,bucketindex-array 将使用快速排序进行排序 - n logn 平均
  • 最后,排序后的数组将被遍历,并且重复的条目将从我们数组的副本中删除 - 这应该再次线性,假设从我们的数组中删除是一个恒定时间操作李>

希望这会有所帮助;)

【讨论】:

  • array.c 的代码可以在这里找到:google.com/…(感谢 Gumbo);包括答案中的链接以某种方式破坏了stackoverflow :(
【解决方案3】:

试试这个算法。它利用了键查找比 in_array() 更快的事实:

function array_unique_mine($A) {
    $keys = Array();
    $values = Array();
    foreach ($A as $k => $v) {
        if (!array_key_exists($v, $values)) {
            $keys[] = $k;
            $values[$v] = $v;
        }
    }
    return array_combine($keys, $values);
}

【讨论】:

  • 我不确定 OP 是否正在寻找一种有效的算法,但可以解释为什么这些方法具有不同的时间复杂度。
  • 这只有在 $A 中的值都是字符串或整数时才有效!
  • 真的。我想你可以序列化 $values[serialize($v)] = $v 部分。
  • @joe_mucchiello 将您的方法添加到我的帖子中
【解决方案4】:

Gabriel's answer 对为什么你朋友的方法胜过你有一些很好的观点。对Christoph'sanswer 之后的对话很感兴趣,我决定自己进行一些测试。

另外,我尝试了不同长度的随机字符串,虽然结果不同,但顺序是一样的。为了简洁起见,我在此示例中使用了 6 个字符。

请注意,array_unique5 实际上与 native 具有相同的键,2 和 3,但只是以不同的顺序输出。

结果...

Testing 10000 array items of data over 1000 iterations:
array_unique6:  1.7561039924622 array ( 9998 => 'b',    9992 => 'a',    9994 => 'f',    9997 => 'e',    9993 => 'c',    9999 => 'd',    )
array_unique4:  1.8798060417175 array ( 0 => 'b',   1 => 'a',   2 => 'f',   3 => 'e',   4 => 'c',   5 => 'd',   )
array_unique5:  7.5023629665375 array ( 10 => 'd',  0 => 'b',   3 => 'e',   2 => 'f',   9 => 'c',   1 => 'a',   )
array_unique3:  11.356487989426 array ( 0 => 'b',   1 => 'a',   2 => 'f',   3 => 'e',   9 => 'c',   10 => 'd',  )
array_unique:   22.535032987595 array ( 0 => 'b',   1 => 'a',   2 => 'f',   3 => 'e',   9 => 'c',   10 => 'd',  )
array_unique2:  62.107122898102 array ( 0 => 'b',   1 => 'a',   2 => 'f',   3 => 'e',   9 => 'c',   10 => 'd',  )
array_unique7:  71.557286024094 array ( 0 => 'b',   1 => 'a',   2 => 'f',   3 => 'e',   9 => 'c',   10 => 'd',  )

还有代码...

set_time_limit(0);
define('HASH_TIMES', 1000);

header('Content-Type: text/plain');

$aInput  = array();
for ($i = 0; $i < 10000; $i++) {
    array_push($aInput, chr(rand(97, 102)));
}

function array_unique2($a) {
    $n = array();
    foreach ($a as $k=>$v)
        if (!in_array($v,$n))
            $n[$k]=$v;
    return $n;
}

function array_unique3($aOriginal) {
    $aUnique = array();

    foreach ($aOriginal as $sKey => $sValue) {
        if (!isset($aUnique[$sValue])) {
            $aUnique[$sValue] = $sKey;
        }
    }

    return array_flip($aUnique);
}

function array_unique4($aOriginal) {
    return array_keys(array_flip($aOriginal));
}

function array_unique5($aOriginal) {
    return array_flip(array_flip(array_reverse($aOriginal, true)));
}

function array_unique6($aOriginal) {
    return array_flip(array_flip($aOriginal));
}

function array_unique7($A) {
    $keys = Array();
    $values = Array();
    foreach ($A as $k => $v) {
        if (!array_key_exists($v, $values)) {
            $keys[] = $k;
            $values[$v] = $v;
        }
    }
    return array_combine($keys, $values);
}

function showResults($sMethod, $fTime, $aInput) {
    echo $sMethod . ":\t" . $fTime . "\t" . implode("\t", array_map('trim', explode("\n", var_export(call_user_func($sMethod, $aInput), 1)))) . "\n";
}

echo 'Testing ' . (count($aInput)) . ' array items of data over ' . HASH_TIMES . " iterations:\n";

$fTime = microtime(1);
for ($i = 0; $i < HASH_TIMES; $i++) array_unique($aInput);
$aResults['array_unique'] = microtime(1) - $fTime;

$fTime = microtime(1);
for ($i = 0; $i < HASH_TIMES; $i++) array_unique2($aInput);
$aResults['array_unique2'] = microtime(1) - $fTime;

$fTime = microtime(1);
for ($i = 0; $i < HASH_TIMES; $i++) array_unique3($aInput);
$aResults['array_unique3'] = microtime(1) - $fTime;

$fTime = microtime(1);
for ($i = 0; $i < HASH_TIMES; $i++) array_unique4($aInput);
$aResults['array_unique4'] = microtime(1) - $fTime;

$fTime = microtime(1);
for ($i = 0; $i < HASH_TIMES; $i++) array_unique5($aInput);
$aResults['array_unique5'] = microtime(1) - $fTime;

$fTime = microtime(1);
for ($i = 0; $i < HASH_TIMES; $i++) array_unique6($aInput);
$aResults['array_unique6'] = microtime(1) - $fTime;

$fTime = microtime(1);
for ($i = 0; $i < HASH_TIMES; $i++) array_unique7($aInput);
$aResults['array_unique7'] = microtime(1) - $fTime;

asort($aResults, SORT_NUMERIC);
foreach ($aResults as $sMethod => $fTime) {
    showResults($sMethod, $fTime, $aInput);
}

使用来自 cmets 的 Christoph's 数据集的结果:

$aInput = array(); for($i = 0; $i < 1000; ++$i) $aInput[$i] = $i; for($i = 500; $i < 700; ++$i) $aInput[10000 + $i] = $i;

Testing 1200 array items of data over 1000 iterations:
array_unique6:  0.83235597610474
array_unique4:  0.84050011634827
array_unique5:  1.1954448223114
array_unique3:  2.2937450408936
array_unique7:  8.4412341117859
array_unique:   15.225166797638
array_unique2:  48.685120105743

【讨论】:

  • 有趣;在我的测试中使用以下输入数组` $array = array(); for($i = 0; $i ` array_unique() 是第二快的...
  • 另外,您能否在测试中包含 joe 的算法 (stackoverflow.com/questions/478002/…)?
  • 或者只是检查array_key_exists()的测试是否会加速array_unique3()
  • 这很有趣。我得到的结果与我使用您的数据发布的结果相似。
  • @Christoph 我会添加它。我对此表示怀疑,因为 array_key_exists 比 isset 慢很多。
【解决方案5】:

PHP 的数组是作为哈希表实现的,即它们的性能特征与您对“真实”数组的期望不同。数组的键值对额外存储在链表中以允许快速迭代。

这解释了为什么您的实现与您朋友的相比如此缓慢:对于每个数字索引,您的算法都必须进行哈希表查找,而 foreach()-loop 只会遍历链表。

以下实现使用反向哈希表,可能是最快的(双重翻转由 joe_mucchiello 提供):

function array_unique2($array) {
    return array_flip(array_flip($array));
}

这仅在$array 的值是有效键(即整数或字符串)时才有效。

我还使用foreach()-loops 重新实现了您的算法。现在,它实际上会比你朋友的小数据集更快,但仍然比array_flip() 的解决方案慢:

function array_unique3($array) {
    $unique_array = array();

    foreach($array as $current_key => $current_value) {
        foreach($unique_array as $old_value) {
            if($current_value === $old_value)
                continue 2;
        }
        $unique_array[$current_key] = $current_value;
    }

    return $unique_array;
}

对于大型数据集,内置版本array_unique() 将优于除双翻转版本之外的所有其他版本。另外,你朋友使用in_array()的版本会比array_unique3()快。

总结一下:本机代码必胜!


另一个版本,应该保留键和它们的顺序:

function array_flop($array) {
    $flopped_array = array();

    foreach($array as $key => $value) {
        if(!isset($flopped_array[$value]))
            $flopped_array[$value] = $key;
    }

    return $flopped_array;
}

function array_unique4($array) {
    return array_flip(array_flop($array));
}

这实际上是 enobrevarray_unique3() - 我没有像我应该做的那样彻底检查他的实现...

【讨论】:

  • 这不会保留密钥。你的意思是 return array_flip(array_flip($array));
  • 好主意,但我不希望它更快。不过,基准测试可能会证明我错了。
  • 实际上它在这个线程中提出了另一个建议。它只是不会产生相同的结果。
  • @joe:Filip 的代码采用数字键,所以我也这样做了;不,我不是说双重翻转:我只关心值,这些是翻转数组的键......
  • 是的,但是你不能只关心值,因为内置的 array_unique 保留了输入数组的键。不保留它们是提供不同的算法。在这一点上,速度比较是无关紧要的。
【解决方案6】:

PHP 的执行速度比原始机器代码慢(很可能由 array_unique 执行)。

你的第二个示例函数(你朋友写的那个)很有趣。我看不出它会比原生实现更快,除非原生实现是删除元素而不是构建新数组。

【讨论】:

    【解决方案7】:

    我承认我不太了解本机代码,但它似乎复制了整个数组,对其进行排序,然后循环遍历它以删除重复项。在这种情况下,您的第二段代码实际上是一种更有效的算法,因为添加到数组的末尾比从中间删除更便宜。

    请记住,PHP 开发人员可能有充分的理由这样做。有人想问他们吗?

    【讨论】:

      【解决方案8】:

      原生 PHP 函数 array_uniqueimplemented in C。因此它比 PHP 快,必须先翻译。更重要的是,PHP 使用的算法与您不同。如我所见,PHP 首先使用Quick sort 对元素进行排序,然后在一次运行中删除重复项。

      为什么他朋友的实现更快有他自己的?因为它使用了更多尝试重新创建它们的内置功能。

      【讨论】:

        猜你喜欢
        • 2018-06-28
        • 2011-08-04
        • 2018-07-10
        • 2011-11-05
        • 1970-01-01
        • 2011-02-21
        • 1970-01-01
        • 1970-01-01
        • 2014-08-06
        相关资源
        最近更新 更多