STL sort函數

sort有兩種重載形式,一種是依靠自定義的比較函數comp的排序函數,一種是依靠<運算符的排序函數。下面的源碼解讀選擇<運算符的版本。
sort函數的流程圖如下
在這裏插入圖片描述

1 提供給外界的接口sort源碼

template <class _RandomAccessIter>
inline void sort(_RandomAccessIter __first, _RandomAccessIter __last) {
  if (__first != __last) {//元素個數>1纔會排序
    __introsort_loop(__first, __last,
                     __VALUE_TYPE(__first),
                     __lg(__last - __first) * 2);//先進行快速排序分割序列塊
    __final_insertion_sort(__first, __last);//到這一步,__first~__last範圍中已經有多個子序列,並且每一個子序列中的值都比前一個子序列大
  }
}

1.1快排分割序列塊函數__introsort_loop,以及用來限定分割次數的函數__lg

template <class _RandomAccessIter, class _Tp, class _Size>
void __introsort_loop(_RandomAccessIter __first,
                      _RandomAccessIter __last, 
                      _Tp*,//traits萃取作用
                      _Size __depth_limit)//快排遞歸深度限制,每用快排分割一次,__depth_limit就減一,如果__depth_limit爲零,就不會再分割了。
{
  while (__last - __first > __stl_threshold) {//當每個分塊的元素個數大於閾值(__stl_threshold)時,
  //就會繼續用快排分塊,而如果≤閾值時,就直接跳過這個函數,用插入排序來排列小型序列,問題一
    if (__depth_limit == 0) {//當遞歸深度限制爲0時,就使用堆排序,問題二
      partial_sort(__first, __last, __last);//將__first~__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;//分割點左邊的部分進入下一個循環,同樣是繼續快排分割操作,只是沒有遞歸,這樣少一輪遞歸,速度會加快很多
  }
}

template <class _Size>
inline _Size __lg(_Size __n) {//計算快排遞歸深度限制的函數,n是整個排序序列的尺寸大小
  _Size __k;
  for (__k = 0; __n != 1; __n >>= 1) ++__k;
  return __k;//最後k值是滿足不等式:2^k≤n的最大整數
}

問題1:爲什麼當被分割的子序列尺寸在閾值之下,就會停止快排,而採用插入排序

  • 因爲當一個序列只有十來個元素時,未必是快排最快,因爲快排是遞歸實現的,對於小規模排序遞歸的代價顯得有點大,反而會拖慢程序。由於是被快排分割成小序列時,所以每個小序列中的元素全部或大於或小於一個值,也就是幾近排好序的序列,這時候使用插入排序會有很好表現。
    問題2:爲什麼需要有遞歸深度限制?
  • 因爲在快排中,不當的樞軸選擇會導致快排從O(nlogn)惡化成O(n^2)。所以這裏使用的是introsort方法,也就是當快排有惡化成二次複雜度傾向時,能夠停止繼續分割,而使用堆排序。
1.1.1 部分堆排序函數partial_sort

找到序列中最小的(__middle-__first)個元素,由小到大排好序,放在序列的__first~__middle

template <class _RandomAccessIter>
inline void partial_sort(_RandomAccessIter __first,
                         _RandomAccessIter __middle,
                         _RandomAccessIter __last) {
  __partial_sort(__first, __middle, __last, __VALUE_TYPE(__first));
}

template <class _RandomAccessIter, class _Tp>
void __partial_sort(_RandomAccessIter __first, _RandomAccessIter __middle,
                    _RandomAccessIter __last, _Tp*) {
  make_heap(__first, __middle);
  for (_RandomAccessIter __i = __middle; __i < __last; ++__i)
    if (*__i < *__first) 
      __pop_heap(__first, __middle, __i, _Tp(*__i),
                 __DISTANCE_TYPE(__first));//把__first位置的元素移到i位置,然後把i位置原來的元素移到最大堆中並排好序
  sort_heap(__first, __middle); //將最大堆轉換成有序數組
}
1.1.2 快排分割函數__unguarded_partition以及找樞軸函數__median
  • __unguarded_partition函數就是根據第三個輸入參數樞軸值,將__first~__last分成個子序列,一個子序列中的元素都大於樞軸,一個子序列中的元素都小於樞軸。(三點中值法)
  • __median就是如何在一個序列中確定樞軸值,就是將一個序列中的頭、尾和中央三個位置的元素數值作比較,找到這三個數值中中間那個值,作爲樞值。
template <class _RandomAccessIter, class _Tp>
_RandomAccessIter __unguarded_partition(_RandomAccessIter __first, 
                                        _RandomAccessIter __last, 
                                        _Tp __pivot) //__pivot就是樞值
{
  while (true) {
    while (*__first < __pivot)//first找到≥__pivot就結束循環
      ++__first;
    --__last;
    while (__pivot < *__last)//last找到≤__pivot 就結束循環
      --__last;
    if (!(__first < __last))//如果first位置≥last位置就結束
      return __first;//最後以first停留的位置作爲分界
    iter_swap(__first, __last);//否則就交換first位置和last位置的元素,也就是把≥__pivot的元素放到右邊,把≤__pivot的元素放到左邊
    ++__first;
  }
}  
//這個函數就是找a,b,c中值
template <class _Tp>
inline const _Tp& __median(const _Tp& __a, const _Tp& __b, const _Tp& __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;
}

__unguarded_partition函數的示例圖:
在這裏插入圖片描述

1.2 插入排序函數__final_insertion_sort

template <class _RandomAccessIter>
void __final_insertion_sort(_RandomAccessIter __first, 
                            _RandomAccessIter __last) {
  if (__last - __first > __stl_threshold) {//如果超過閾值,就分兩段排序
    __insertion_sort(__first, __first + __stl_threshold);//第一段需要加上邊界判斷,是因爲前16個值可能比__first要小,會移到__first前面,因爲可能都屬於第一個子序列
    __unguarded_insertion_sort(__first + __stl_threshold, __last);//但是後面的值一定不可能比__first值要小,因爲後面的元素肯定不屬於第一個子序列,根據快排的性質,不可能比前面序列要小,所以不需要加邊界判斷。
  }
  else
    __insertion_sort(__first, __last);//否則就分一段,但是不確定裏面有幾個子序列,所以都加了邊界判斷。
}

問題:爲什麼超過16的位置處可以不用加邊界判斷?
因爲經過快排分割以後,從__first~__last中就是由若干個子序列組成的,比如上面那張圖,可能完全排列以後是這樣的:所以其實只有第一個子序列需要進行邊界判斷,因爲後面的子序列中的值一定小於第一個子序列中的值。而第一個子序列一定小於閾值,如果比閾值大,只有可能是已經堆排序的了。所以小於閾值的進性邊界檢測,只是因爲不確定第一子序列的元素個數,所以設定的安全值。
在這裏插入圖片描述

1.2.1 快排的封裝函數__insertion_sort和__linear_insert
  • __insertion_sort函數就是主要是把所有元素都按照同一種插入排序的方法進行排序
  • __linear_insert函數就是把__last插入到__first~__last-1的序列中。首先把__last< *__first的情況單獨拿出來,如果是這種情況,那麼__last元素一定添加在__first前面。如果不是這種情況,就再執行__unguarded_linear_insert函數。
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));
}//插入排序,迭代器範圍從左往右逐漸擴大,每次都把迭代器範圍的最後一個元素往前移動,移到比它小的第一個數後面,如下圖所示

