STL源码分析之std::sort

一.原理分析:

首先,我们要知道STL中的sort用到了快速排序,堆排序和插入排序.这几种基础排序在这里就不讲了.

但是我们要知道这几种排序的优缺点:

快速排序:

               时间复杂度:平均O(N*logN),最坏O(N^2); 优点:大部分情况性能很好.缺点:时间复杂度不稳定,数据量大时递归深度大.

堆排序:

            时间复杂度:O(N*logN); 优点:时间复杂度稳定,适合数据量大的数据. 缺点:一:堆排序对数据的访问是跳跃的,对缓存不友好.二:建堆时会降低数据有序度,使堆排序的交换次数比快速排序多,时间常数也更大.

插入排序:

               时间复杂度:O(N^2); 优点:数据量小或数据基本有序时效率高.缺点:时间复杂度高.

std::sort就是根据这几种排序的优缺点进行了结合.

  1. 快速排序到 2*logN 的深度限制后采用堆排序。
  2. 快速排序+堆排序后,数据已经被分为多个数据量小于等于阈值(16)的子区域,子区域里面的数据可能是无序的,但是子区域之间已经是有序了。
  3. 最后整个基本有序数据区域采用插入排序,达到完全有序。

其中快排+堆排的结合就是introsort(内省排序)

二.std::sort 源码:

 template<typename _RandomAccessIterator, typename _Compare>
    inline void
    __sort(_RandomAccessIterator __first, _RandomAccessIterator __last,
       _Compare __comp)//comp默认为less()
    {
      if (__first != __last)
    {
      /* __introsort_loop先进行一遍IntroSort,但是不是严格意义上的IntroSort。
      *因为执行完之后区间并不是完全有序的,而是基本有序的。
      *__introsort_loop和IntroSort不同的地方是,__introsort_loop会在一开始会判断区间的大小,
      *当区间小于16的时候,就直接返回。这样就得到了很多个区间有序的小分块.
      *第三个参数是获得2*logN,为递归深度限制,当达到此限制时改用堆排序.*/
      std::__introsort_loop(__first, __last,
                std::__lg(__last - __first) * 2,
                __comp); 
      // 在区间基本有序的基础上再做一遍插入排序,使区间完全有序
      std::__final_insertion_sort(__first, __last, __comp);
    }
    }

2.1 __introsort_loop代码:

  enum { _S_threshold = 16 };//分块阈值

  template<typename _RandomAccessIterator, typename _Size, typename _Compare>
    void
    __introsort_loop(_RandomAccessIterator __first,
             _RandomAccessIterator __last,
             _Size __depth_limit, _Compare __comp)
    {
      // 若区间大小<=16就不再排序。
      while (__last - __first > int(_S_threshold))
    {
      // 若递归深度达到限制,就改用堆排序
      if (__depth_limit == 0)
        {
          std::__partial_sort(__first, __last, __last, __comp);
          return;
        }
      --depth_limit;
        /* 递归排序,并返回排序区域被分成两个区域后,区域的分界位置 __cut。 */
        _RandomAccessIterator __cut =
        std::__unguarded_partition_pivot(__first, __last, __comp);
        std::__introsort_loop(__cut, __last, __depth_limit, __comp);//对右半区域递归调用
        __last = __cut; // 单边递归,这么做能减少次数
    }
}

2.1.1 __introsort_loop中用到的函数代码

template<typename _RandomAccessIterator, typename _Compare>
inline _RandomAccessIterator
__unguarded_partition_pivot(_RandomAccessIterator __first,
            _RandomAccessIterator __last, _Compare __comp) {
    _RandomAccessIterator __mid = __first + (__last - __first) / 2;
    /* 比较三个值:第一个位置 ,最后一个位置,中间位置这三个值。
     * 哪个位置上的数据处于中间的(a < b < c 取 b)作为快速排序的哨兵。
     * 然后将哨兵数据与第一个位置数据交换。 */
    std::__move_median_to_first(__first, __first + 1, __mid, __last - 1, __comp);
    /* 第一个位置上的数据是哨兵,那么将 [__first + 1, __last) 这个区间的数据
     * 根据哨兵的值分成左右两部分.*/
    return std::__unguarded_partition(__first + 1, __last, __first, __comp);
}

