前几天阿里电话一面,被问到STLsort函数的实现。以前没有仔细探究过,听人说是快速排序,于是回答说用快速排序实现的,但听电话另一端面试官的声音,感觉不对劲,知道自己回答错了。这几天特意看了一下,在此记录。


函数声明

#include <algorithm>

template< class RandomIt >
void sort( RandomIt first, RandomIt last ); template< class RandomIt, class Compare >
void sort( RandomIt first, RandomIt last, Compare comp );

使用方法非常简单,STL提供了两种调用方式,一种是使用默认的<操作符比较,一种可以自定义比较函数。可是为什么它通常比我们自己写的排序要快那么多呢?


实现原理

原来,STL中的sort并非只是普通的快速排序,除了对普通的快速排序进行优化,它还结合了插入排序堆排序。根据不同的数量级别以及不同情况,能自动选用合适的排序方法。当数据量较大时采用快速排序,分段递归。一旦分段后的数据量小于某个阀值,为避免递归调用带来过大的额外负荷,便会改用插入排序。而如果递归层次过深,有出现最坏情况的倾向,还会改用堆排序。

普通的快速排序

普通快速排序算法可以叙述如下,假设S代表需要被排序的数据序列:

  1. 如果S中的元素只有0个或1个,结束。
  2. S中的任何一个元素作为枢轴pivot
  3. S分割为LR两端,使L内的元素都小于等于pivotR内的元素都大于等于pivot
  4. LR递归执行上述过程。

快速排序最关键的地方在于枢轴的选择,最坏的情况发生在分割时产生了一个空的区间,这样就完全没有达到分割的效果。STL采用的做法称为median-of-three,即取整个序列的首、尾、中央三个地方的元素,以其中值作为枢轴。

分割的方法通常采用两个迭代器headtailhead从头端往尾端移动,tail从尾端往头端移动,当head遇到大于等于pivot的元素就停下来,tail遇到小于等于pivot的元素也停下来,若head迭代器仍然小于tail迭代器,即两者没有交叉,则互换元素,然后继续进行相同的动作,向中间逼近,直到两个迭代器交叉,结束一次分割。

看一张来自维基百科上关于快速排序的动态图片,帮助理解。

内省式排序 Introsort

不当的枢轴选择,导致不当的分割,会使快速排序恶化为 O(n2)。David R.Musser于1996年提出一种混合式排序算法:Introspective Sorting(内省式排序),简称IntroSort,其行为大部分与上面所说的median-of-three Quick Sort完全相同,但是当分割行为有恶化为二次方的倾向时,能够自我侦测,转而改用堆排序,使效率维持在堆排序的 O(nlgn),又比一开始就使用堆排序来得好。


代码分析

下面是完整的SGI STL sort()源码(使用默认<操作符版)

template <class _RandomAccessIter>
inline void sort(_RandomAccessIter __first, _RandomAccessIter __last) {
__STL_REQUIRES(_RandomAccessIter, _Mutable_RandomAccessIterator);
__STL_REQUIRES(typename iterator_traits<_RandomAccessIter>::value_type,
_LessThanComparable);
if (__first != __last) {
__introsort_loop(__first, __last,
__VALUE_TYPE(__first),
__lg(__last - __first) * 2);
__final_insertion_sort(__first, __last);
}
}

其中,__introsort_loop便是上面介绍的内省式排序,其第三个参数中所调用的函数__lg()便是用来控制分割恶化情况,代码如下:

template <class Size>
inline Size __lg(Size n) {
Size k;
for (k = 0; n > 1; n >>= 1) ++k;
return k;
}

即求lg(n)(取下整),意味着快速排序的递归调用最多 2*lg(n) 层。

内省式排序算法如下:

template <class _RandomAccessIter, class _Tp, class _Size>
void __introsort_loop(_RandomAccessIter __first,
_RandomAccessIter __last, _Tp*,
_Size __depth_limit)
{
while (__last - __first > __stl_threshold) {
if (__depth_limit == 0) {
partial_sort(__first, __last, __last);
return;
}
--__depth_limit;
_RandomAccessIter __cut =
__unguarded_partition(__first, __last,
_Tp(__median(*__first,
*(__first + (__last - __first)/2),
*(__last - 1))));
__introsort_loop(__cut, __last, (_Tp*) 0, __depth_limit);
__last = __cut;
}
}
  1. 首先判断元素规模是否大于阀值__stl_threshold__stl_threshold是一个常整形的全局变量,值为16,表示若元素规模小于等于16,则结束内省式排序算法,返回sort函数,改用插入排序。
  2. 若元素规模大于__stl_threshold,则判断递归调用深度是否超过限制。若已经到达最大限制层次的递归调用,则改用堆排序。代码中的partial_sort即用堆排序实现。
  3. 若没有超过递归调用深度,则调用函数__unguarded_partition()对当前元素做一趟快速排序,并返回枢轴位置。__unguarded_partition()函数采用的便是上面所讲的使用两个迭代器的方法,代码如下:

    template <class _RandomAccessIter, class _Tp>
    _RandomAccessIter __unguarded_partition(_RandomAccessIter __first,
    _RandomAccessIter __last,
    _Tp __pivot)
    {
    while (true) {
    while (*__first < __pivot)
    ++__first;
    --__last;
    while (__pivot < *__last)
    --__last;
    if (!(__first < __last))
    return __first;
    iter_swap(__first, __last);
    ++__first;
    }
    }
  4. 经过一趟快速排序后,再递归对右半部分调用内省式排序算法。然后回到while循环,对左半部分进行排序。源码写法和我们一般的写法不同,但原理是一样的,需要注意。

