0

0

C++如何在STL中使用自定义排序规则

P粉602998670

P粉602998670

发布时间:2025-09-18 09:46:01

|

405人浏览过

|

来源于php中文网

原创

自定义排序规则通过提供满足严格弱序的比较器实现,可应用于std::sort、std::set、std::map、std::priority_queue等STL容器和算法,支持按多条件、对象属性或非标准逻辑排序,提升数据处理灵活性。

c++如何在stl中使用自定义排序规则

在C++的STL中,如果你想让数据按照非默认的、你自己的逻辑来排列,核心思路就是提供一个自定义的比较规则。这通常通过一个函数对象(functor)、一个lambda表达式,或者一个普通函数指针来实现,将其作为参数传递给需要排序的算法或容器。这样,STL算法就不会用它内置的

<
操作符来比较元素,而是用你给出的规则。

解决方案

在C++标准库中,自定义排序规则主要围绕着“比较器”(Comparator)的概念展开。无论你处理的是

std::sort
这样的算法,还是
std::set
std::map
std::priority_queue
这类有序容器,其原理都是一致的:提供一个可调用的实体,它接受两个元素作为参数,并返回一个
bool
值,指示第一个元素是否“小于”第二个元素(按照你的自定义规则)。这个“小于”必须满足严格弱序(Strict Weak Ordering)的要求。

1. 使用Lambda表达式(推荐方式)

这是现代C++中最灵活、最简洁的方式,特别适合于比较规则不复杂,或者只在特定位置使用一次的情况。

立即学习C++免费学习笔记(深入)”;

#include 
#include 
#include 
#include 

struct Person {
    std::string name;
    int age;
};

int main() {
    std::vector people = {
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35},
        {"David", 25}
    };

    // 按年龄升序排序
    std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) {
        return a.age < b.age;
    });

    std::cout << "Sorted by age (ascending):" << std::endl;
    for (const auto& p : people) {
        std::cout << p.name << " (" << p.age << ")" << std::endl;
    }

    // 如果年龄相同,按姓名降序排序
    std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) {
        if (a.age != b.age) {
            return a.age < b.age; // 年龄不同时,按年龄升序
        }
        return a.name > b.name; // 年龄相同时,按姓名降序
    });

    std::cout << "\nSorted by age (asc), then name (desc):" << std::endl;
    for (const auto& p : people) {
        std::cout << p.name << " (" << p.age << ")" << std::endl;
    }

    return 0;
}

2. 使用函数对象(Functor)

当比较规则比较复杂,或者需要在多个地方复用,甚至需要比较器本身维护一些状态时,函数对象是一个非常好的选择。它是一个重载了

operator()
的类。

#include 
#include 
#include 
#include 

struct Person {
    std::string name;
    int age;
};

// 按年龄降序排序的函数对象
struct ComparePersonByAgeDesc {
    bool operator()(const Person& a, const Person& b) const {
        return a.age > b.age; // 注意这里是 >
    }
};

// 另一个例子:按姓名长度升序
struct ComparePersonByNameLength {
    bool operator()(const Person& a, const Person& b) const {
        return a.name.length() < b.name.length();
    }
};

int main() {
    std::vector people = {
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35},
        {"David", 25}
    };

    std::sort(people.begin(), people.end(), ComparePersonByAgeDesc());

    std::cout << "Sorted by age (desc) using functor:" << std::endl;
    for (const auto& p : people) {
        std::cout << p.name << " (" << p.age << ")" << std::endl;
    }

    std::sort(people.begin(), people.end(), ComparePersonByNameLength());

    std::cout << "\nSorted by name length (asc) using functor:" << std::endl;
    for (const auto& p : people) {
        std::cout << p.name << " (" << p.age << ")" << std::endl;
    }

    return 0;
}

3. 使用普通函数指针

虽然不如lambda和函数对象灵活,但对于简单的、无状态的比较逻辑,也可以使用普通函数。

#include 
#include 
#include 
#include 

struct Person {
    std::string name;
    int age;
};

// 普通函数作为比较器
bool comparePeopleByNameAsc(const Person& a, const Person& b) {
    return a.name < b.name;
}

int main() {
    std::vector people = {
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35},
        {"David", 25}
    };

    std::sort(people.begin(), people.end(), comparePeopleByNameAsc);

    std::cout << "Sorted by name (asc) using function pointer:" << std::endl;
    for (const auto& p : people) {
        std::cout << p.name << " (" << p.age << ")" << std::endl;
    }

    return 0;
}

这三种方式都殊途同归,都是为了提供一个满足严格弱序的二元谓词(binary predicate),让STL算法或容器知道如何比较两个元素。

C++自定义排序规则的实际应用场景是什么?