/* 三个数取中值,放在 __result 位置。*/
template<typename _Iterator, typename _Compare>
void __move_median_to_first(_Iterator __result,_Iterator __a, _Iterator __b,
            _Iterator __c, _Compare __comp) {
    if (__comp(__a, __b)) {
        if (__comp(__b, __c))
            std::iter_swap(__result, __b);
        else if (__comp(__a, __c))
            std::iter_swap(__result, __c);
        else
            std::iter_swap(__result, __a);
    }
    else if (__comp(__a, __c))
        std::iter_swap(__result, __a);
    else if (__comp(__b, __c))
        std::iter_swap(__result, __c);
    else
        std::iter_swap(__result, __b);
}

/* 根据哨兵,将数据区域分成两部分,并返回区域分界位置。 */
template<typename _RandomAccessIterator, typename _Compare>
_RandomAccessIterator
__unguarded_partition(_RandomAccessIterator __first,
            _RandomAccessIterator __last,
            _RandomAccessIterator __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;
    }
}

2.2 __final_insertion_sort代码:

  template<typename _RandomAccessIterator, typename _Compare>
    void
    __final_insertion_sort(_RandomAccessIterator __first,
               _RandomAccessIterator __last, _Compare __comp)
    {
      if (__last - __first > int(_S_threshold)) // 区间长度大于16
    {
      // 插入排序
      std::__insertion_sort(__first, __first + int(_S_threshold), __comp); 
      // 也是插入排序,只是在插入排序的内循环时,不再判断边界条件,因为已经保证了区间前面肯定有比待插入元素更小的元素
      std::__unguarded_insertion_sort(__first + int(_S_threshold), __last, 
                      __comp);
    }
      else // 区间长度小于等于16的话
    std::__insertion_sort(__first, __last, __comp); // 插入排序
    }

2.2.1 __final_insertion_sort用到的函数代码:

template <typename _RandomAccessIterator, typename _Compare>
void
__insertion_sort(_RandomAccessIterator __first,
                    _RandomAccessIterator __last, _Compare __comp) {
    if (__first == __last) return;

    for (_RandomAccessIterator __i = __first + 1; __i != __last; ++__i) {
        /* 如果当前数据比第一个数据还要小,[__first, __i) 区域数据向右移动一个位置,
         * __i 数据放在首位。 */
        if (__comp(__i, __first)) {
            typename iterator_traits<_RandomAccessIterator>::value_type
                __val = _GLIBCXX_MOVE(*__i);
            _GLIBCXX_MOVE_BACKWARD3(__first, __i, __i + 1);
            *__first = _GLIBCXX_MOVE(__val);
        } else
            /* __i 位置上的数据作为要插入的数据,
             * 从右到左(__i 位置开始)逐个数据进行比较排序,直到不满足条件再停下来。 */
            std::__unguarded_linear_insert(__i,
                __gnu_cxx::__ops::__val_comp_iter(__comp));//直接插入
    }
}

template <typename _RandomAccessIterator, typename _Compare>
void
__unguarded_linear_insert(_RandomAccessIterator __last, _Compare __comp) {
    typename iterator_traits<_RandomAccessIterator>::value_type
        __val = _GLIBCXX_MOVE(*__last);
    _RandomAccessIterator __next = __last;
    --__next;
    while (__comp(__val, __next)) {
        *__last = _GLIBCXX_MOVE(*__next);
        __last = __next;
        --__next;
    }
    *__last = _GLIBCXX_MOVE(__val);
}

注意:因为 __unguarded_linear_insert 内部 while 循环没有作边界检查,所以 __comp 函数两个参数的比较必须是 v1 > v2 或者 v1 < v2,不能 v1 >= v2 或者 v1 <= v2 这样的,否则程序可能会因为内存越界崩溃.

三.总结:

C++ STL std::sort综合了堆排+快排+插入排序三种排序,限制了递归深度,与分块大小。当递归深度大于2*logn时说明进入了快排陷阱。(如果分块的key值选的好最小深度就是logn)转为使用堆排序。快排/堆排后,可以使得块间相对有序。再使用插入排序加速整个排序结果(插入排序最好的情况下时间复杂度为O(n),擅长对相对有序的数据进行排序)

参考文章:[stl 源码分析] std::sort - 知乎 (zhihu.com)

                C++ STL 源码阅读 (四): sort - 简书 (jianshu.com)

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值