1、sort实现
stl中的sort调用了两个函数,
__introsort_loop:可以避免最坏情况的快速排序;
__final_insertion_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);
}
}
为什么有两个排序搭配:
这是因为stl实现的策略是当需要排序的元素个数少于某个阈值的时候,就从快速排序转换成插入排序。
因为数据量很少的时候,插入排序效果并不差,而且关键的是不需要递归。
现在先看看快速排序的实现:
//lg是2^k <= n中的最大k值
template <class _Size>
inline _Size __lg(_Size __n) {
_Size __k;
for (__k = 0; __n != 1; __n >>= 1) ++__k;
return __k;
}
template <class _RandomAccessIter, class _Tp, class _Size>
void __introsort_loop(_RandomAccessIter __first,
_RandomAccessIter __last, _Tp*,
_Size __depth_limit)
{
while (__last - __first > __stl_threshold) {//判断只有数组个数超过__stl_threshold的时候,才会继续使用快排。
if (__depth_limit == 0) {//前面看出depth_limit是__lg(__last - __first) * 2,为的是防止递归深度过深
partial_sort(__first, __last, __last);//如果递归过深,采用partial_sort,也就是堆排序。
return;
}
--__depth_limit;
_RandomAccessIter __cut =
__unguarded_partition(__first, __last,_Tp(__median(*__first,*(__first + (__last - __first)/2),*(__last - 1))));//对于数组左边使用while进行递推
__introsort_loop(__cut, __last, (_Tp*) 0, __depth_limit);//对右边进行递归?????????
__last = __cut;
}
}
__unguarded_partition:根据pivot将区间分割为两个子序列,注意该函数是没有边界检查的
template <class _RandomAccessIter, class _Tp>
_RandomAccessIter __unguarded_partition(_RandomAccessIter __first, _RandomAccessIter __last, _Tp __pivot)
{
while (true) {
while (*__first < __pivot)//当左值小于pivot,f向后移动,当左值大于等于pivot,停止
++__first;
--__last;
while (__pivot < *__last)当右值大于pivot,l向前移动,当右值小于等于pivot,停止
--__last;
if (!(__first < __last))//如果两个指针接错,循环停止
return __first;
iter_swap(__first, __last);//否则交换
++__first;//指针向后移动一格
}
}
其中的pivot用的是三中值法,这可以避开最坏情况:
就是在头尾和中点的值进行比较,取中间的值作为pivot。
__median(*__first,*(__first + (__last - __first)/2),*(__last - 1)))
插入排序:
为什么要接近排完序的数组,还需要分大小进行分段插入排序呢?
template <class _RandomAccessIter>
void __final_insertion_sort(_RandomAccessIter __first,
_RandomAccessIter __last) {
if (__last - __first > __stl_threshold) {
__insertion_sort(__first, __first + __stl_threshold);
__unguarded_insertion_sort(__first + __stl_threshold, __last);
}
else
__insertion_sort(__first, __last);
}
template <class _RandomAccessIter>
void __insertion_sort(_RandomAccessIter __first, _RandomAccessIter __last) {
if (__first == __last) return;
for (_RandomAccessIter __i = __first + 1; __i != __last; ++__i)
__linear_insert(__first, __i,__VALUE_TYPE(__first));
}
__insertion_sort最终又会调用__linear_insert
该函数会首先判断,最小值是否已经在最左边了,如果不是直接将元素整体后移一位,将新值放在最左边。
template <class _RandomAccessIter, class _Tp>
inline void __linear_insert(_RandomAccessIter __first, _RandomAccessIter __last, _Tp*) {
_Tp __val = *__last;
if (__val < *__first) {
copy_backward(__first, __last, __last + 1);
*__first = __val;//后一个值小于第一个值,直接将该值前面的整体移后一位,然后将第一个位置为该值
}
else//否则执行插入排序
__unguarded_linear_insert(__last, __val);
}
unguarded_linear_insert
这个函数输入一个迭代器,只要判断val的值比指针所指的值小,指针就往前移动一格,直至val不小于指针所指值。
插入排序在判断逆序对同时还要判断*next是否大于0,也就是边界的判断,但是__linear_insert中的第一个if判断,已经保证最小值将必然在数组的最左边了,所以就不用再判断是否越界了。
这样又可以省下比较操作。
template <class _RandomAccessIter, class _Tp>
void __unguarded_linear_insert(_RandomAccessIter __last, _Tp __val) {
_RandomAccessIter __next = __last;
--__next;
while (__val < *__next) {
*__last = *__next;
__last = __next;
--__next;
}
*__last = __val;
}
所以为什么要接近排完序的数组,还需要分大小进行分段插入排序呢?
大体上还是因为__unguarded_insertion_sort的效率比较高,但是__unguarded_insertion_sort又没有边界检查,所以先用 __insertion_sort来进行确保边界问题,整个序列的最小值就在最左边。
2、根据快排思路,自己实现
首先是medianOfThree:
选取的三个值将会按照升序排列,递增地顺序:a[left]—a[middle] ——a[right],显然,此时a[right]比a[middle]大,所以可以将pivot放置在right-1的位置,这样就可以使得a[right]此时已经在pivot的右边了。
const int medianOfThree(vector<int> &nums,int left,int right){
int middle = (left + right)/2;
if(a[middle] < a[left])
swap(a[middle],a[left]);
if(a[right] < a[left])
swap(a[right],a[middle]);
if(a[right] < a[middle])
swap(a[right],a[middle]);
swap(a[middle],a[right-1]);
return a[right-1];
}
void qiucksort(vector<int> &nums,int left,int right){
int i = left,j = right - 1;
int pivot = medianOfThree(nums,left,right);
while(true){
while(a[++i] < pivot){}
while(a[--j] > pivot){}
if(i < j)
swap(a[i],a[j]);
else
break;
}
swap(a[i],a[right-1]);//此时i,j交替,i就是右边区间第一个值,显然右边区间所有值都大于等于pivot,所以更换有利。
quicksort(nums,left,i-1);
quicksort(nums,i+1,right);
}
3、stl的递归与递推
(1)中源码中我曾经有些疑问,就是为什么左边是递推右边是递归呢?
其实这跟数组个数小于阈值后使用插入是一个道理。为了减少递归调用的开销,一半是递归一半是递推,__last = __cu之后,再进行一次while循环,最终会调用partition进行分割的。