自定义排序规则在实际开发中几乎无处不在,远不止是把数字从小到大排那么简单。我个人觉得,它真正解放了我们处理复杂数据结构的能力,让数据组织变得更加灵活和富有表现力。

想象一下,你有一个包含用户信息的列表,每个用户有ID、姓名、注册日期、活跃度等多个字段。你可能需要:

  • 按注册日期排序:最新的用户排在前面,方便查看新用户增长。
  • 按活跃度排序:最活跃的用户排在前面,用于奖励或特殊推荐。
  • 多条件复合排序:比如,先按会员等级降序,如果等级相同,再按消费金额降序,如果金额也相同,最后按注册时间升序。这种多级排序是自定义比较器最常见的应用之一,用默认的
    <
    操作符根本无法实现。
  • 自定义对象排序:你的
    struct
    class
    对象通常没有默认的
    <
    操作符,或者默认的比较逻辑不符合你的需求。比如一个
    Point
    类,你可能想按距离原点的远近排序,而不是按X坐标或Y坐标。
  • 非标准顺序排序:比如,对字符串进行不区分大小写的排序,或者按照特定的字母表顺序(例如,某些语言的特殊字符排序规则)。
  • 优先级队列的自定义
    std::priority_queue
    默认是最大堆,如果你想要一个最小堆,或者基于某个复杂逻辑的优先级队列,就必须提供自定义比较器。
  • std::set
    std::map
    的键值排序
    :如果你想用自定义对象作为
    std::set
    的元素或
    std::map
    的键,并且希望它们按照特定规则排序,那么自定义比较器是必不可少的。
  • 性能优化或特定算法需求:在某些情况下,你可能需要一个更高效的比较函数,或者某个算法(如
    std::nth_element
    )需要一个非标准的比较逻辑来找到第k个元素。

从我的经验来看,一旦你的数据结构稍微复杂一点,或者排序需求超出了简单的升序/降序,自定义比较器就会成为你的得力助手。它让代码更清晰,意图更明确,避免了为了排序而修改原始数据结构或创建临时数据结构的麻烦。

C++自定义比较函数中的常见错误有哪些?

自定义比较函数虽然强大,但它有一个非常严格的要求:必须满足“严格弱序”(Strict Weak Ordering, SWO)。这是STL算法和容器能够正确工作的基础。违反SWO是自定义比较器最常见的错误来源,而且往往会导致程序行为异常,从错误结果到崩溃,甚至无限循环都有可能。

1. 严格弱序(SWO)的核心原则

一个比较函数

comp(a, b)
返回
true
表示
a
在排序上“小于”
b
,它必须满足:

X-Node企业快速建站1.0.6.0801
X-Node企业快速建站1.0.6.0801

特色介绍: 1、ASP+XML+XSLT开发,代码、界面、样式全分离,可快速开发 2、支持语言包,支持多模板,ASP文件中无任何HTML or 中文 3、无限级分类,无限级菜单,自由排序 4、自定义版头(用于不规则页面) 5、自动查找无用的上传文件与空目录,并有回收站,可删除、还原、永久删除 6、增强的Cache管理,可单独管理单个Cache 7、以内存和XML做为Cache,兼顾性能与消耗 8、

下载
  • 非自反性(Irreflexivity)
    comp(a, a)
    必须始终为
    false
    。一个元素不能“小于”它自己。
  • 非对称性(Asymmetry):如果
    comp(a, b)
    true
    ,那么
    comp(b, a)
    必须为
    false
  • 传递性(Transitivity):如果
    comp(a, b)
    true
    comp(b, c)
    true
    ,那么
    comp(a, c)
    也必须为
    true
  • 等价性(Equivalence):如果
    comp(a, b)
    false
    comp(b, a)
    也为
    false
    ,那么
    a
    b
    被认为是等价的。这种等价关系也必须是传递的:如果
    a
    等价于
    b
    b
    等价于
    c
    ,那么
    a
    也必须等价于
    c

