C++使用sort排序导致runtime error(strict weak ordering)

本文讲述了在C++中自定义排序函数时的常见错误,重点强调了比较函数在处理相同元素时应返回false的重要性,避免了可能导致的程序崩溃或未定义行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文后半部分转自: https://blog.youkuaiyun.com/jiange_zh/article/details/78240806

1. 教训: 永远让比较函数对相同元素返回false!

今天在写一道medium难度的leetcode题,写完逻辑之后提交,发现输入的数组里面的每一个元素并不是排好序的,因此萌生了先排序的想法,从《C++ Primer》中参考了这样的一段代码:

stable_sort(words.begin(), words.end(),
            [](const string& a, string& b)
            { return a.length<=b.length; });

刚开始照抄的时候,发现编译不通过,犹豫一秒之后换成了sort

// 错误的写法!
sort(intervals.begin(), intervals.end(),
        [](const Interval& a, Interval& b)
        { return a.start <= b.start; });

然后提交就一直是runtime error,但是Run Code中我输入长度为5的数组是正确的,出现问题的是leetcode提供的一个超长超长的数组。

一开始我以为是输入太长的原因导致的超时,换了python写,就通过了。但是我点开了Discuss,有人和我的思路基本一致,但是他居然是8ms,而我就runtime error。不服气的我试着搜了一下,原来问题出在排序上,并且超时的原因是数组越界。正确写法是:

// 正确的写法
sort(intervals.begin(), intervals.end(),
        [](const Interval& a, Interval& b)
        { return a.start < b.start; });

然后就12ms通过,打败97.32%的提交。

2. 转载内容(有小部分删改及整理)

