科普:std::sort干了什么

std::sort算是STL中对OIer比较友好的函数了,但你有想过sort是如何保证它的高速且稳定吗?

 

正文

我们首先来到第一层:sort函数

template<typename _RandomAccessIterator>
inline void sort(_RandomAccessIterator __first, _RandomAccessIterator __last)
{
    //申请使用随机访问迭代器 
    __glibcxx_function_requires(_Mutable_RandomAccessIteratorConcept<_RandomAccessIterator>)
    //申请使用内置的__gnu_cxx::__ops::__iter_less_iter函数 
    __glibcxx_function_requires(_LessThanComparableConcept<typename iterator_traits<_RandomAccessIterator>::value_type>)
    //声明有效区间 
    __glibcxx_requires_valid_range(__first, __last);
    //推锅给std::__sort函数 
    std::__sort(__first, __last, __gnu_cxx::__ops::__iter_less_iter());
}

 

这一层其实也没干什么,只是把锅推给了第二层:__sort函数

template<typename _RandomAccessIterator, typename _Compare>
inline void __sort(_RandomAccessIterator __first, _RandomAccessIterator __last, _Compare __comp)
{
    if (__first != __last)
    {
        //有限制地O(n log n)排序(复杂度优秀,但常数大),将数据几乎有序 
        std::__introsort_loop(__first, __last, std::__lg(__last - __first) * 2, __comp);
        //当数据较有序时,用常数比较小的插入排序(复杂度差,但常数优秀) 
        std::__final_insertion_sort(__first, __last, __comp);
    }
}

这里我们就可以见到当年那些大神的神奇操作了:不同的排序方法各司其职,取长补短

 

接下来我们分开来看,首先看O(n log n)排序的部分:__introsort_loop函数

 

在所有O(n log n)排序中,常数最优秀的当属快速排序,它自然也成了实现O(n log n)排序的首要选择

当然,直接用快速排序是很可能会被卡的,所以我们要用一个另外的函数兜底

template<typename _RandomAccessIterator, typename _Size, typename _Compare>
void __introsort_loop(_RandomAccessIterator __first, _RandomAccessIterator __last, _Size __depth_limit, _Compare __comp)
{
    //如果排序区间较大,复杂度对效率的影响超过了算法的长度,则使用快速排序 
    while (__last - __first > int(_S_threshold))
    {
        //如果快速排序的层数过大,说明数据对快速排序不友好 
        if (__depth_limit == 0)
        {
            //改用堆排序 
            std::__partial_sort(__first, __last, __last, __comp);
            return;
        }
        --__depth_limit;
        //将数据分为两个集合 
        _RandomAccessIterator __cut = std::__unguarded_partition_pivot(__first, __last, __comp);
        //将后一半递归排序 
        std::__introsort_loop(__cut, __last, __depth_limit, __comp);
        //继续排序前一半 
        __last = __cut;
        //其实这个方法很骚,只会向下增加一次递归,另一层用循环代替 
        //对栈空间的影响比直接两次递归小了不少 
    }
}

 小声BB:这个参照值的选取太随便了:__unguarded_partition_pivot函数

template<typename _RandomAccessIterator, typename _Compare>
inline _RandomAccessIterator __unguarded_partition_pivot(_RandomAccessIterator __first, _RandomAccessIterator __last, _Compare __comp)
{
    //选取中间值 
    _RandomAccessIterator __mid = __first + (__last - __first) / 2;
    //选取first + 1, mid, last - 1的三个位置的中位数作为参照值,并存储在first这个位置上 
    //里面的函数实现太暴力了,全是if(比暴力还暴力),就不拿出来了 
    std::__move_median_to_first(__first, __first + 1, __mid, __last - 1, __comp);
    //快排标准移动,实现如下 
    return std::__unguarded_partition(__first + 1, __last, __first, __comp);
}
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;
    }
}

 

如果用快速排序太卡,就改用堆排序:__partial_sort函数

template<typename _RandomAccessIterator, typename _Compare>
inline void __partial_sort(_RandomAccessIterator __first, _RandomAccessIterator __middle, _RandomAccessIterator __last, _Compare __comp)
{
    //建堆 
    std::__heap_select(__first, __middle, __last, __comp);
    //弹堆 
    std::__sort_heap(__first, __middle, __comp);
}

template<typename _RandomAccessIterator, typename _Compare>
void __heap_select(_RandomAccessIterator __first, _RandomAccessIterator __middle, _RandomAccessIterator __last, _Compare __comp)
{
    //建堆 
    //估计这个算法是用堆得到优先级最大的多个元素,所有会有一个空循环 
    std::__make_heap(__first, __middle, __comp);
    for (_RandomAccessIterator __i = __middle; __i < __last; ++__i)
        if (__comp(__i, __first))
            std::__pop_heap(__first, __middle, __i, __comp);
}

template<typename _RandomAccessIterator, typename _Compare>
void __make_heap(_RandomAccessIterator __first, _RandomAccessIterator __last, _Compare __comp)
{
    typedef typename iterator_traits<_RandomAccessIterator>::value_type _ValueType;
    typedef typename iterator_traits<_RandomAccessIterator>::difference_type _DistanceType;
    
    if (__last - __first < 2)
        return;
    
    const _DistanceType __len = __last - __first;
    _DistanceType __parent = (__len - 2) / 2;
    while (true)
    {
        //从堆底向堆顶更新 
        _ValueType __value = _GLIBCXX_MOVE(*(__first + __parent));
        std::__adjust_heap(__first, __parent, __len, _GLIBCXX_MOVE(__value), __comp);
        if (__parent == 0)
            return;
        __parent--;
    }
}

