1. 程式人生 > >STL sort 函式實現詳解

STL sort 函式實現詳解

作者:fengcc 原創作品 轉載請註明出處

前幾天阿里電話一面,被問到STLsort函式的實現。以前沒有仔細探究過,聽人說是快速排序,於是回答說用快速排序實現的,但聽電話另一端面試官的聲音,感覺不對勁,知道自己回答錯了。這幾天特意看了一下,在此記錄。

函式宣告

#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提供了兩種呼叫方式,一種是使用預設的<操作符比較,一種可以自定義比較函式。可是為什麼它通常比我們自己寫的排序要快那麼多呢?

實現原理

原來,STL中的sort並非只是普通的快速排序,除了對普通的快速排序進行優化,它還結合了插入排序堆排序。根據不同的數量級別以及不同情況,能自動選用合適的排序方法。當資料量較大時採用快速排序,分段遞迴。一旦分段後的資料量小於某個閥值,為避免遞迴呼叫帶來過大的額外負荷,便會改用插入排序。而如果遞迴層次過深,有出現最壞情況的傾向,還會改用堆排序。

普通的快速排序

普通快速排序演算法可以敘述如下,假設S代表需要被排序的資料序列:

  1. 如果S
    中的元素只有0個或1個,結束。
  2. S中的任何一個元素作為樞軸pivot
  3. S分割為LR兩端,使L內的元素都小於等於pivotR內的元素都大於等於pivot
  4. LR遞迴執行上述過程。

快速排序最關鍵的地方在於樞軸的選擇,最壞的情況發生在分割時產生了一個空的區間,這樣就完全沒有達到分割的效果。STL採用的做法稱為median-of-three,即取整個序列的首、尾、中央三個地方的元素,以其中值作為樞軸。

分割的方法通常採用兩個迭代器headtailhead從頭端往尾端移動,tail從尾端往頭端移動,當head遇到大於等於pivot的元素就停下來,tail遇到小於等於pivot的元素也停下來,若head

迭代器仍然小於tail迭代器,即兩者沒有交叉,則互換元素,然後繼續進行相同的動作,向中間逼近,直到兩個迭代器交叉,結束一次分割。

看一張來自維基百科上關於快速排序的動態圖片,幫助理解。

內省式排序 Introsort

不當的樞軸選擇,導致不當的分割,會使快速排序惡化為 O(n2)。David R.Musser於1996年提出一種混合式排序演算法:Introspective Sorting(內省式排序),簡稱IntroSort,其行為大部分與上面所說的median-of-three Quick Sort完全相同,但是當分割行為有惡化為二次方的傾向時,能夠自我偵測,轉而改用堆排序,使效率維持在堆排序的 O(nlgn),又比一開始就使用堆排序來得好。

程式碼分析

下面是完整的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;
  }
}
  1. 首先判斷元素規模是否大於閥值__stl_threshold__stl_threshold是一個常整形的全域性變數,值為16,表示若元素規模小於等於16,則結束內省式排序演算法,返回sort函式,改用插入排序。
  2. 若元素規模大於__stl_threshold,則判斷遞迴呼叫深度是否超過限制。若已經到達最大限制層次的遞迴呼叫,則改用堆排序。程式碼中的partial_sort即用堆排序實現。
  3. 若沒有超過遞迴呼叫深度,則呼叫函式__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;
    }
}
  1. 經過一趟快速排序後,再遞迴對右半部分呼叫內省式排序演算法。然後回到while迴圈,對左半部分進行排序。原始碼寫法和我們一般的寫法不同,但原理是一樣的,需要注意。

遞迴上述過程,直到元素規模小於__stl_threshold,然後返回sort函式,對整個元素序列呼叫一次插入排序,此時序列中的元素已基本有序,所以插入排序也很快。至此,整個sort函式執行結束。

結束語

好了,今天就到這裡了,相信大家對STL sort也有了一定的瞭解,如果發現任何錯誤,歡迎大家批評指正,一起交流!

參考

  • 《STL原始碼剖析》 作者:侯捷