常见的SWO违反错误:

  • 使用
    <=
    而不是
    <
    (或
    >=
    而不是
    >
    : 这是最典型的错误。例如,如果你写
    return a.value <= b.value;
    • comp(a, a)
      会返回
      true
      (因为
      a.value <= a.value
      ),违反了非自反性。
    • 结果:程序可能崩溃,或者进入无限循环,或者排序结果不正确。
  • 不一致的比较逻辑: 当比较逻辑涉及多个字段时,很容易出错。 例如,你可能想先按年龄排序,年龄相同再按姓名排序。
    // 错误示例:可能违反SWO
    [](const Person& a, const Person& b) {
        if (a.age < b.age) return true;
        if (a.name < b.name) return true; // 这里可能导致问题
        return false;
    }

    这个例子的问题在于,如果

    a.age == b.age
    a.name > b.name
    ,它会返回
    false
    。如果
    b.age == c.age
    b.name > c.name
    ,它也会返回
    false
    。但如果
    a.age < c.age
    ,那么
    a
    应该小于
    c
    。但如果
    a.age == b.age
    a.name > b.name
    ,同时
    b.age == c.age
    b.name > c.name
    ,那么
    a
    b
    b
    c
    都被认为是等价的。但
    a
    c
    可能并非等价。 正确的写法是:

    [](const Person& a, const Person& b) {
        if (a.age != b.age) {
            return a.age < b.age;
        }
        return a.name < b.name; // 只有当年龄完全相等时才比较姓名
    }

    这种分层比较的逻辑,确保了只有在更高优先级字段相等时,才进入下一级比较。

  • 比较器捕获了不稳定的状态(针对Lambda或Functor): 如果你的Lambda通过引用捕获了外部变量,而这个变量在排序过程中被修改了,或者在Lambda被调用时已经失效,那么比较结果就会变得不稳定和不可预测。
    int sort_direction = 1; // 1 for asc, -1 for desc
    std::sort(vec.begin(), vec.end(), [&](int a, int b) {
        return (a * sort_direction) < (b * sort_direction); // 错误:sort_direction可能被修改
    });

    应该使用按值捕获

    [=]
    或者将
    sort_direction
    作为
    const
    引用捕获。

  • 性能问题: 比较函数如果执行了复杂的操作(如字符串拷贝、大量计算、IO操作),会显著拖慢排序速度,因为比较操作在排序算法中会被频繁调用。尽量保持比较函数简洁高效。
  • const
    的误解
    : 函数对象中的
    operator()
    通常应该声明为
    const
    ,因为它不应该修改对象的状态。如果它修改了,并且这个函数对象被拷贝(例如在
    std::set
    std::map
    中),那么每个拷贝可能都有不同的状态,导致行为异常。

我个人在调试这类问题时,通常会先检查比较函数是否满足

comp(a, a)
false
。如果不是,那几乎可以肯定就是
<=
之类的符号用错了。然后,对于复杂的多条件排序,我会仔细检查每一层逻辑,确保它们是互斥且递进的。一旦SWO被破坏,STL的行为是未定义的,任何奇怪的事情都可能发生。

除了std::sort,哪些C++ STL容器和算法支持自定义排序?

STL的强大之处在于其一致性和通用性。一旦你理解了自定义比较器的概念,你会发现它在许多地方都能派上用场,远不止

std::sort
。它允许我们对STL的各种组件进行精细控制,使其适应我们独特的数据组织需求。

1. 有序关联容器:

std::set
std::map

这些容器内部使用红黑树实现,它们的元素或键是自动排序的。默认情况下,它们使用

std::less
(也就是
<
操作符)来比较键。如果你想改变这个排序规则,就需要在模板参数中提供你的自定义比较器。

  • std::set
    :存储唯一且已排序的元素。

    #include 
    #include 
    #include 
    
    struct CustomStringCompare {
        bool operator()(const std::string& a, const std::string& b) const {
            // 按字符串长度降序,长度相同则按字典序升序
            if (a.length() != b.length()) {
                return a.length() > b.length(); // 注意这里是 >
            }
            return a < b;
        }
    };
    
    int main() {
        std::set mySet;
        mySet.insert("apple");
        mySet.insert("banana");
        mySet.insert("cat");
        mySet.insert("dog");
        mySet.insert("elephant");
    
        for (const auto& s : mySet) {
            std::cout << s << std::endl;
        }
        // 输出可能为:elephant, banana, apple, dog, cat (长度降序,同长度字典序)
        return 0;
    }
  • std::map
    :存储键值对,键是唯一的且已排序的。

    #include 
    #include 
    #include 
    
    // 使用上面定义的 CustomStringCompare
    int main() {
        std::map myMap;
        myMap["apple"] = 1;
        myMap["banana"] = 2;
        myMap["cat"] = 3;
        myMap["dog"] = 4;
        myMap["elephant"] = 5;
    
        for (const auto& pair : myMap) {
            std::cout << pair.first << ": " << pair.second << std::endl;
        }
        return 0;
    }

    需要注意的是,对于

    std::set
    std::map
    ,比较器是作为模板参数传递的,这意味着它在编译时就确定了,并且通常是无状态的(或者状态在构造时确定)。

2. 优先级队列:

std::priority_queue

std::priority_queue
默认是一个最大堆,也就是说,队首元素是最大的。如果你想要一个最小堆,或者基于自定义优先级的队列,同样需要提供比较器。它的比较器参数是
Compare
,表示“less than”的关系,但实际上它用于构建一个堆,使得“最大”的元素在顶部。所以,如果你想让“最小”的元素在顶部(最小堆),你的比较器应该定义“大于”的关系。

#include 
#include 
#include 

struct Task {
    std::string name;
    int priority; // 越小优先级越高
};

// 自定义比较器:让优先级小的元素排在前面(即“更大”),实现最小堆
struct CompareTasks {
    bool operator()(const Task& a, const Task& b) const {
        return a.priority > b.priority; // 如果 a 的优先级数字更大,那么 a 优先级更低,排在后面
    }
};

int main() {
    std::priority_queue, CompareTasks> taskQueue;
    taskQueue.push({"High importance", 1});
    taskQueue.push({"Medium importance", 2});
    taskQueue.push({"Low importance", 3});
    taskQueue.push({"Urgent", 0});

    while (!taskQueue.empty()) {
        std::cout << "Processing task: " << taskQueue.top().name
                  << " (Priority: " << taskQueue.top().priority << ")" << std::endl;
        taskQueue.pop();
    }
    // 输出顺序:Urgent, High importance, Medium importance, Low importance
    return 0;
}

3. 其他算法

除了

std::sort
,许多其他STL算法也接受自定义比较器,例如:

  • std::min_element
    ,
    std::max_element
    :查找序列中的最小/最大元素。
    // 查找年龄最大的Person
    auto oldest = std::max_element(people.begin(), people.end(),
                                   [](const Person& a, const Person& b) {
                                       return a.age < b.age; // 返回年龄较小的那个
                                   });
    std::cout << "Oldest person: " << oldest->name << std::endl;
  • std::nth_element
    :将序列重新排列,使得第n个元素是如果整个序列被排序后,它将位于的位置。它也会把所有“小于”它的元素放在它前面,所有“大于”它的元素放在它后面。
    // 找到年龄第三大的Person(即按年龄降序排列后的第3个)
    // 注意:这里的比较器和std::sort一样,是定义“小于”的关系
    // 所以要找第三大,相当于找按升序排列后的倒数第三个,或者写一个降序比较器
    std::nth_element(people.begin(), people.begin() + 2, people.end(),
                     [](const Person& a, const Person& b) {
                         return a.age > b.age; // 降序比较器,找第3大的
                     });
    std::cout << "Third oldest person: " << people[2].name << std::endl;
  • std::stable_sort
    :与
    std::sort
    类似,但保证相等元素的相对顺序不变。同样接受自定义比较器。
  • std::partial_sort
    :对序列的一部分进行排序。
  • std::is_sorted
    :检查序列是否已排序。
  • std::merge
    :合并两个已排序的序列。

可以说,任何涉及“比较”操作的STL算法或容器,都有可能提供自定义比较器的接口。掌握了这一核心模式,你就能更灵活、更高效地利用C++标准库来解决各种实际问题。它确实是C++泛型编程思想的一个绝佳体现。

相关专题

更多
Sass和less的区别
Sass和less的区别

Sass和less的区别有语法差异、变量和混合器的定义方式、导入方式、运算符的支持、扩展性等。本专题为大家提供Sass和less相关的文章、下载、课程内容,供大家免费下载体验。

201

2023.10.12

sort排序函数用法
sort排序函数用法

sort排序函数的用法:1、对列表进行排序,默认情况下,sort函数按升序排序,因此最终输出的结果是按从小到大的顺序排列的;2、对元组进行排序,默认情况下,sort函数按元素的大小进行排序,因此最终输出的结果是按从小到大的顺序排列的;3、对字典进行排序,由于字典是无序的,因此排序后的结果仍然是原来的字典,使用一个lambda表达式作为key参数的值,用于指定排序的依据。

387

2023.09.04

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

525

2023.09.20

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

258

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

209

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1468

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

620

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

550

2024.03.22

Java JVM 原理与性能调优实战
Java JVM 原理与性能调优实战

本专题系统讲解 Java 虚拟机(JVM)的核心工作原理与性能调优方法,包括 JVM 内存结构、对象创建与回收流程、垃圾回收器(Serial、CMS、G1、ZGC)对比分析、常见内存泄漏与性能瓶颈排查,以及 JVM 参数调优与监控工具(jstat、jmap、jvisualvm)的实战使用。通过真实案例,帮助学习者掌握 Java 应用在生产环境中的性能分析与优化能力。

19

2026.01.20

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Go 教程
Go 教程

共32课时 | 4万人学习

Go语言实战之 GraphQL
Go语言实战之 GraphQL

共10课时 | 0.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号