Menu

C++ 排序:std::sort、自定义比较器与稳定排序

用 std::sort 给 C++ 中的 vector 和数组排序:默认顺序、自定义比较器、按字段给结构体排序,以及会导致崩溃的严格弱序陷阱。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

排序只是另一种算法

在上一页中你看到,标准库提供了现成的算法,它们通过迭代器作用于任何范围。排序是你最常用的那个,它之所以单独占一页,是因为它有几个容易伤人的地方:自定义顺序、稳定性,还有一条规则,一旦违反,你得到的不是错误答案,而是未定义行为。

主力是 <algorithm> 中的 std::sort。你把一个范围的开头和结尾交给它,它就会把元素原地排成升序:

这里不会产生拷贝,重新排列的是 vector 本身。std::sort 底层通常是内省排序(会退回堆排序的快速排序),平均复杂度为 O(n log n)。这几乎总是比你自己写排序更快,也不容易出错得多。

它对普通的 C 数组同样适用,只需用指针描述范围:

用比较器自定义顺序

默认情况下,std::sort 用 operator< 给元素排序。想换一种排法,就传入第三个参数:一个比较器,它接收两个元素,如果第一个应该排在第二个之前就返回 true。

lambda 最适合做这件事。降序就是 a > b:

对于内置类型的普通降序这种常见情况,标准库甚至提供了现成的比较器,即 <functional> 中的 greater<T>():

#include <functional>
sort(nums.begin(), nums.end(), greater<int>());  // same as a > b

比较器也是按值本身以外的东西排序的方法,例如按长度而不是按字母顺序给字符串排序:

对于比几个字节大的东西(比如 string),比较器的参数要用 const& 接收:每次比较都拷贝每个元素纯属浪费。

按字段给结构体排序

在实际程序中,你通常会按某个字段给一组结构体排序。比较器只需取出你关心的字段。这里我们按年龄给人排序,年龄小的在前:

注意 Linus 和 Dennis 的年龄都是 25。这里他们恰好保持了原来的相对顺序,但 std::sort 不保证这一点。如果相等元素的相对顺序很重要,就用 std::stable_sort,它会保持这个顺序(性能上略有代价):

要有意地处理并列情况,比如先按年龄、再按名字的字母顺序排序,就只在主键相等时才比较次键。std::tie 让这件事写得很干净:

严格弱序陷阱

这是 C++ 排序中最危险的错误,因为它给你的不是错误答案,而是未定义行为,往往意味着崩溃或越界读取。

std::sort 要求你的比较器定义一个严格弱序。实用的规则是:对任何元素 x,comp(x, x) 必须为 false。换句话说,一个元素永远不会排在它自己“之前”。< 和 > 恰好满足这一点,而 <= 和 >= 恰好破坏了它:

// BUG: returns true when a == b, violating strict weak ordering.
sort(v.begin(), v.end(), [](int a, int b) {
    return a <= b;   // undefined behavior - may crash on some inputs
});

用 <= 时,比较器声称一个 5 排在另一个 5 之前,这自相矛盾。std::sort 随后可能让指针越过范围的末尾。很小的输入有时看起来没问题,这正是这个 bug 可怕的地方:它可能通过你的测试,却在生产环境中崩溃。修复方法很简单,用 <:

第二个经典陷阱:排序会让所有指向该范围内部的东西失效。 排序前保存的迭代器、指针和下标,排序后不再指向同一个逻辑元素,因为元素已经移动了。需要的任何位置都要在排序之后重新求出,绝不能在之前。

只排序范围的一部分

有时你并不需要全部排好序,只想要前几名。为了读前三个而把整个 vector 排序是一种浪费。std::partial_sort 只排好你要求的那些元素,其余元素的顺序不做保证,所以开销更小:

如果你只需要本应位于某个位置上的那一个元素,比如中位数,std::nth_element 做的工作更少:它把正确的元素放到那个下标上,比它小的都在前面,比它大的都在后面,平均只需 O(n)。

当“完全有序”超出了问题的实际需要时,就用它们:在大数据上能省下实实在在的时间。

下一节:模板

你有没有注意到,同一个 std::sort 既能处理 int,也能处理 string 和你自己的 Person 结构体,而 greater<int>() 也完全可以写成 greater<string>()?这种通用性不是魔法,而是模板:一种让同一段代码适用于调用者传入的任何类型的机制。下一页我们会学习如何编写自己的模板函数和模板类,让你的代码像你一直在用的这些算法一样与类型无关。

常见问题

C++ 中如何给 vector 排序?

包含 <algorithm>,然后调用 sort(v.begin(), v.end())。它会用 operator< 把元素原地排成升序。要给数组排序,就传入 arr 和 arr + n(或 begin(arr) / end(arr))。

C++ 中如何降序排序?

传入一个返回 a > b 的比较器:sort(v.begin(), v.end(), [](int a, int b){ return a > b; });。也可以使用 <functional> 中内置的 sort(v.begin(), v.end(), greater<int>());。

为什么我的 C++ 比较器会让 std::sort 崩溃?

比较器必须满足严格弱序:两个参数相等时必须返回 false。使用 <= 或 >=(对相等元素返回 true)会破坏这条规则,属于未定义行为:std::sort 可能越界读取并崩溃。比较时始终用 < 或 >。

Coddy 编程语言插图

和 Coddy 一起学编程

开始