template <class _RandomAccessIter, class _Tp>
inline void __linear_insert(_RandomAccessIter __first, 
                            _RandomAccessIter __last, _Tp*) {//last就是要插入的值
  _Tp __val = *__last;
  /*先把最後一個數和第一個數比較,避免出現最壞情況*/
  if (__val < *__first) {//如果最後一個數小於第一個數
    copy_backward(__first, __last, __last + 1);
    *__first = __val;//直接把最後一個數放到前面,整體往後移
  }/*把最後一個元素從後往前依次比較,找到最後一個元素應該放置的位置*/
  //由於有了第一個判斷,所以下面的情況中最後一個元素一定大於等於第一個元素,所以__unguarded_linear_insert就不許喲進行邊界判斷了。節省了一些時間
  else//如果最後一個數不小於第一個數,就從後面往前找到小於__val的第一個數,然後將__val插在它的後面
  //相當於把比__val大的數都往後面移動一格
    __unguarded_linear_insert(__last, __val);//__val就是__last迭代器指向的值
}

相當於從頭開始,每一次都在已經排好序的隊列中找到新加入元素的位置,所以叫插入排序。
在這裏插入圖片描述

1.2.1.1 不檢測邊界的插值排序__unguarded_linear_insert函數

這個函數沒有邊界判斷的,就是不需要判斷–__next會不會超過first,因爲如果會超過first,就不會進入這個函數了

//從_last往前尋找,把比__val大的數整體往後移一位,直到找到一個比__val小的值
template <class _RandomAccessIter, class _Tp>
void __unguarded_linear_insert(_RandomAccessIter __last, _Tp __val) {
  _RandomAccessIter __next = __last;
  --__next;//在這個函數中,__next和__last之間的距離一直是1
  while (__val < *__next) {//這個循環一定會在first之前結束的,因爲在執行這個函數之前已經判斷了,first一定比__val小
    *__last = *__next;//迭代器所指向的值交換
    __last = __next;//迭代器所指向元素交換,其實就是將__next和__last元素位置調換
    --__next;
  }
  *__last = __val;
}
1.2.2 __unguarded_insertion_sort函數

執行不帶邊界判斷的插入排序

template <class _RandomAccessIter>
inline void __unguarded_insertion_sort(_RandomAccessIter __first, 
                                _RandomAccessIter __last) {
  __unguarded_insertion_sort_aux(__first, __last, __VALUE_TYPE(__first));
}

template <class _RandomAccessIter, class _Tp>
void __unguarded_insertion_sort_aux(_RandomAccessIter __first, 
                                    _RandomAccessIter __last, _Tp*) {
  for (_RandomAccessIter __i = __first; __i != __last; ++__i)
    __unguarded_linear_insert(__i, _Tp(*__i));
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章