下面内容转自这篇博文

  • 当我们需要自定义排序规则时,需要实现一个比较函数,该函数类似如下:

    bool cmp(int a, int b) {
        return a>b;
    }
    

    cmp返回true时,a将会排在b前面,因此上面的函数将从大到小排序。

    换句话说,cmp函数重新定义了“小”的概念(当a>b时,a“小于”b),整个序列将按照这个“小”的规则从“小”到“大”排序。

  • 那么,如果对一组数据从小到大排序,对于值相等的两个元素,经过排序之后,原本靠后的元素排在前面。于是出现如下的比较函数:

    // 错误的写法!
    bool cmp(int a, int b) {
        return a<=b;
    }
    

    以上写法有两个问题:

    • 从使用上来说,传递给sortcmp函数,就不应该使用等号
    • 从后果上来说,上述用法不仅不能解决问题,还可能导致程序coredump
  • 排序算法稳定性

    • 排序算法稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且rirj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的
    • sort内部使用的排序算法不一定是稳定的(当元素较多时,使用的快速排序是不稳定的),对于一个不稳定的算法,是无法保证值相同的两个元素的顺序的
  • coredump的原因分析

    对于std::sort(),当容器里面元素的个数大于_S_threshold的枚举常量值时,会使用快速排序(STL的这个默认值是16)。

    STL快速排序源码的关键代码:

    template<typename _RandomAccessIterator, typename _Tp, typename _Compare>
     _RandomAccessIterator
     __unguarded_partition(_RandomAccessIterator __first,
                           _RandomAccessIterator __last,
                           _Tp __pivot, _Compare __comp)
     {
       while (true)
         {
           while (__comp(*__first, __pivot))
             ++__first;
           --__last;
           while (__comp(__pivot, *__last))
             --__last;
           if (!(__first < __last))
             return __first;
           std::iter_swap(__first, __last);
           ++__first;
         }
     }
    

    注意到:

    while (__comp(*__first, __pivot))
         ++__first;
    
    • 这两行代码里面,__pivot是中间值,__first是迭代器,假设我们的__comp函数对于相等的值返回true,那么如果一个容器里面最后一段元素所有值都相等,那么__comp(*__first,__pivot)就恒为真。迭代器往前移的时候,终会移过最后一个元素,于是迭代器失效,程序core。

    可以看到,上面快排的思路是:

    • 从左往右找一个比中间值“大”(即cmp函数返回false)的元素,从右往左找一个比中间值“小”元素,然后交换两个元素的位置,使得大的在右边,小的在左边。
    • cmp函数return a < b时,如果所有元素已经是有序的了,只要遇到一个值跟中间元素相等的元素(包括中间元素自己),cmp便返回false__first迭代器右移停止。
    • 而当cmp函数return a <= b时,若中间元素前面的元素都比它小,而后面的元素都跟它相等或者比它小,那么cmp恒返回true__first迭代器不断右移,程序越界导致coredump。
  • 其实类似的问题在《Effective C++》中的条款21就有讨论:

    • 构建一个set,比较类型用的是less_equal,然后insert一个10

      // 错误写法!
      set<int, less_equal<int>> s;       // s is sorted by “<=”
      s.insert(10);                    // insert the value 10
      
    • 对于这一个insert的调用,set必须先要搞明白10是否已经位于其中。 我们知道它已经位于其中,但set可是木头木脑的,它必须执行检查。为了便于弄明白发生了什么,我们将已经在set中的10称为10A,而正试图insert的那个叫10B

    • set遍历它的内部数据结构以查找加入10B的位置。最终,它总要检查10B是否与10A相同。关联容器对“相同”的定义是equivalence(equivalence应指“数学相等”,two elements are equal if neither is less than the other,见《The C++ Standard Library》中文版P82,英文版电子P77;equality指“逻辑等价”,使用operator==(),见《The Standard Template Library》英文电子版P30)

    • 因此set测试10B是否数学等值于10A。 当执行这个测试时,它自然是使用set的比较函数。在这一例子里,是operator<=,因为我们指定set的比较函数为less_equal,而less_equal就是operator<=。于是,set将计算这个表达式是否为真:

      !(10 A <= 10 B ) && !(10 B <= 10 A )       // test 10 A and 10 B for equivalence
      
    • 结果当然是false。 也就是说,set得出的结论是10A10B不等值,因此不一样,于是它将10B加入容器。在技术上而言,这个行动导致未定义的行为,但是通常的结果是set终结于拥有了两个值为10的元素的拷贝,也就是说它不再是一个set了。通过使用less_equal作为我们的比较类型,我们破坏了容器!

  • 此外,所有对相同的元素返回true的比较函数都会做相同的事情。根据定义,相同的元素,是不等值的!因此我们需要确保用在关联容器上的比较函数总是对相同的元素返回false

  • 要避免掉入这个陷阱,你所要记住的就是比较函数的返回值指明的是:在此函数定义的排序方式下,一个元素是否应该位于另一个之前。对于关联容器,相同的元素绝不该一个领先于另一个,所以比较函数总应该为相同的元素返回false

  • 以上的讨论是针对关联容器的。从技术上讲,用于关联容器的比较函数必须在它们所比较的对象上定义一个strict weak ordering。其实,传给sort等泛型算法的比较函数也有同样的限制。如果一个comp函数要满足“Strict Weak Ordering”,意味着它应该满足如下特征:

    1. 非自反性(irreflexive)

      comp(x, x)必须为假

    2. 非对称性(antisymmetric)

      comp(x, y)comp(y, x)的结果必然相反

    3. 可传递性(transitive)

      如果comp(x, y)为真,comp(y, z)为真,那么comp(x, z)必然为真

  • 所以现在可以看到,示例错误的代码的comp定义明显违反了非自反性和非对称性,所以sort使用它时就可能工作不正常。解决办法也很简单,去掉那个=,再对照下Strict Weak Ordering的定义,就满足了。