递归上述过程,直到元素规模小于__stl_threshold,然后返回sort函数,对整个元素序列调用一次插入排序,此时序列中的元素已基本有序,所以插入排序也很快。至此,整个sort函数运行结束。


STL sort 函数实现详解 ZZ的更多相关文章

  1. STL sort 函数实现详解

    作者:fengcc 原创作品 转载请注明出处 前几天阿里电话一面,被问到STL中sort函数的实现.以前没有仔细探究过,听人说是快速排序,于是回答说用快速排序实现的,但听电话另一端面试官的声音,感觉不 ...

  2. sort函数用法详解

    用于C++中,对给定区间所有元素进行排序.头文件是#include <algorithm> sort函数进行快速排序,时间复杂度为n*log2n,比冒泡之类的要省时不少 Sort函数使用模 ...

  3. sort()函数使用详解

    使用时需要导入头文件<algorithm> #include<algorithm> 语法描述:sort(begin,end,cmp),cmp参数可以没有,如果没有默认非降序排序 ...

  4. C++中的STL中map用法详解(转)

    原文地址: https://www.cnblogs.com/fnlingnzb-learner/p/5833051.html C++中的STL中map用法详解   Map是STL的一个关联容器,它提供 ...

  5. 自写函数VB6 STUFF函数 和 VB.net 2010 STUFF函数 详解

    '*************************************************************************'**模 块 名:自写函数VB6 STUFF函数 和 ...

  6. SQL Server数据库ROW_NUMBER()函数使用详解

    SQL Server数据库ROW_NUMBER()函数使用详解 摘自:http://database.51cto.com/art/201108/283399.htm SQL Server数据库ROW_ ...

  7. PHP函数篇详解十进制、二进制、八进制和十六进制转换函数说明

    PHP函数篇详解十进制.二进制.八进制和十六进制转换函数说明 作者: 字体:[增加 减小] 类型:转载   中文字符编码研究系列第一期,PHP函数篇详解十进制.二进制.八进制和十六进制互相转换函数说明 ...

  8. PHP date函数参数详解

    PHP date函数参数详解 作者: 字体:[增加 减小] 类型:转载       time()在PHP中是得到一个数字,这个数字表示从1970-01-01到现在共走了多少秒,很奇怪吧 不过这样方便计 ...

  9. SQL中CONVERT()函数用法详解

    SQL中CONVERT函数格式: CONVERT(data_type,expression[,style]) 参数说明: expression 是任何有效的 Microsoft® SQL Server ...

随机推荐

  1. 洛谷——P2083 找人

    P2083 找人 题目背景 无 题目描述 小明要到他的同学家玩,可他只知道他住在某一单元,却不知住在哪个房间.那个单元有N层(1,2……N),每层有M(1,2……M)个房间. 小明会从第一层的某个房间 ...

  2. 美团DB数据同步到数据仓库的架构与实践

    背景 在数据仓库建模中,未经任何加工处理的原始业务层数据,我们称之为ODS(Operational Data Store)数据.在互联网企业中,常见的ODS数据有业务日志数据(Log)和业务DB数据( ...

  3. 网络图片嗅探工具driftnet

    网络图片嗅探工具driftnet   图片是网络数据传输的重要内容.Kali Linux内置了一款专用工具drifnet.该工具可以支持实时嗅探和离线嗅探.它可以从数据流中提取JPEG和GIF这两种网 ...

  4. 1020 Tree Traversals (25)(25 point(s))

    problem Suppose that all the keys in a binary tree are distinct positive integers. Given the postord ...

  5. 机器学习之路: 初识tensorflow 第一个程序

    计算图 tensorflow是一个通过计算图的形式来表示计算的编程系统tensorflow中每一个计算都是计算图上的一个节点节点之间的边描述了计算之间的依赖关系 张量 tensor张量可以简单理解成多 ...

  6. 从Table 表中取出第 m 条到第 n 条的记录

    * FROM Table id FROM Table )) --从TABLE表中取出第m到n条记录 (Exists版本) * FROM TABLE AS a WHERE Not Exists ( * ...

  7. C# 复制、粘贴文本信息到系统剪贴板

    复制: Clipboard.SetDataObject(textBox1.SelectedText); 粘贴: IDataObject iData = Clipboard.GetDataObject( ...

  8. [POI2015]Myjnie

    [POI2015]Myjnie 题目大意: 有\(n(n\le50)\)家洗车店从左往右排成一排,每家店都有一个正整数价格\(d_i\). 有\(m(m\le4000)\)个人要来消费,第\(i\)个 ...

  9. (转)park1.0.0生态圈一览

    转自博客:http://www.tuicool.com/articles/FVBJBjN Spark1.0.0生态圈一览 Spark生态圈,也就是BDAS(伯克利数据分析栈),是伯克利APMLab实验 ...

  10. HDU 5700 区间交 离线线段树

    区间交 题目连接: http://acm.hdu.edu.cn/showproblem.php?pid=5700 Description 小A有一个含有n个非负整数的数列与m个区间.每个区间可以表示为 ...