按两列对行数组进行排序,同时不移动“锁定”(粘性)行

Sort an array of rows by two columns while not moving "locked" (sticky) rows

提问人:Jack co 提问时间:8/26/2022 最后编辑:mickmackusaJack co 更新时间:8/27/2022 访问量:135

问:

我需要按分数对项目数组进行排序,然后按日期排序,而无需移动锁定值大于零的行。

换句话说,排序完成后,带有 和 的行应保持在相同/原始位置。lock=3lock=5

[
    ['id' => 7867867, 'lock' => 0, 'score' => 322, 'strtotime' => 16614713],
    ['id' => 7867867, 'lock' => 0, 'score' => 444, 'strtotime' => 16614613],
    ['id' => 7867867, 'lock' => 3, 'score' =>   0, 'strtotime' => 16613713],
    ['id' => 7867867, 'lock' => 0, 'score' =>  11, 'strtotime' => 16612713],
    ['id' => 7867867, 'lock' => 5, 'score' =>   0, 'strtotime' => 16614413],
    ['id' => 7867867, 'lock' => 0, 'score' =>  42, 'strtotime' => 16614113],
    ['id' => 7867867, 'lock' => 0, 'score' =>  22, 'strtotime' => 16614013],
]

我使用以下代码对 than 进行排序,但这会影响不应该移动的行。scorestrtotime

usort($array, function ($a, $b) {
    if ( $a->score == $b->score ) {  //score are same
       return $b->strtotime <=> $a->strtotime; //sort by strtotime
    }
    return $b->score <=> $a->score; //else sort by score
});

我想要的输出是:

[
    ['id' => 7867867, 'lock' => 0, 'score' =>  11, 'strtotime' => 16612713],
    ['id' => 7867867, 'lock' => 0, 'score' =>  22, 'strtotime' => 16614013],
    ['id' => 7867867, 'lock' => 3, 'score' =>   0, 'strtotime' => 16613713],
    ['id' => 7867867, 'lock' => 0, 'score' =>  42, 'strtotime' => 16614113],
    ['id' => 7867867, 'lock' => 5, 'score' =>   0, 'strtotime' => 16614413],
    ['id' => 7867867, 'lock' => 0, 'score' => 322, 'strtotime' => 16614713],
    ['id' => 7867867, 'lock' => 0, 'score' => 444, 'strtotime' => 16614613],
]
php 数组粘 usort 自定义排序

评论

0赞 mickmackusa 8/27/2022
在缺乏明确性的情况下,我们必须假设行的“粘性位置”不是行的索引,而是从开始的 1 索引计数——这意味着第 3 个位置是第 2 个索引位置。这个任务是 Stack Overflow 上多个预先存在的问题的组合:PHP 根据值将数组拆分为两个数组,按多列对多维数组进行排序,以及在 PHP 中任何位置的数组中插入新项lock
0赞 mickmackusa 8/27/2022
我会以这种方式调整接受的答案以更改粘性位置并确保将整行重新注入数组中。

答:

1赞 IMSoP 8/26/2022 #1

在排序过程中,您无法访问列表中的绝对位置,只能访问正在比较的一对项目,因此我以这种方式处理它:

  1. 从列表中删除“锁定”值
  2. 对其他所有内容进行排序
  3. 将锁定的值放回原处

对于步骤 1,只需遍历数组,生成两个新数组:

$result = [];
$locked = [];
foreach ( $input as $item ) {
    if ( $item['lock'] > 0 ) {
        $locked[] = $item;
    }
    else {
        $result[] = $item;
    }
}

第 2 步是您已经拥有的代码,使用我称之为 的解锁项数组,因为它最终将包含最终结果。$result

对于步骤 3,您可以使用 array_splice 将项目放入数组中所选位置,然后向下移动所有内容。

这里需要注意的重要一点是,您插入的顺序很重要:如果您将项目 X 插入位置 5,然后将项目 Y 插入位置 3,则位置 X 将向前移动到位置 6。因此,如果您锁定的物品尚未按顺序排列,请对它们进行排序:

usort($locked, fn($a,$b) => $a['lock'] <=> $b['lock']);

然后循环,将它们拼接到所需的位置:

foreach ( $locked as $item ) {
    array_splice($result, $item['lock'], 0, $item);
}

然后你应该完成:)

0赞 mickmackusa 8/27/2022 #2

在没有任何过滤、临时数组或拼接的情况下,我精心制作了一个带有三个嵌套循环的手动排序算法(具有条件短路以获得最佳性能)以提供“粘性排序”。

没有函数调用,因此执行速度相当快。我添加了内联注释来帮助理解代码。

下面的代码片段不关心非零锁值是什么。在发布的问题中,锁定的行已经位于它们应该在结果中的位置。该算法从不移动具有非零锁定值的行。这使得脚本很容易适应其他场景,其中另一个标志指示“固定行”。

代码:(演示)

$maxIndex = count($array) - 1;
for ($a = 0; $a < $maxIndex; ++$a) {
    if ($array[$a]['lock'] !== 0) {
        continue;  // cannot move locked row
    }
    for ($b = 0; $b < $maxIndex; ++$b) {
        if ($array[$b]['lock'] !== 0) {
            continue;  // cannot move locked row
        }
        // find next movable row
        for ($c = $b + 1; $c <= $maxIndex; ++$c) {
            if ($array[$c]['lock'] === 0) {
                break;  // $c is index of non-locked row
            }
        }
        if ($c > $maxIndex) {
            break;  // no more movable rows
        }
        // sort movable rows
        if (
            $array[$b]['score'] > $array[$c]['score']
            || ($array[$b]['score'] === $array[$c]['score']
                && $array[$b]['strtotime'] > $array[$c]['strtotime'])
        ) {
             [$array[$b], $array[$c]] = [$array[$c], $array[$b]];
        }
    }
}
var_export($array);