用C++ STL sort函数实现多条件合影排序:从信息学奥赛真题到工程实践

在信息学竞赛和实际开发中,排序是最基础却最考验编程功底的算法之一。OpenJudge NOI 1.10第7题"合影效果"看似简单,却完美展现了如何用C++标准库的sort函数解决复杂排序问题。这道题要求将男生按身高升序、女生按身高降序排列,最终形成男生在前、女生在后且各自有序的队列。本文将深入剖析如何通过自定义比较规则,用STL的sort函数优雅解决这类多条件排序问题。

1. 理解问题本质与排序需求

合影排序问题的核心在于处理多重排序条件。我们需要同时考虑性别优先级(男生在前)和不同性别的排序方向(男生升序、女生降序)。这种需求在实际开发中非常常见,比如电商平台需要先按商品类别分组,再按价格或销量排序。

传统解法可能将数据分成两个数组分别排序,但这样会带来额外的内存开销和代码复杂度。更优雅的方式是定义一个统一的比较规则,这正是STL sort函数的强大之处。sort函数的时间复杂度为O(nlogn),对于NOI题目中常见的n≤1e5数据规模完全够用。

提示:在算法竞赛中,理解题目隐含的排序规则比立即编码更重要。建议先用纸笔列出几个测试用例,明确输入输出关系。

2. STL sort函数的核心机制

C++标准库中的sort函数位于 <algorithm> 头文件中,其基本用法如下:

template< class RandomIt, class Compare >
void sort( RandomIt first, RandomIt last, Compare comp );

其中 comp 是比较函数对象,它决定了元素的排序规则。当不提供comp参数时,默认使用 operator< 进行升序排序。

2.1 比较函数的实现方式

实现自定义比较有三种主流方式:

  1. 普通函数 :适用于简单比较逻辑

    bool cmp(const Student& a, const Student& b) {
        // 比较逻辑
    }
    
  2. 函数对象(仿函数) :适合需要保持状态的比较

    struct Comparator {
        bool operator()(const Student& a, const Student& b) {
            // 比较逻辑
        }
    };
    
  3. 重载运算符 :最面向对象的方式

    struct Student {
        bool operator<(const Student& other) const {
            // 比较逻辑
        }
    };
    

对于合影问题,这三种方式各有优劣。我们将在下一节具体分析如何选择。

3. 解决合影问题的三种STL实现

3.1 方案一:分离数组法

这是最直观的解法,将男女数据分开存储并分别排序:

#include <algorithm>
#include <iomanip>
#include <iostream>
#include <vector>

using namespace std;

int main() {
    vector<double> males, females;
    int n;
    cin >> n;
    
    while (n--) {
        string gender;
        double height;
        cin >> gender >> height;
        
        if (gender == "male") {
            males.push_back(height);
        } else {
            females.push_back(height);
        }
    }
    
    // 男生升序
    sort(males.begin(), males.end());
    // 女生降序
    sort(females.rbegin(), females.rend());
    
    cout << fixed << setprecision(2);
    for (auto h : males) cout << h << " ";
    for (auto h : females) cout << h << " ";
    
    return 0;
}

优点

  • 逻辑简单直接
  • 易于理解和调试

缺点

  • 需要额外内存存储两个数组
  • 输出时需要两次循环

3.2 方案二:自定义比较函数

更优雅的解法是使用单一数组配合自定义比较函数:

#include <algorithm>
#include <iomanip>
#include <iostream>
#include <vector>

using namespace std;

struct Person {
    string gender;
    double height;
};

bool compare(const Person& a, const Person& b) {
    if (a.gender != b.gender) {
        return a.gender == "male"; // 男生排前面
    }
    return a.gender == "male" ? a.height < b.height : a.height > b.height;
}

int main() {
    int n;
    cin >> n;
    vector<Person> people(n);
    
    for (auto& p : people) {
        cin >> p.gender >> p.height;
    }
    
    sort(people.begin(), people.end(), compare);
    
    cout << fixed << setprecision(2);
    for (const auto& p : people) {
        cout << p.height << " ";
    }
    
    return 0;
}

关键点

  1. 性别不同时,男生优先
  2. 同性时,男生升序、女生降序

3.3 方案三:运算符重载

面向对象风格的最佳实践是重载 operator<

#include <algorithm>
#include <iomanip>
#include <iostream>
#include <vector>

using namespace std;

struct Person {
    string gender;
    double height;
    
    bool operator<(const Person& other) const {
        if (gender != other.gender) {
            return gender == "male";
        }
        return gender == "male" ? height < other.height : height > other.height;
    }
};

int main() {
    int n;
    cin >> n;
    vector<Person> people(n);
    
    for (auto& p : people) {
        cin >> p.gender >> p.height;
    }
    
    sort(people.begin(), people.end());
    
    cout << fixed << setprecision(2);
    for (const auto& p : people) {
        cout << p.height << " ";
    }
    
    return 0;
}

优势

  • 代码更简洁
  • 比较逻辑与数据结构绑定
  • 可以自然用于其他需要比较的场景

4. 工程实践中的扩展应用

合影排序问题虽然简单,但其解决方案可以扩展到许多实际场景。以下是几个典型案例:

4.1 多条件排序的通用模式

当需要按多个字段排序时,可以遵循以下模式:

bool compare(const Item& a, const Item& b) {
    if (a.field1 != b.field1) {
        return a.field1 < b.field1; // 第一优先级
    }
    if (a.field2 != b.field2) {
        return a.field2 > b.field2; // 第二优先级
    }
    return a.field3 < b.field3; // 第三优先级
}

4.2 性能优化技巧

对于大型数据集,可以考虑以下优化:

  1. 移动语义 :确保数据结构支持移动构造

    struct Person {
        string gender;
        double height;
        // 添加移动构造函数
        Person(Person&&) = default;
    };
    
  2. 缓存友好 :将频繁比较的字段放在结构体开头

    struct Person {
        char gender[6]; // 改为固定数组避免堆分配
        double height;
    };
    
  3. 并行排序 :对于超大数据集

    #include <execution>
    sort(execution::par, people.begin(), people.end());
    

4.3 常见陷阱与调试技巧

陷阱1 :比较函数不符合严格弱序

// 错误示例:不满足传递性
bool cmp(int a, int b) {
    return abs(a) <= abs(b);
}

陷阱2 :修改正在排序的元素

vector<Person> people;
// 错误:在比较函数中访问people会导致未定义行为
sort(people.begin(), people.end(), [&](auto& a, auto& b) {
    return people.size() > 0 && a.height < b.height;
});

调试技巧

  • 在比较函数中添加日志
  • 使用 std::is_sorted 验证结果
  • 对小数据集进行单步调试

5. 从算法竞赛到实际工程

信息学奥赛题目往往提炼自实际问题。合影排序问题可以延伸至以下工程场景:

  1. 电商商品排序 :先按品类,再按价格/销量
  2. 任务调度系统 :先按优先级,再按截止时间
  3. 数据分析报表 :多维度数据透视排序

一个真实案例是为在线教育平台实现课程排序:

struct Course {
    bool isPremium;
    double rating;
    int studentCount;
    Date publishDate;
    
    bool operator<(const Course& other) const {
        if (isPremium != other.isPremium)
            return isPremium;
        if (rating != other.rating)
            return rating > other.rating;
        if (studentCount != other.studentCount)
            return studentCount > other.studentCount;
        return publishDate > other.publishDate;
    }
};

在工程实践中,还需要考虑:

  • 排序稳定性(stable_sort)
  • 异常安全性
  • 多线程环境下的同步
  • 内存占用与缓存效率

更多推荐