在PHP中,如何删除对象数组中的重复项?其中,重复项被定义为具有相同值的键值对子集。

4

我有一个形式如下的数组:

class anim {
    public $qs;
    public $dp;
    public $cg;
    public $timestamp;
}
$animArray = array();

$myAnim = new anim();
$myAnim->qs = "fred";
$myAnim->dp = "shorts";
$myAnim->cg = "dino";
$myAnim->timestamp = 1590157029399;
$animArray[] = $myAnim;

$myAnim = new anim();
$myAnim->qs = "barney";
$myAnim->dp = "tshirt";
$myAnim->cg = "bird";
$myAnim->timestamp = 1590133656330;
$animArray[] = $myAnim;

$myAnim = new anim();
$myAnim->qs = "fred";
$myAnim->dp = "tshirt";
$myAnim->cg = "bird";
$myAnim->timestamp = 1590117032286;
$animArray[] = $myAnim;

我该如何创建一个新的数组,仅包含 $animArray 中不重复的项(以及重复项中的最新条目),其中重复定义为: $myAnim->dp 的值与另一个数组元素的 $myAnim->dp 相同,且第一个和第二个数组元素的 $myAnim->cg 均相等。
在上面的示例中,只有第一个元素符合此定义。
我希望有一种优雅的解决方案。我查看了 PHP 手册中的所有数组函数但未找到可行方法。
我可以循环遍历每个数组元素,检查 $myAnim->dp 是否与另一个数组元素的 $myAnim->dp 相同,将匹配项保存到新数组中,然后循环遍历该新数组,检查其 $myAnim->cg 是否与该新数组中任何其他元素的 $myAnim->cg 相匹配。
更优雅的解决方案应该允许我更改哪些键值对的组合确定是否有重复,而无需大量重构代码。
这样的解决方案存在吗?
谢谢帮助这位新手 :)

1
那么在你的例子中,应该返回对象0和对象2,对吗?对象0是唯一的,对象2是重复项中的最后一个。 - MatsLindh
是的,没错,MatsLindh。 - Mark Highton Ridley
这不是一个好的制作类的方式。我希望这是为了展示你想要实现的主要目的 :-) - bestprogrammerintheworld
你放弃了吗?你有3个答案。 - AbraCadaver
我正在对更大的数据集进行审查和测试建议。 - Mark Highton Ridley
3个回答

3

虽然没有内置函数可以直接使用,但是处理任意数量的属性以考虑其唯一性的代码不多。通过在查找数组中跟踪每个唯一属性,我们可以构建一个数组,其中叶节点(即非数组本身的节点)是对象。

我们通过在数组中保留对当前层级的引用(&),然后继续为每个属性构建查找数组来实现此目的。

function find_uniques($list, $properties) {
    $lookup = [];
    $unique = [];
    $last_idx = count($properties) - 1;

    // Build our lookup array - the leaf nodes will be the items themselves,
    // located on a level that matches the number of properties to look at
    // to consider a duplicate
    foreach ($list as $item) {
        $current = &$lookup;

        foreach ($properties as $idx => $property) {
            // last level, keep object for future reference
            if ($idx == $last_idx) {
                $current[$item->$property] = $item;
                break;
            } else if (!isset($current[$item->$property])) {
                // otherwise, if not already set, create empty array
                $current[$item->$property] = [];
            }

            // next iteration starts on this level as its current level
            $current = &$current[$item->$property];
        }
    }

    // awr only calls the callback for leaf nodes - i.e. our items.
    array_walk_recursive($lookup, function ($item) use (&$unique) {
        $unique[] = $item;
    });

    return $unique;
}

根据您提供的数据和要求(保留唯一值和重复项中的最后一个元素),我们得到以下结果:

var_dump(find_uniques($animArray, ['dp', 'cg']));

array(2) {
  [0] =>
  class anim#1 (4) {
    public $qs =>
    string(4) "fred"
    public $dp =>
    string(6) "shorts"
    public $cg =>
    string(4) "dino"
    public $timestamp =>
    int(1590157029399)
  }
  [1] =>
  class anim#3 (4) {
    public $qs =>
    string(4) "fred"
    public $dp =>
    string(6) "tshirt"
    public $cg =>
    string(4) "bird"
    public $timestamp =>
    int(1590117032286)
  }
}