C++ 中,`std::sort` 是 `<algorithm>` 头文件中提供的排序函数,用于对数组或容器中的元素进行升序或自定义排序。以下是详细用法和示例: --- ### **1. 基本用法** #### **对数组排序** ```cpp #include <algorithm> #include <iostream> int main() { int arr[] = {5, 2, 9, 1, 5, 6}; int n = sizeof(arr) / sizeof(arr[0]); // 默认升序排序 std::sort(arr, arr + n); // 输出结果 for (int i = 0; i < n; i++) { std::cout << arr[i] << " "; } // 输出:1 2 5 5 6 9 return 0; } ``` #### **对 `std::vector` 排序** ```cpp #include <algorithm> #include <vector> #include <iostream> int main() { std::vector<int> vec = {5, 2, 9, 1, 5, 6}; // 默认升序排序 std::sort(vec.begin(), vec.end()); // 输出结果 for (int num : vec) { std::cout << num << " "; } // 输出:1 2 5 5 6 9 return 0; } ``` --- ### **2. 自定义排序规则** #### **降序排序** ```cpp // 方法1:使用 lambda 表达式 std::sort(arr, arr + n, [](int a, int b) { return a > b; // 降序 }); // 方法2:使用标准库函数对象 #include <functional> std::sort(arr, arr + n, std::greater<int>()); ``` #### **按结构体/类的成员排序** ```cpp #include <algorithm> #include <vector> #include <string> struct Person { std::string name; int age; }; int main() { std::vector<Person> people = {{"Alice", 25}, {"Bob", 20}, {"Charlie", 22}}; // 按年龄升序排序 std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) { return a.age < b.age; }); // 输出结果 for (const auto& p : people) { std::cout << p.name << ": " << p.age << std::endl; } // 输出: // Bob: 20 // Charlie: 22 // Alice: 25 return 0; } ``` --- ### **3. 排序部分范围** ```cpp int arr[] = {5, 2, 9, 1, 5, 6}; int n = sizeof(arr) / sizeof(arr[0]); // 仅排序前4个元素 std::sort(arr, arr + 4); // 结果:1 2 5 9 5 6 // 对 vector 的中间部分排序 std::vector<int> vec = {5, 2, 9, 1, 5, 6}; std::sort(vec.begin() + 1, vec.begin() + 4); // 排序索引 [1, 4) 的元素 // 结果:5 1 2 9 5 6 ``` --- ### **4. 稳定排序(`std::stable_sort`)** - `std::sort` 不保证相等元素的相对顺序,而 `std::stable_sort` 会保留。 ```cpp #include <algorithm> #include <vector> int main() { std::vector<std::pair<int, char>> vec = {{3, 'a'}, {1, 'b'}, {2, 'c'}, {2, 'd'}}; // 稳定排序:先按数字升序,再按字符升序 std::stable_sort(vec.begin(), vec.end(), [](const auto& a, const auto& b) { return a.first < b.first; }); // 输出结果 for (const auto& p : vec) { std::cout << p.first << ":" << p.second << " "; } // 输出:1:b 2:c 2:d 3:a (相等元素的顺序保留) return 0; } ``` --- ### **5. 性能说明** - **时间复杂度**: - 平均和最坏情况下为 \(O(N \log N)\),其中 \(N\) 是元素数量。 - **底层实现**: - 通常使用快速排序(如 `std::__introsort`)或混合算法(快速排序 + 堆排序 + 插入排序)。 - **稳定性**: - `std::sort` 不保证稳定性,`std::stable_sort` 保证。 --- ### **6. 注意事项** 1. **自定义比较函数**: - 必须满足严格弱序(Strict Weak Ordering),否则行为未定义。 - 例如,错误的比较函数: ```cpp // 错误:可能导致未定义行为 std::sort(arr, arr + n, [](int a, int b) { return a >= b; // 不满足严格弱序 }); ``` 2. **对容器排序**: - 仅支持随机访问迭代器(如 `vector`、`deque`、普通数组),不适用于 `list`(需用 `list::sort()`)。 3. **大数据量优化**: - 如果数据量较大且部分有序,`std::stable_sort` 可能比 `std::sort` 更高效。 --- ### **示例:综合用法** ```cpp #include <algorithm> #include <vector> #include <iostream> #include <string> int main() { std::vector<std::string> words = {"apple", "banana", "cherry", "date"}; // 按字符串长度升序排序 std::sort(words.begin(), words.end(), [](const std::string& a, const std::string& b) { return a.size() < b.size(); }); // 输出结果 for (const auto& word : words) { std::cout << word << " "; } // 输出:date apple banana cherry return 0; } ``` --- ### **相关问题** 1. `std::sort` 和 `std::stable_sort` 的区别是什么? 2. 如何对自定义对象的多字段排序? 3. 为什么 `std::sort` 的比较函数必须满足严格弱序? 4. 在 C++ 中如何对链表(`std::list`)进行排序? 5. 如何优化 `std::sort` 的性能(如自定义内存分配或并行排序)?
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值