记录一个sort问题
2020.8.7 更新
更新说明
某位某里的“同学”说我这个错是自己写程序的问题。你的自定义比较函数直接返回true会有偏序问题,他是这么说的,如果你传递进A,B和传递进B,A都返回true的话,那么就无法判断A和B谁大,就会造成问题
。如果是在visual studio下debug模式编译器会报错。在他的“提示”下,我改用visual studio debug模式试了下。代码和下面的一致。结果是不报错。
环境
windows 10
Visual Studio Community 2019 版本16.1.3
编译截图
windows下不会报错,那么在linux下呢?
环境
Ubuntu 14.04.6 LTS
gcc 4.8.5
编译命令如下
g++ test.cc -o test -std=c++11 -DDEBUG
结果正常编译
为了完全反驳他,可以把自定义比较函数改一下
bool cmp(const int &a, const int &b)
{
if (a > b)
return false;
return true;
}
这样就不会产生所说的偏序的问题,那么这么写会不会产生问题?让我们来试一下,编译命令如下
g++ test.cc -o test -std=c++11 -DDEBUG
运行结果如图
可见,仍然出现错误。至此,问题结束。
问题复现
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
bool cmp(const int &a, const int &b)
{
return true;
}
int main()
{
vector<int> v;
for (int i = 0; i < 33; i++)
v.push_back(1);
sort(v.begin(), v.end(), cmp);
for (int i = 0; i < v.size(); i++)
cout << v[i] << endl;
return 0;
}
这段代码很简单,首先向vector里插入了33个1(只要是重复元素就会出现这个问题),然后用sort函数,利用自定义比较函数对其进行排序,最后将排序的结果输出。按道理来说,自定义排序函数如果一直返回true的结果是,不对vector内元素进行排序,但是编译执行后的结果是程序一直处于运行状态,迟迟没有结果输出。经测试,只要数组元素大于16,就会出现这种情况。百思不得其解,后来查看stl源码得到了答案。
查找问题
从stl-sort源码开始分析。源码版本5.1.5。
template <class _RandomAccessIter, class _Compare>
// 带自定义比较函数的sort函数
void sort(_RandomAccessIter __first, _RandomAccessIter __last, _Compare __comp) {
_STLP_DEBUG_CHECK(_STLP_PRIV __check_range(__first, __last))
// 这里判断 如果元素数量不为0则进行排序
if (__first != __last) {
// 先执行introsort(自省)排序
_STLP_PRIV __introsort_loop(__first, __last,
_STLP_VALUE_TYPE(__first, _RandomAccessIter),
_STLP_PRIV __lg(__last - __first) * 2, __comp);
// 之后用简单的插入排序做合并
_STLP_PRIV __final_insertion_sort(__first, __last, __comp);
}
}
自省排序:是一种混合排序方式,大部分情况下与median-of-3 Quick Sort排序算法完全相同,但是当分割行为有恶化为二次行为倾向时,能能够自我侦测,转而改用Heap Sort,使效率维持在O(NlogN)。注:二次行为倾向,看过代码后感觉就是快排的次数超过了设置的阈值,这个阈值由__lg()函数计算出,代码下面有)。
用来控制分割阈值的情况,找出2^k <= n的最大值k返回
template <class _Size>
inline _Size __lg(_Size __n) {
_Size __k;
for (__k = 0; __n != 1; __n >>= 1) ++__k;
return __k;
}
template <class _RandomAccessIter, class _Tp, class _Size, class _Compare>
// 自省排序主流程
void __introsort_loop(_RandomAccessIter __first,
_RandomAccessIter __last, _Tp*,
_Size __depth_limit, _Compare __comp) {
// 如果元素数量少于__stl_threshold,则直接返回
// __stl_threshold是一个全局常数,const int 16
while (__last - __first > __stl_threshold) {
if (__depth_limit == 0) {
// 至此,分割恶化,改用堆排序
partial_sort(__first, __last, __last, __comp);
return;
}
--__depth_limit;
// 利用快排进行排序,并返回中枢节点
_RandomAccessIter __cut =
__unguarded_partition(__first, __last,
_Tp(__median(*__first,
*(__first + (__last - __first)/2),
*(__last - 1), __comp)),
__comp);
// 对右半段进行sort
__introsort_loop(__cut, __last, (_Tp*) 0, __depth_limit, __comp);
__last = __cut;
// 现在回到while循环,对左半段进行sort
}
}
先简单说一下快排算法。分割方法不只一种,以下叙述既简单又有良好成效的做法。令头端迭代器first向尾部移动, 尾端迭代器last向头部移动。当*first大于或等千枢轴时就停下来, 当*last小于或等于枢轴时也停下来,然后检验两个迭代器是否交错。如果first仍然在左而last仍然在右, 就将两者元素互换, 然后各自调整一个位置(向中央逼近),再继续进行相同的行为。如果发现两个迭代器交错了(亦即!(first<last)),表示整个序列已经调整完毕,以此时的first为轴,将序列分为左右两半,左半部所有元素值都小于或等于枢轴,右半部所有元素值都大于或等于枢轴。
template <class _RandomAccessIter, class _Tp, class _Compare>
// 快排算法,也是书里所说的分割算法
_RandomAccessIter __unguarded_partition(_RandomAccessIter __first,
_RandomAccessIter __last,
_Tp __pivot, _Compare __comp) {
for (;;) {
// 这里调用自定义的比较函数,等到first >= pivot 元素就停下来
// 问题就出在了这,一直返回true会让fitst指针一直++,而这个函数并没有边界检查,所以就会死在这
// 返回false的话就没问题
while (__comp(*__first, __pivot)) {
_STLP_VERBOSE_ASSERT(!__comp(__pivot, *__first), _StlMsg_INVALID_STRICT_WEAK_PREDICATE)
++__first;
}
--__last;
// last找到 <= pivot 的元素就停下来
while (__comp(__pivot, *__last)) {
_STLP_VERBOSE_ASSERT(!__comp(*__last, __pivot), _StlMsg_INVALID_STRICT_WEAK_PREDICATE)
--__last;
}
// 交错,结束循环
if (!(__first < __last))
return __first;
// 大小值交换
iter_swap(__first, __last);
++__first;
}
}
接着走完整个流程。当待排序数据变为局部有序之后,就可以执行最后一步,调用插入排序来完成整个排序过程。
template <class _RandomAccessIter, class _Compare>
void __final_insertion_sort(_RandomAccessIter __first,
_RandomAccessIter __last, _Compare __comp) {
// 判断元素个数是否大于__stl_threshold
if (__last - __first > __stl_threshold) {
// 将前16调用这个函数排序
__insertion_sort(__first, __first + __stl_threshold, _STLP_VALUE_TYPE(__first,_RandomAccessIter), __comp);
// 余下调用这个函数排序
__unguarded_insertion_sort(__first + __stl_threshold, __last, __comp);
}
else
__insertion_sort(__first, __last, _STLP_VALUE_TYPE(__first,_RandomAccessIter), __comp);
}
问题原因
从头说一下流程。当我们调用sort进行排序的时候,首先会判断元素个数,如果大于16,则先进行快排(分割算法),等数据呈有序小块的时候再调用插入排序进行合并。
在快排那里,它首先要找到调用自定义比较函数返回false的那个数据(cmp(first,pivot)),如果返回true就一直向后找,注意这里是没有进行边界检查了,也就是说如果我们让cmp函数一直返回true,程序就会一直让first++,死在这里。
结论
sort函数自定义排序函数中,当两个数相等时返回false。避免快排时比较两个数大小越界的问题。