本文后半部分转自: 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; }
以上写法有两个问题:
- 从使用上来说,传递给
sort
的cmp
函数,就不应该使用等号 - 从后果上来说,上述用法不仅不能解决问题,还可能导致程序coredump
- 从使用上来说,传递给
-
排序算法稳定性
- 排序算法稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,
ri=rj
,且ri
在rj
之前,而在排序后的序列中,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
得出的结论是10A
与10B
不等值,因此不一样,于是它将10B
加入容器。在技术上而言,这个行动导致未定义的行为,但是通常的结果是set
终结于拥有了两个值为10
的元素的拷贝,也就是说它不再是一个set
了。通过使用less_equal
作为我们的比较类型,我们破坏了容器!
-
-
此外,所有对相同的元素返回
true
的比较函数都会做相同的事情。根据定义,相同的元素,是不等值的!因此我们需要确保用在关联容器上的比较函数总是对相同的元素返回false
。 -
要避免掉入这个陷阱,你所要记住的就是比较函数的返回值指明的是:在此函数定义的排序方式下,一个元素是否应该位于另一个之前。对于关联容器,相同的元素绝不该一个领先于另一个,所以比较函数总应该为相同的元素返回
false
。 -
以上的讨论是针对关联容器的。从技术上讲,用于关联容器的比较函数必须在它们所比较的对象上定义一个
strict weak ordering
。其实,传给sort
等泛型算法的比较函数也有同样的限制。如果一个comp函数要满足“Strict Weak Ordering”,意味着它应该满足如下特征:-
非自反性(irreflexive)
comp(x, x)
必须为假 -
非对称性(antisymmetric)
comp(x, y)
和comp(y, x)
的结果必然相反 -
可传递性(transitive)
如果
comp(x, y)
为真,comp(y, z)
为真,那么comp(x, z)
必然为真
-
-
所以现在可以看到,示例错误的代码的
comp
定义明显违反了非自反性和非对称性,所以sort使用它时就可能工作不正常。解决办法也很简单,去掉那个=
,再对照下Strict Weak Ordering
的定义,就满足了。