template<typename _RandomAccessIterator, typename _Compare>
void __sort_heap(_RandomAccessIterator __first, _RandomAccessIterator __last, _Compare __comp)
{
    //pop and pop 
    while (__last - __first > 1)
    {
        --__last;
        std::__pop_heap(__first, __last, __last, __comp);
    }
}

当然,STL延续了一贯大常数的“祖宗之法”,调整堆和弹堆都如此复杂

template<typename _RandomAccessIterator, typename _Distance, typename _Tp, typename _Compare>
void __push_heap(_RandomAccessIterator __first, _Distance __holeIndex, _Distance __topIndex, _Tp __value, _Compare __comp)
{
    //向上跳,直到堆稳定为止 
    _Distance __parent = (__holeIndex - 1) / 2;
    while (__holeIndex > __topIndex && __comp(__first + __parent, __value))
    {
        *(__first + __holeIndex) = _GLIBCXX_MOVE(*(__first + __parent));
        __holeIndex = __parent;
        __parent = (__holeIndex - 1) / 2;
    }
    *(__first + __holeIndex) = _GLIBCXX_MOVE(__value);
}
    
template<typename _RandomAccessIterator, typename _Distance, typename _Tp, typename _Compare>
void __adjust_heap(_RandomAccessIterator __first, _Distance __holeIndex, _Distance __len, _Tp __value, _Compare __comp)
{
    //调整堆 
    //调整方法:先无脑移动到堆底,再向上更新 
    const _Distance __topIndex = __holeIndex;
    _Distance __secondChild = __holeIndex;
    while (__secondChild < (__len - 1) / 2)
    {
        __secondChild = 2 * (__secondChild + 1);
        if (__comp(__first + __secondChild, __first + (__secondChild - 1)))
            __secondChild--;//选择优先级较高的儿子 
        *(__first + __holeIndex) = _GLIBCXX_MOVE(*(__first + __secondChild));
        __holeIndex = __secondChild;//向下调整 
    }//如果只有一个儿子 
    if ((__len & 1) == 0 && __secondChild == (__len - 2) / 2)
    {
        __secondChild = 2 * (__secondChild + 1);
        *(__first + __holeIndex) = _GLIBCXX_MOVE(*(__first  + (__secondChild - 1)));
        __holeIndex = __secondChild - 1;
    }
    //向上更新 
    std::__push_heap(__first, __holeIndex, __topIndex, _GLIBCXX_MOVE(__value), __gnu_cxx::__ops::__iter_comp_val(__comp));
}

template<typename _RandomAccessIterator, typename _Compare>
inline void __pop_heap(_RandomAccessIterator __first, _RandomAccessIterator __last, _RandomAccessIterator __result, _Compare __comp)
{
    typedef typename iterator_traits<_RandomAccessIterator>::value_type _ValueType;
    typedef typename iterator_traits<_RandomAccessIterator>::difference_type _DistanceType;
    //删除堆顶 
    _ValueType __value = _GLIBCXX_MOVE(*__result);
    *__result = _GLIBCXX_MOVE(*__first);
    //调整堆 
    std::__adjust_heap(__first, _DistanceType(0), _DistanceType(__last - __first), _GLIBCXX_MOVE(__value), __comp);
}

 

当比较有序时,我们就可以用插入排序优化常数:__final_insertion_sort函数

template<typename _RandomAccessIterator, typename _Compare>
void __final_insertion_sort(_RandomAccessIterator __first, _RandomAccessIterator __last, _Compare __comp)
{
    if (__last - __first > int(_S_threshold))
    { 
        //先将序列开头排序,作为后面插入排序的预排序区间 
        std::__insertion_sort(__first, __first + int(_S_threshold), __comp);
        //将后面的所有元素排序 
        std::__unguarded_insertion_sort(__first + int(_S_threshold), __last, __comp);
    }
    else
        std::__insertion_sort(__first, __last, __comp);//如果序列较短,就直接排序 
}

实现方法也很简单,只是有一点奇怪的操作:

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)
    {
        //如果插入位置为序列开头,那么直接移动整个序列??? 
        //什么骚操作??? 
        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
            std::__unguarded_linear_insert(__i, __gnu_cxx::__ops::__val_comp_iter(__comp));
    }
}
template<typename _RandomAccessIterator, typename _Compare>
inline void __unguarded_insertion_sort(_RandomAccessIterator __first, _RandomAccessIterator __last, _Compare __comp)
{
    //十分老实的插入排序 
    for (_RandomAccessIterator __i = __first; __i != __last; ++__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);
}

 事实上我真的去测试过,在基本有序时,快排真的比插入排序慢(常数太大了)

 

总结,sort的实现时这样的:

sort( *begin, *end )
{
    __sort( *begin, *end )
    {
        __introsort_loop( *begin, *end, floor )
        {
            if(/*区间长度较大*/)
            {
                if(/*递归层数过大*/)
                {
                    //堆排序
                    __partial_sort( *begin, *end ); 
                } 
                //选择参照值,并将元素分离 
                __cut = __unguarded_partition_pivot( *begin, *end ) 
                //分治 
                __introsort_loop( *begin, *__cut );
                __introsort_loop( *__cut, *end );
            }
        }
        __final_insertion_sort( *begin, *end )
        {
            //插入排序 
        }
    }
}

 

懵逼~~~

看代码看得头晕

 

Update

发现Luogu有个神贴:https://www.luogu.org/discuss/show/112808

可以发现,如果__last在__first前面,那么就永远不会有__i==__last出现,也就是说,在插入排序时会把__first后面所有的数据全部“排序”,emmm

这内存一定是中暑了,要不我们……

STL这鲁棒性太差了

——会某人

posted @ 2019-09-26 20:46  会虎GreyTiger  阅读(820)  评论(2编辑  收藏  举报