在您的示例中,它映射到元素[0]和元素[2]。如果您希望保留重复项中的第一个对象,可以添加一个isset语句来终止内部循环,如果属性值已经被看到:

foreach ($properties as $idx => $property) {
    if ($idx == $last_idx) {
        if (isset($current[$item->$property])) {
            break;
        }

        $current[$item->$property] = $item;
    } else {
        $current[$item->$property] = [];
    }

    // next iteration starts on this level as its current level
    $current = &$current[$item->$property];
}

需要注意的是,本文假设你要检查唯一性的数组本身不包含其他数组(因为我们使用 -> 查找属性,并且使用 array_walk_recursive 查找任何不是数组的内容)。


感谢您的回答。不幸的是,当我使用以下代码扩展数组数据时,它无法正确识别唯一性:$myAnim = new anim(); $myAnim->qs = "wilma"; $myAnim->dp = "shorts"; $myAnim->cg = "bird"; $myAnim->timestamp = 1590117035383; $animArray[] = $myAnim;$myAnim = new anim(); $myAnim->qs = "pebbles"; $myAnim->dp = "tshirt"; $myAnim->cg = "bird"; $myAnim->timestamp = 1590117038461; $animArray[] = $myAnim; - Mark Highton Ridley
答案有什么问题吗?如果您能解释一下就更好了 :-) - MatsLindh
我想我明白你在想什么。已修复。 - MatsLindh
感谢@MatsLindh - 我会再试一次 :) - Mark Highton Ridley
2
我选择了你的回答作为被采纳的答案,Mats。虽然@AbraCadaver的解决方案也完美地解决了问题,但我之所以这样做是因为你的回答对像我这样的初学者更易读/易懂。 - Mark Highton Ridley

2

这很有趣:

array_multisort(array_column($animArray, 'timestamp'), SORT_DESC, $animArray);

$result = array_intersect_key($animArray,
          array_unique(array_map(function($v) { return $v->dp.'-'.$v->cg; }, $animArray)));

首先,提取timestamp并按降序排序该数组,从而对原始数组进行排序。然后,映射以使用 dp 和 cg 组合创建新数组。接下来,使组合数组唯一,这将保留第一个遇到的重复项(这就是我们按降序排序的原因)。最后,获取原始数组和唯一数组的键的交集。
在具有动态属性的函数中:
function array_unique_custom($array, $props) {

    array_multisort(array_column($array, 'timestamp'), SORT_DESC, $array);

    $result = array_intersect_key($array,
              array_unique(array_map(function($v) use ($props) {
                  return implode('-', array_map(function($p) use($v) { return $v->$p; }, $props));;
              },
              $array)));

    return $result;
}
$result = array_unique_custom($animArray, ['dp', 'cg']);

另一个选项是按升序排序,然后构建一个以 dpcg 组合作为键的数组,这将保留最后一个重复项。
array_multisort(array_column($animArray, 'timestamp'), SORT_ASC, $animArray);

foreach($animArray as $v) {
    $result[$v->dp.'-'.$v->cg] = $v;
}

在具有动态属性的函数中:
function array_unique_custom($array, $props) {

    array_multisort(array_column($array, 'timestamp'), SORT_ASC, $array);

    foreach($array as $v) {
        $key = implode(array_map(function($p) use($v) { return $v->$p; }, $props));
        $result[$key] = $v;
    }
    return $result;
}
$result = array_unique_custom($animArray, ['dp', 'cg']);

1
请注意,使用implode函数可能会创建错误的重复项;即如果一个值的后缀与另一个值的前缀匹配- implode(['foo', 'bar'])将给出与implode(['foob', 'ar'])相同的键。如果使用分隔符,则情况会稍微好一些,但是如果该字符是值的一部分,则仍可能遇到相同的问题。 - MatsLindh
@MatsLindh 很好的发现,已添加分隔符。 - AbraCadaver
@AbraCadaver:您的选项很好: "另一种选择是按升序排序,然后构建一个数组,其中dp和cg组合作为键,这将保留最后一个重复项" - Mark Highton Ridley
我已经接受了另一个答案。我想两个答案都接受,但如果我接受你的答案,另一个答案就会变为未接受状态。 - Mark Highton Ridley

