在 PHP 面试中,数组排序是一个看似基础却极易暴露知识深度的考点。很多候选人对 sort、usort 的用法倒背如流,但当面试官追问“PHP 的排序是稳定的吗”“usort 的比较函数返回值有什么讲究”“多个字段排序怎么写才不出错”时,往往就露了怯。本文从 PHP 内置排序函数出发,讲清稳定排序的概念、PHP 各排序函数的行为差异,以及 usort 回调设计的实战要点。
一、什么是稳定排序
稳定排序指的是:当两个元素的排序键相等时,它们在排序后保持原有的相对顺序。
举个例子,有一组按“价格”排序的商品,现在要再按“销量”排序。如果排序算法是稳定的,那么销量相同的商品会保持原来的价格顺序;如果不稳定,销量相同的商品顺序可能被打乱,导致上一轮的排序结果部分丢失。
这个性质在多字段排序、分步排序场景中非常关键。面试中如果被问到“什么是稳定排序”,能结合业务场景说清楚,比单纯背定义要加分得多。
二、PHP 排序函数的稳定性演变
PHP 的排序函数分为几类:
- 按值排序:
sort、rsort、asort、arsort - 按键排序:
ksort、krsort - 自定义比较:
usort、uasort、uksort - 自然排序:
natsort、natcasesort
在 PHP 8.0 之前,sort、rsort、asort、arsort、ksort、krsort 以及 usort 等函数使用的都是不稳定排序(内部实现为快速排序的变体)。这意味着相等元素的顺序是不确定的,官方文档也明确说明“排序不稳定”。
从 PHP 8.0 开始,PHP 将所有排序函数改为稳定排序。这是一个重要变更,官方 RFC 中明确写道:排序的稳定性现在得到保证。实现上,PHP 在比较时引入了原始位置的比较作为“平局决胜”,从而保证稳定性。
面试回答要点:
- PHP 8.0 是一个分水岭,之前不稳定,之后稳定。
- 稳定性的代价是额外的内存和比较开销,但换来的是可预测的行为。
- 即使 PHP 8.0+ 保证了稳定,理解稳定排序的原理仍然重要,因为跨语言、跨版本迁移时可能踩坑。
三、usort 回调设计的常见陷阱
usort($array, $callback) 用自定义函数比较元素。回调接收两个参数 $a 和 $b,返回值必须是整数:
- 返回 负数:
$a排在$b前面 - 返回 0:两者顺序不变(PHP 8.0+ 稳定)
- 返回 正数:
$a排在$b后面
陷阱一:返回布尔值
这是最常见的错误:
// 错误写法
usort($arr, function ($a, $b) {
return $a['age'] > $b['age'];
});
布尔值 true 会被转换为 1,false 转换为 0。当 $a['age'] > $b['age'] 为 true 时返回 1(表示 $a 在后),为 false 时返回 0(表示相等)。结果就是排序方向混乱,且永远返回不了负数,逻辑完全错误。
正确写法应使用“飞船运算符”:
usort($arr, fn($a, $b) => $a['age'] <=> $b['age']);
陷阱二:返回值不是整数
如果回调返回浮点数,PHP 会将其强制转换为整数。比如返回 0.5 会变成 0,返回 -0.3 会变成 0,导致排序结果不符合预期。比较函数应始终返回整数。
陷阱三:多字段排序的写法
面试常考“先按年龄升序,年龄相同再按姓名升序”。很多人会写成:
usort($arr, function ($a, $b) {
if ($a['age'] === $b['age']) {
return $a['name'] <=> $b['name'];
}
return $a['age'] <=> $b['age'];
});
这种写法正确,但可以更简洁:
usort($arr, fn($a, $b) => [$a['age'], $a['name']] <=> [$b['age'], $b['name']]);
利用数组比较的特性:PHP 会先比较第一个元素,相等时再比较第二个。这需要 PHP 7.0+ 的飞船运算符支持,代码更紧凑,也更容易扩展到三个、四个字段。
陷阱四:依赖不稳定排序做“技巧”
在 PHP 8.0 之前,有人会利用不稳定排序的“随机性”来打乱数据,或者依赖某种未定义行为。这种代码在升级到 PHP 8.0 后行为会改变。正确的做法是显式使用 shuffle 或 array_rand 来打乱,不要依赖排序的副作用。
四、实战:一个可复用的多字段比较器
面试中如果被要求“写一个通用的多字段排序函数”,可以这样设计:
function multiSort(array &$data, array $rules): void
{
usort($data, function ($a, $b) use ($rules) {
foreach ($rules as $field => $direction) {
$cmp = $a[$field] <=> $b[$field];
if ($cmp !== 0) {
return $direction === 'desc' ? -$cmp : $cmp;
}
}
return 0;
});
}
// 使用
multiSort($users, ['age' => 'asc', 'name' => 'desc']);
这个设计的要点:
- 用
<=>得到三态结果,避免布尔值陷阱。 - 遇到非零结果立即返回,减少比较次数。
- 通过
$direction控制升降序,用取负实现降序。 - 所有字段相等时返回 0,配合 PHP 8.0+ 的稳定排序,保持原有顺序。
五、面试高频追问
问:usort 和 uasort 有什么区别?
答:usort 重新索引数组键,uasort 保留键值关联。处理关联数组时用 uasort,纯列表用 usort。
问:usort 的时间复杂度是多少?
答:PHP 8.0+ 的排序实现基于 Timsort 的变体(实际是 PHP 自己的混合实现),平均和最坏情况为 O(n log n),空间复杂度 O(n)。稳定性通过额外的位置信息实现。
问:为什么 PHP 8.0 要把排序改成稳定的?
答:稳定排序让多字段排序、分步排序的行为可预测,减少了因排序不稳定导致的隐蔽 bug,提升了语言的一致性。虽然有一定性能开销,但收益更大。
问:如何在不支持稳定排序的版本中模拟稳定排序?
答:在比较函数中,当主键相等时,用原始索引作为决胜条件。可以先给每个元素附加原始位置,再在回调中比较位置。
六、总结
从数组排序函数切入,面试官考察的其实是三个层次的能力:
- 基础层:是否清楚各排序函数的区别和适用场景。
- 原理层:是否理解稳定排序的概念及 PHP 8.0 的变更。
- 工程层:是否能写出正确、健壮、可复用的比较回调,避开布尔值、浮点数、多字段排序等常见陷阱。
下次面试再遇到排序问题时,不妨主动提一句“PHP 8.0 之后排序是稳定的”,再展示一下飞船运算符和多字段比较的写法,相信能让面试官眼前一亮。
未经允许不得转载:任鹏个人博客 » PHP 面试精讲:从数组排序函数看稳定排序与 usort 回调设计

