SGISTL源码探究-stl_algo.h中的排序算法
来源:互联网 发布:梦里花落知多少闻婧被 编辑:程序博客网 时间:2024/05/29 08:34
前言
在本小节中,我们将分析STL
中算法组件的最后一部分:排序算法。排序算法分为内部排序和外部排序。而内部排序中常见的有八种排序算法:直接插入排序、希尔排序、简单选择排序、堆排序、冒泡排序、快速排序、归并排序、桶排序。
在SGISTL
实现的sort
算法中,并不是简单的使用快速排序作为排序算法,而是交叉使用了插入排序、堆排序以及快速排序,并且做了一定的优化,并且只接受迭代器类型为RandomAccessIterator
。这里不会详细去讲解这几种排序算法的原理,只简单介绍下排序的思想及流程并根据SGISTL
实现的源码进行分析。
堆排序
基本思路
关于堆排序算法的实现思路就是:初始时把要排序的n个数的序列调整成一个堆,将堆顶元素取出,得到n个元素中最小(或最大)的元素,然后对前面(n-1)个元素重新调整使之成为堆,再取出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序。
关于堆排序,最重要的就两点:
- 如果将序列调整成堆;
- 取出堆顶元素之后,如果再次调整,使得剩下的元素依然为堆。关于这两点,在我们分析优先级队列前就已经对大根堆进行分析,如果你有所遗忘,建议回看(http://blog.csdn.net/move_now/article/details/77994869)。
接下来我们介绍partial_sort
算法(排序部分采用堆排序),它会在sort
算法的实现中被调用。
partial_sort
该算法的作用是接收一个middle迭代器,将[first, last)
序列中的middle - first
个最小元素以递增序列排在[first, middle)
内,而剩下的[middle, last)
个元素随意放置。partial_sort
有两个版本,另外一个版本提供自己的比较仿函数comp
,这里不做分析。
//函数名前加了两个下划线,这种命名方式的意义我们早就见怪不怪了,代表partial_sort的内部实现中会调用它//通过参数我们可以知道partial_sort只接受迭代器类型为RandomAccessIterator/* 实现思路: * 1. 将[first, middle)序列中的元素形成大根堆 * 2. 遍历[middler, last)中的元素,如果比大根堆堆顶的元素小,则与之对调,然后重新调整堆 * 3. 遍历完成之后,[first, middle)中的元素必然是前middle-first个小的元素 * 4. 最后我们只需要对该堆进行排序即可(内部实现就是不断的调用pop_heap) */template <class RandomAccessIterator, class T>void __partial_sort(RandomAccessIterator first, RandomAccessIterator middle, RandomAccessIterator last, T*) { //构建堆 make_heap(first, middle); //遍历[middle, last),找出比[first, middle)中还小的元素,进行调整 for (RandomAccessIterator i = middle; i < last; ++i) if (*i < *first) __pop_heap(first, middle, i, T(*i), distance_type(first)); //最后进行排序 sort_heap(first, middle);}template <class RandomAccessIterator>inline void partial_sort(RandomAccessIterator first, RandomAccessIterator middle, RandomAccessIterator last) { __partial_sort(first, middle, last, value_type(first));}
直接插入排序
基本思路
直接插入排序的思想就是维护一个有序的左区间,将未排序部分的第一个元素插入到已排序左区间中合适的位置,中间可能涉及到寻找合适的位置而不断的比较。
该算法的时间复杂度为O(n^2),虽然看起来很慢,但是其实它特别适用于部分有序的序列,这样做的调整很少,并且对于少量数据来说也比较适合,因为快速排序、归并排序等效率较高的算法涉及到递归的函数调用,对于少量数据来说,这样反而比较慢,并且快排有个硬伤,那就是需要排序的序列已经部分有序甚至有序时,时间复杂度会从O(nlog2n)退化成O(n^2),由于还涉及到了大量的递归函数调用,效率还不如直接插入排序。
insertion_sort
在SGISTL
实现直接插入排序中,提供了两个版本,版本二使用传入的比较仿函数comp
进行比较,这里不做分析。并且实现过程中做了一定程度的优化,这点我们在它实现中可以看到。
源码如下:
/* 实现思路: * __insertion_sort中提供外循环,即左区间依次增大,直到覆盖整个序列 * __linear_insert处理特殊情况及调用__unguarded_linear_insert处理普遍情况 * __unguarded_linear_insert提供内循环,使得左区间有序 */template <class RandomAccessIterator>void __insertion_sort(RandomAccessIterator first, RandomAccessIterator last) { //序列中元素个数为0,直接返回 if (first == last) return; //外层循环,扩展有序区间([first, i)) for (RandomAccessIterator i = first + 1; i != last; ++i) __linear_insert(first, i, value_type(first));}template <class RandomAccessIterator, class T>inline void __linear_insert(RandomAccessIterator first, RandomAccessIterator last, T*) { //value作为要插入的元素(i指向的元素) T value = *last; //特殊情况,若value比*first还小,那就不用一个一个比较找合适的位置了 //直接把[first, last)后移一个位置 //然后将头元素置为value if (value < *first) { copy_backward(first, last, last + 1); *first = value; } //处理普遍情况,内循环(寻找value可以插入的合适的位置) else __unguarded_linear_insert(last, value);}//内循环,为value寻找合适的插入位置并插入//可以看到该函数命名的前缀是__unguarded,这个我们后面还会看到//这是因为它并没有处理边界情况,即next是否到达first//因为next并不会到达first,这种情况在__linear_insert已经处理了,所以在到达边界之前,一定会找到合适的位置插入//这样就省下了一个判断,当数据量比较大的时候,效果还是比较显著template <class RandomAccessIterator, class T>void __unguarded_linear_insert(RandomAccessIterator last, T value) { RandomAccessIterator next = last; --next; /* 从(first, last)序列中从后往前依次寻找合适的插入位置 * 若value比当前比较的元素小,证明还有可能可以前移也有可能当前位置就是插入点 * 所以将*next的值后移,为前一个元素的后移或这value腾出位置 * 将next指向前一个元素继续进行比较 */ while (value < *next) { *last = *next; last = next; --next; } //出循环代表找到了合适的插入点,进行赋值 *last = value;}
快速排序
快排的实现思路是选取序列中的任意一个元素当作枢轴(pivot),然后将小于枢轴的元素放在枢轴的左边,大于枢轴的元素放在枢轴的右边,再以该枢轴为中心,将左区间和右区间分别递归执行上面的操作。
实现的三个步骤:
1. 选择枢纽:在待排的序列中,以某种方式选择一个元素作为枢纽(pivot)
2. 分割操作:以该枢纽在序列中的位置,将该序列分成两个子序列。并且保证在枢纽左边的元素都比该枢纽小,在枢纽右边的元素都比枢纽大。
3. 分出来的两个子序列相当于是两个子问题,按照上面的步骤分别进行递归,直到序列为空或者只有一个元素。
比较经典的实现如下:
template <class RandomAccessIterator, class T>RandomAccessIterator Partition(RandomAccessIterator first, RandomAccessIterator last, T pivot){ while(true) { while(*first < pivot) ++first; --last; while(*last > pivot) --last; if(!(first < last)) return first; iter_swap(first, last); ++first; }}template <class RandomAccessIterator, class T>void qsort(RandomAccessIterator first, RandomAccessIterator last){ if(first != last) { RandomAccessIterator cut = Partition(first, last, *first); qsort(first, cut); qsort(cut, last); }}
选取枢纽
做法一般是取首部/尾部元素作为枢纽,但是当序列是有序时,会导致效率急剧下降。
我们可以通过随机选择元素作为枢纽,这样有可能可以避免序列有序时效率的下降问题,但是最坏情况还是O(n^2),而且万一运气不好…..
比较好的做法是三数取中(median-of-three),即使用首部位置、中间位置、尾部位置这三个位置上的元素的中间值作为枢纽。比如首部为4,中间位置的元素为6,尾部位置上的元素为10,则选取中间位置的元素作为枢纽。事实上,SGISTL
实现sort
算法时就是采用的这种做法取枢纽。
三种优化
当分解的序列元素个数足够少时,可以采用直接插入排序。
因为直接插入排序比较适合数据量少或者序列已经一定程序有序的情况,如果还是调用分解操作,又涉及到递归函数调用,效率反而不高。
减少递归的次数。
众所周知,递归次数多了会爆栈。有n个元素就会调用n次递归函数,但是因为在
qsort
函数中,执行了一次递归之后,last
或者first
就没用了,我们可以选择其中一个递归以迭代的形式进行,使其只调用logn次递归。做法如下:
template <class RandomAccessIterator, class T>void qsort(RandomAccessIterator first, RandomAccessIterator last){ while(first != last) { RandomAccessIterator cut = Partition(first, last, *first); qsort(cut, last); last = cut; }}
- 当序列有很多重复元素时,我们可以在执行分割时,把与枢纽相等的元素放入序列的两端,然后在分割之后,把与枢纽相等的元素移动到枢纽周围。这样下次分割时,就可以不对重复的元素进行操作了。具体的实现这里不做讨论。(因为
sort
实现中,只要使用的前面的优化)
- 当序列有很多重复元素时,我们可以在执行分割时,把与枢纽相等的元素放入序列的两端,然后在分割之后,把与枢纽相等的元素移动到枢纽周围。这样下次分割时,就可以不对重复的元素进行操作了。具体的实现这里不做讨论。(因为
内省排序
这种排序可能你没有听说过,其实我也没有。这个排序算法首先从快速排序开始,当递归深度超过一定深度(深度为排序元素数量的对数值)后转为堆排序。采用这个方法,内省排序既能在常规数据集上实现快速排序的高性能,又能在最坏情况下仍保持 O(nlogn)的时间复杂度。
SGISTL
实现sort
算法时就是采用的这种算法。所以总的来说,它即用到了快速排序,也用到了堆排序以及直接插入排序。
有了以上的铺垫,下面我们就进入到sort
算法的真正实现中应该就比较容易了。
sort
的实现
它的实现也提供了两个版本,另一个是传入comp
仿函数进行比较,这里不做讨论。还有sort
算法仅支持迭代器类型为RandomAccessIterator
,这一点不要忘了。(所以list
容器实现了自己的排序算法)
//计算递归深度(深度为排序元素数量的对数值)//内省排序会通过递归的深度决定是否采用堆排序template <class Size>inline Size __lg(Size n) { Size k; for (k = 0; n > 1; n >>= 1) ++k; return k;}//三点定位,返回中间值template <class T>inline const T& __median(const T& a, const T& b, const T& c) { if (a < b) if (b < c) return b; else if (a < c) return c; else return a; else if (a < c) return a; else if (b < c) return c; else return b;}template <class RandomAccessIterator>inline void sort(RandomAccessIterator first, RandomAccessIterator last) { if (first != last) { //内省排序,最后一个参数即递归深度 __introsort_loop(first, last, value_type(first), __lg(last - first) * 2); //该函数用于收尾工作,因为当元素数量低于一定程度时就会中止排序(以__stl_threshold表示数量个数多少的界定,值为16) //数量少了,就可以让直接插入排序开始工作了 __final_insertion_sort(first, last); }}//内省排序template <class RandomAccessIterator, class T, class Size>void __introsort_loop(RandomAccessIterator first, RandomAccessIterator last, T*, Size depth_limit) { //优化,当元素个数大于16时,才进行排序 while (last - first > __stl_threshold) { //depth_limit表示递归深度,由__lg函数计算得出 //为0时,代表此时该启用堆排序了 if (depth_limit == 0) { //堆排 partial_sort(first, last, last); return; } //不为0,则减1,因为等下要调用递归了 --depth_limit; //分割操作,__median选取三点中值 RandomAccessIterator cut = __unguarded_partition (first, last, T(__median(*first, *(first + (last - first)/2), *(last - 1)))); //对右半递归进行sort __introsort_loop(cut, last, value_type(first), depth_limit); //优化,更新last,消除了一半的递归 last = cut; }}//当__introsort_loop执行完之后,会有多个元素个数少于16的子序列并不是完全有序的,因为元素个数少于16个时会停止排序//这个时候就该使用插入排序进行收尾了template <class RandomAccessIterator>void __final_insertion_sort(RandomAccessIterator first, RandomAccessIterator last) { //若当前序列的个数大于16个,则将该序列分割成两个序列,一个长度为16,另一个是剩余的序列,分别排序 if (last - first > __stl_threshold) { //对前16个元素调用__insertion_sort排序 __insertion_sort(first, first + __stl_threshold); //对剩余的序列进行排序 __unguarded_insertion_sort(first + __stl_threshold, last); } //若当前序列的个数小于等于16个,调用__insertion_sort进行排序 else __insertion_sort(first, last);}//调用__unguarded_insertion_sort_auxtemplate <class RandomAccessIterator>inline void __unguarded_insertion_sort(RandomAccessIterator first, RandomAccessIterator last) { __unguarded_insertion_sort_aux(first, last, value_type(first));}//直接插入排序,__unguarded_linear_insert函数前面分析过template <class RandomAccessIterator, class T>void __unguarded_insertion_sort_aux(RandomAccessIterator first, RandomAccessIterator last, T*) { for (RandomAccessIterator i = first; i != last; ++i) __unguarded_linear_insert(i, T(*i));}
小结
由此可见sort
算法并不是我们想象的那样简单的使用快排算法。我们再回顾一下它的优化点:
- 关于轴枢的选择,采用的是三点中值法确定轴枢,这是为了防止序列中的元素不够随机导致效率的退化
- 为了防止递归次数太多导致栈溢出,消除了部分递归
- 当元素个数比较少时,停止使用分割操作进行排序,而是留到最后使用直接插入排序进行处理
- 当递归的深度太深时,直接使用堆排序,这样可以将最坏情况推进到O(nlogn)
关于sort
算法的讨论到此为止,迄今为止,我们已经分析了stl_algo.h
中的大部分算法,相信你已经对该部分有一定的了解了。
在分析这些算法的时候,基本上每个算法都提供了具有仿函数的版本,在下一小节中,我们就将进入到STL六大组件之一的仿函数的分析中。
- SGISTL源码探究-stl_algo.h中的排序算法
- SGISTL源码探究-stl_algo.h中的基础算法
- SGISTL源码探究-stl_algo.h中的排列算法
- SGISTL源码探究-stl_algobase.h中的算法
- SGISTL源码探究-stl_numeric.h中的数值算法
- SGISTL源码探究-stl_alog.h中的二分查找算法
- SGISTL源码探究-STL中的算法(前言)
- SGISTL源码探究-STL中的红黑树(上)
- SGISTL源码探究-STL中的红黑树(下)
- SGISTL源码探究-STL中的hashtable(上)
- SGISTL源码探究-STL中的hashtable(下)
- STL 源码剖析 算法 stl_algo.h -- merge
- STL 源码剖析 算法 stl_algo.h -- partition
- STL 源码剖析 算法 stl_algo.h -- includes
- STL 源码剖析 算法 stl_algo.h -- rotate
- STL 源码剖析 算法 stl_algo.h -- search
- STL 源码剖析 算法 stl_algo.h -- search_n
- STL 源码剖析 算法 stl_algo.h -- lower_bound
- [LeetCode]习题5
- 公众号开发——点击菜单拉消息
- sql缓存
- SOA、SOAP、RPC、REST、DUBBO的区别与联系
- ubuntu 系统性能提升
- SGISTL源码探究-stl_algo.h中的排序算法
- Junit单元测试多线程的问题
- 渗透测试 | 无线渗透 | 1-802.11 标准
- iOS 实现颜色渐变
- Math、Date对象
- 【Python笔记】编码一个generator实现杨辉三角
- Variational Autoencoder: Basic Concept
- 滤波器的延时计算问题
- WITH table AS及其他