0
//Create an array with dp and cg values only
$new_arr = [];
foreach($animArray as $key=>$item) {
    $new_arr[] = $item->dp.','.$item->cg;
}
$cvs = array_count_values($new_arr);
$final_array = [];
foreach($cvs as $cvs_key=>$occurences) {
    if ($occurences == 1) {
        $filter_key = array_keys($new_arr, $cvs_key)[0];         
        $final_array[$filter_key] = $animArray[$filter_key];    
    }
}

最终结果将会是(根据您的示例)在$final_array中:
[0] => anim Object
    (
        [qs] => fred
        [dp] => shorts
        [cg] => dino
        [timestamp] => 1590157029399
    )

一些解释:
//Create a new array based on your array of objects with the attributes dp and cg
//with a comma  between them
$new_arr = [];
foreach($animArray as $key=>$item) {
    $new_arr[] = $item->dp.','.$item->cg;
}
/*
$new_arr now contains:

    [0] => shorts,dino
    [1] => tshirt,bird
    [2] => tshirt,bird
*/

//Use builtin-function array_count_values to get the nr of occurences for 
//each item in an array
$cvs = array_count_values($new_arr);

/*
$cvs would contain:

(
    [shorts,dino] => 1
    [tshirt,bird] => 2
)
*/

//Iterate through the $cvs array.
//Where there are only one occurence (no duplicates)
//create a final array $final_array
$final_array = [];
foreach($cvs as $cvs_key=>$occurences) {
    if ($occurences == 1) {

        /*
        array_keys with second argument $csv_key searches for key with 
        with the key from $cvs-key

        so basically search for:
        shorts,dino and retrieve the key 0 (first element)        
        */
        $filter_key = array_keys($new_arr, $cvs_key)[0];         

        /*
        Add a new item to the $final_array based on the key in
        the original array $animArray
        if you don't want the original key in the new array
        you could just do $final_array[] instead of 
        $final_array[$filter_key]
        */
        $final_array[$filter_key] = $animArray[$filter_key];    
    }
}

您说您想要进行某种功能测试以检查不同的属性。我相信只需编写一个函数/方法,其中将两个值传递给参数$attr1('dp'?),$attr2('cg'?)或类似参数即可。

更新

我没有理解你也想要最后一个值。实际上,这似乎是一个更容易的任务。也许我错过了什么,但是想出与其他答案不同的方法很有趣 :-)

//Create an array with dp and cg values only
$new_arr = [];
foreach($animArray as $key=>$item) {
    $new_arr[] = $item->dp.','.$item->cg;
}

//Sort keys descending order
krsort($new_arr); 

//Because of sending order of keys above, the unique values would return the 
//last item of the duplicates
$new_arr2 = array_unique($new_arr); 

//Switch order of keys back to normal (ascending)
ksort($new_arr2); 

//Create a new array based on the keys set in $new_arr2
//
$final_arr = [];
foreach($new_arr2 as $key=>$item) {
    $final_arr[] = $animArray[$key];
}

$final_arr[] 的输出将会是(以你的示例为例)

Array
(
    [0] => anim Object
        (
            [qs] => fred
            [dp] => shorts
            [cg] => dino
            [timestamp] => 1590157029399
        )

    [1] => anim Object
        (
            [qs] => fred
            [dp] => tshirt
            [cg] => bird
            [timestamp] => 1590117032286
        )

)

你的回答没有包含最新的重复项。我说:“如何创建一个仅包含非重复项(以及找到重复项的最新条目)的新数组?” - Mark Highton Ridley
啊哈。抱歉我完全错过了那个问题。我会尽快回复你一个更好的答案。 - bestprogrammerintheworld
我还没有尝试过这个,BPTW。不过我会尝试并告诉你结果的。 - Mark Highton Ridley
糟糕,我以为你已经修改了它。我会等的 :) - Mark Highton Ridley
@MarkHightonRidley - 哈哈,抱歉。最近一周发生了很多事情。我相信明天下午我会有时间进一步研究它。 - bestprogrammerintheworld
我不在乎你是否已经接受了它 - 我仍然会尝试让我的“解决方案”更好。很高兴你找到了适合你的解决方案 :-) - bestprogrammerintheworld

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接