C++编程笔记之sort

来源:互联网 发布:苏轼 文言文徙知 编辑:程序博客网 时间:2024/06/07 09:36

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进行分割的。