
本文介绍一种分治策略:先分离锁定行(lock > 0),对非锁定行按 score 降序、strtotime 降序排序,再将锁定行按原始索引位置精准插回,确保其绝对位置不变。
本文介绍一种分治策略:先分离锁定行(lock > 0),对非锁定行按 score 降序、strtotime 降序排序,再将锁定行按原始索引位置精准插回,确保其绝对位置不变。
在实际业务场景中(如内容置顶、管理员固定条目、广告位保留等),常需对数据列表进行多字段排序,同时严格保留某些“粘性”或“锁定”行的原始位置。PHP 的 usort() 等原生排序函数仅提供两两比较逻辑,无法感知元素在原数组中的绝对索引,因此无法直接实现“局部排序 + 全局位置锁定”的混合行为。正确解法是采用三步分离-排序-归并策略:
✅ 步骤一:分离锁定行与待排序行
遍历原始数组,依据 'lock' > 0 条件拆分为两个独立数组:$unlocked(参与排序)和 $locked(需原位保留)。注意:此处的 'lock' 值并非布尔标记,而是隐含原始索引位置的占位符(如 lock => 3 表示该行应始终位于最终结果的第 3 个位置,即索引 3 —— PHP 数组索引从 0 开始,因此实际插入位置为 3)。
$unlocked = [];
$locked = [];
foreach ($array as $item) {
if ($item['lock'] > 0) {
$locked[] = $item;
} else {
$unlocked[] = $item;
}
}
✅ 步骤二:对非锁定行执行多列排序
使用 usort() 对 $unlocked 按主键 score(降序)、次键 strtotime(降序)稳定排序。注意原始代码中误用对象属性访问($a->score),而输入数据为关联数组,应改为数组下标访问:
usort($unlocked, function ($a, $b) {
if ($a['score'] === $b['score']) {
return $b['strtotime'] $a['strtotime']; // strtotime 降序
}
return $b['score'] $a['score']; // score 降序
});
✅ 步骤三:按原始 lock 值将锁定行精准插回
关键点在于:$locked 中每个元素的 'lock' 值即为其目标插入索引(非偏移量)。需确保按 lock 升序插入,避免因前序插入导致后续索引偏移。例如,若先插入 lock=5,再插入 lock=3,则后者会挤入前者之前,破坏位置一致性。
// 按 lock 值升序排列,保证插入顺序由小到大
usort($locked, fn($a, $b) => $a['lock'] $b['lock']);
// 逐个插入:array_splice($array, $offset, $length, $replacement)
foreach ($locked as $item) {
array_splice($unlocked, $item['lock'], 0, [$item]);
}
⚠️ 注意事项:
array_splice()的第三个参数0表示不删除任何元素,仅插入;第四个参数必须为数组(即使单元素),故写为[$item];- 若原始数据中
lock值存在重复或超出范围(如lock > count($array)),需提前校验并处理边界情况;- 此方案时间复杂度为 O(n log n),空间复杂度 O(n),适用于中等规模数据;超大规模时可考虑就地分区优化,但逻辑复杂度显著上升。
最终 $unlocked 即为满足要求的完整结果数组——所有 lock > 0 的行稳居指定索引,其余行按双字段规则有序排列,完美兼顾灵活性与确定性。










