sort
1 函数声明
#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提供了两种调用方式,一种是使用默认的<操作符比较,一种可以自定义比较函数。可是为什么它通常比我们自己写的排序要快那么多呢?
2 实现原理
原来,STL中的sort并非只是普通的快速排序,除了对普通的快速排序进行优化,它还结合了插入排序和堆排序。根据不同的数量级别以及不同情况,能自动选用合适的排序方法。当数据量较大时采用快速排序,分段递归。一旦分段后的数据量小于某个阀值,为避免递归调用带来过大的额外负荷,便会改用插入排序。而如果递归层次过深,有出现最坏情况的倾向,还会改用堆排序。
普通的快速排序
普通快速排序算法可以叙述如下,假设S代表需要被排序的数据序列:
- 如果
S中的元素只有0个或1个,结束。 - 取
S中的任何一个元素作为枢轴pivot。 - 将
S分割为L、R两端,使L内的元素都小于等于pivot,R内的元素都大于等于pivot。 - 对
L、R递归执行上述过程。
快速排序最关键的地方在于枢轴的选择,最坏的情况发生在分割时产生了一个空的区间,这样就完全没有达到分割的效果。STL采用的做法称为median-of-three,即取整个序列的首、尾、中央三个地方的元素,以其中值作为枢轴。
分割的方法通常采用两个迭代器head和tail,head从头端往尾端移动,tail从尾端往头端移动,当head遇到大于等于pivot的元素就停下来,tail遇到小于等于pivot的元素也停下来,若head迭代器仍然小于tail迭代器,即两者没有交叉,则互换元素,然后继续进行相同的动作,向中间逼近,直到两个迭代器交叉,结束一次分割。
看一张来自维基百科上关于快速排序的动态图片,帮助理解。
内省式排序 Introsort
不当的枢轴选择,导致不当的分割,会使快速排序恶化为 O(n2)。David R.Musser于1996年提出一种混合式排序算法:Introspective Sorting(内省式排序),简称IntroSort,其行为大部分与上面所说的median-of-three Quick Sort完全相同,但是当分割行为有恶化为二次方的倾向时,能够自我侦测,转而改用堆排序,使效率维持在堆排序的 O(nlgn),又比一开始就使用堆排序来得好。
3 代码分析
下面是完整的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; } }
- 首先判断元素规模是否大于阀值
__stl_threshold,__stl_threshold是一个常整形的全局变量,值为16,表示若元素规模小于等于16,则结束内省式排序算法,返回sort函数,改用插入排序。 - 若元素规模大于
__stl_threshold,则判断递归调用深度是否超过限制。若已经到达最大限制层次的递归调用,则改用堆排序。代码中的partial_sort即用堆排序实现。 - 若没有超过递归调用深度,则调用函数
__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; } }
- 经过一趟快速排序后,再递归对右半部分调用内省式排序算法。然后回到while循环,对左半部分进行排序。源码写法和我们一般的写法不同,但原理是一样的,需要注意。
递归上述过程,直到元素规模小于__stl_threshold,然后返回sort函数,对整个元素序列调用一次插入排序,此时序列中的元素已基本有序,所以插入排序也很快。至此,整个sort函数运行结束。
4 sort扩展
sort不只是能像上面那样简单的使用,我们可以对sort进行扩展,关键就在于第三个参数<cmp比较函数>,我们想降序排列,或者说我不是一个简简单单的数组,而是结构体、类怎么办,下面给出一些方法和例子。
//情况一:数组排列 int A[100]; bool cmp1(int a,int b)//int为数组数据类型 { return a>b;//降序排列 //return a<b;//默认的升序排列 } sort(A,A+100,cmp1); //情况二:结构体排序 Student Stu[100]; bool cmp2(Student a,Student b) { return a.id>b.id;//按照学号降序排列 //return a.id<b.id;//按照学号升序排列 } sort(Stu,Stu+100,cmp2);
注:比较方法也可以放在结构体中或类中定义。
方法二:使用标准库函数
另外,其实我们还可以再懒一点,在标准库中已经有现成的。它在哪呢?答案是functional,我们include进来试试看。functional提供了一堆基于模板的比较函数对象,它们是:equal_to<Type>、not_equal_to<Type>、greater<Type>、greater_equal<Type>、less<Type>、less_equal<Type>。这些东西的用法看名字就知道了。在这里,我么sort要用到的也只是greater和less就足够了,用法如下:
- 升序:sort(begin,end,less<data-type>())
- 降序:sort(begin,end,greater<data-type>())
缺点:也只是实现简单的排序,结构体不适用。
#include <iostream> #include <cstdio> #include <algorithm> #include <functional> using namespace std; //简单使用方法 sort(A,A+100,greater<int>());//降序排列 sort(A,A+100,less<int>());//升序排列
方法三:重载结构体或类的比较运算符
//情况一:在结构体内部重载 typedef struct Student{ int id; string name; double grade; bool operator<(const Student& s) { return id>s.id;//降序排列 //return id<s.id;//升序排列 } }; vector<Student> V; sort(V.begin(),V.end()); //情况二:在外部重载 vector<Student> V; bool operator<(const Student& s1, const Student& s2) { return s1.id>s2.id;//降序排列 //return s1.id<s2.id;//升序排列 } sort(V.begin(),V.end());
注意:一定要重载<运算符,因为系统默认是降序,用的是<运算符。
方法四:声明比较类(少用)
struct Less { bool operator()(const Student& s1, const Student& s2) { return s1.id<s2.id; //升序排列 } }; sort(sutVector.begin(),stuVector.end(),Less());
作者: AlvinZH
出处: http://www.cnblogs.com/AlvinZH/
本人Github:https://github.com/Pacsiy/JobDu
本文版权归作者AlvinZH和博客园所有,欢迎转载和商用,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
5 应用
(2018czday1)魔法(tsort.*)
【题目描述】
苍崎青子最近学会了改变重力方向的魔法!
众所周知,地球上的重力原本是竖直向下的。而苍崎青子可以用魔法将重力的方向改为水平向右。她决定用N排箱子来测试自己的魔法。
她先将N个箱子排成一列,然后再在每个箱子上放上任意数量的箱子,每个箱子上放的箱子数可能相同,也可能不同。第i个箱子上放的箱子数为ai,ai由手动输入。
然后,她发动改变重力方向的魔法,使得所有箱子向右移动至极限(假设第N列箱子右边有一堵墙,所以箱子不会掉出这N列之外)。
例如发动魔法前:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
发动魔法后:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
【输入】
第一行,一个整数N,含义见题目。
第二行,N个整数,第i个整数为ai,含义见题目。
保证 1<=N<=30000。
0<=ai<=1000000000。
【输出】
一行,N个整数,每两个数之间用一个空格隔开,表示青子发动魔法后第i个箱子上的箱子数。
【样例输入】
4
0 2 0 1
【样例输出】
0 0 1 2
(2018czday1)竞赛排名(paiming.*)
【问题描述】
一年一度的江苏省小学生程序设计比赛开始了,组委会公布了所有学生的成绩,成绩按分数从高到低排名,成绩相同按年级从低到高排。现在主办单位想知道每一个排名的学生前,有几位学生的年级低于他。
【输入格式】
第1行只有一个正整数n(1≤n≤200),表示参赛的学生人数。
第2到n+1行,每行有两个正整数s(0≤s≤400)和g(1≤g≤ 6),之间用一个空格隔开,其中第i+1行的第一个数s表示第i个学生的成绩,第i+1行的第二个数g表示第i个学生的年级。
【输出格式】
输出n行,每行只有一个正整数,其中第i行的数k表示排第i名的学生前面有k个学生排名比他高,且年级比他低。
【输入样例】
5
300 5
200 6
350 4
400 6
250 5
【输出样例】
0
0
1
1
3

浙公网安备 33010602011771号