解析调用sort导致死循环问题的原因

记录一个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。避免快排时比较两个数大小越界的问题。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值