C++ vector 自定义排序实战:从基础规则到Lambda表达式进阶
1. 为什么需要自定义vector排序?
在日常开发中,我们经常遇到标准排序规则无法满足需求的情况。比如处理二维坐标点时,可能需要先按x轴降序排列,x相同的再按y轴升序排列;或者处理任务队列时,需要根据任务优先级和创建时间进行多级排序。这时候就需要自定义排序规则。
C++的vector容器虽然提供了默认的sort排序,但面对复杂数据结构时往往力不从心。我最近在优化一个游戏引擎的渲染系统时,就遇到了需要对十万级别的精灵图元按层级和深度排序的需求,默认排序完全无法满足性能要求。
2. 基础排序方法:比较函数
2.1 一维vector的排序
我们先从最简单的例子开始。假设有一个整数vector:
vector<int> numbers = {3,1,4,1,5,9,2,6};默认的升序排序很简单:
sort(numbers.begin(), numbers.end()); // 结果:1,1,2,3,4,5,6,9如果想降序排列,可以使用标准库提供的greater:
sort(numbers.begin(), numbers.end(), greater<int>()); // 结果:9,6,5,4,3,2,1,12.2 二维vector的排序
当处理二维vector时,情况就变得有趣了。考虑一个存储坐标点的vector:
vector<vector<int>> points = {{0,2},{1,5},{1,9},{4,6},{5,9},{8,10}};默认排序会先比较每个子vector的第一个元素,相等时再比较第二个:
sort(points.begin(), points.end()); // 结果:{0,2},{1,5},{1,9},{4,6},{5,9},{8,10}但如果我们想要先按x坐标降序,x相同再按y升序呢?这就需要自定义比较函数:
static bool cmp(const vector<int>& a, const vector<int>& b) { if(a[0] == b[0]) return a[1] < b[1]; // x相同则y升序 return a[0] > b[0]; // x降序 } sort(points.begin(), points.end(), cmp); // 结果:{8,10},{5,9},{4,6},{1,5},{1,9},{0,2}这里有个关键点:比较函数必须声明为static。这是因为在类内部定义时,非静态成员函数有隐含的this指针参数,与sort函数期望的调用约定不匹配。我在LeetCode刷题时就因为这个坑卡了半天。
3. pair类型的排序技巧
3.1 pair的基本排序
pair是C++中非常实用的模板类,常用于存储键值对。它的默认排序规则是先比较first,再比较second:
vector<pair<int,int>> pairs = {{0,2},{1,5},{1,9},{4,6},{5,9},{8,10}}; sort(pairs.begin(), pairs.end()); // 结果:{0,2},{1,5},{1,9},{4,6},{5,9},{8,10}3.2 自定义pair排序
假设我们要实现和之前一样的排序规则:first降序,second升序。比较函数可以这样写:
static bool cmp(const pair<int,int>& a, const pair<int,int>& b) { if(a.first == b.first) return a.second < b.second; return a.first > b.first; } sort(pairs.begin(), pairs.end(), cmp); // 结果:{8,10},{5,9},{4,6},{1,5},{1,9},{0,2}在实际项目中,我经常用pair来存储带权重的数据。比如在实现Dijkstra算法时,就需要按距离优先处理节点,这时候自定义排序就派上用场了。
4. Lambda表达式:更现代的解决方案
4.1 Lambda基础用法
C++11引入的Lambda表达式让自定义排序变得更简洁。上面的例子可以改写为:
sort(points.begin(), points.end(), [](const vector<int>& a, const vector<int>& b) { if(a[0] == b[0]) return a[1] < b[1]; return a[0] > b[0]; });Lambda的语法糖让我们可以就地定义排序规则,不需要额外写比较函数。这在算法竞赛和快速原型开发中特别有用。
4.2 使用auto简化代码
C++14进一步增强了Lambda的能力,允许参数使用auto:
sort(points.begin(), points.end(), [](const auto& a, const auto& b) { if(a[0] == b[0]) return a[1] < b[1]; return a[0] > b[0]; });这种写法更通用,可以适配不同类型的容器元素。我在处理模板代码时特别喜欢用这种方式,可以减少很多重复代码。
4.3 捕获外部变量
Lambda的强大之处在于它能捕获外部变量。比如我们需要根据一个外部定义的权重表来排序:
unordered_map<int, int> weight_table = {{0,5}, {1,3}, {4,8}, {5,2}, {8,1}}; sort(points.begin(), points.end(), [&weight_table](const auto& a, const auto& b) { return weight_table[a[0]] < weight_table[b[0]]; });这个特性在处理复杂业务逻辑时非常有用。记得我在开发一个推荐系统时,就利用这个特性实现了基于多种因素加权的动态排序。
5. 性能考量与最佳实践
5.1 比较函数与Lambda的性能
很多人担心Lambda会带来性能开销,但实际上现代编译器对简单Lambda的优化非常好。在我的测试中,对于基本类型的排序,Lambda和普通函数的性能差异可以忽略不计。
但对于复杂对象,建议尽量使用引用传递参数,避免不必要的拷贝:
// 好:使用const引用 sort(items.begin(), items.end(), [](const BigObject& a, const BigObject& b) { return a.value < b.value; }); // 不好:值传递会导致拷贝 sort(items.begin(), items.end(), [](BigObject a, BigObject b) { return a.value < b.value; });5.2 稳定排序的重要性
当排序的等价元素需要保持原有相对顺序时,应该使用stable_sort而不是sort:
vector<Student> students = {...}; stable_sort(students.begin(), students.end(), [](const auto& a, const auto& b) { return a.grade > b.grade; // 按成绩降序,同分学生保持原顺序 });在处理GUI元素排序或者有多个排序条件的场景下,这个特性特别有用。
5.3 多条件排序的技巧
当需要按多个条件排序时,可以这样组织代码:
sort(employees.begin(), employees.end(), [](const auto& a, const auto& b) { if(a.department != b.department) return a.department < b.department; if(a.salary != b.salary) return a.salary > b.salary; return a.hire_date < b.hire_date; });这种模式清晰表达了"先按部门升序,同部门按薪资降序,最后按入职时间升序"的业务逻辑。我在处理人力资源系统时,就用这种方式实现了复杂的员工排序需求。
