關於stl_list的sort算法
原文地址:http://www.blog.edu.cn/user1/5010/archives/2006/1166534.shtml
stl中的list被實現爲環狀的雙向鏈表,設置一個“哨”node作爲end( )。list沒有使用標準sort算法,而是實現自身的sort,本質上是mergesort(侯捷解釋的是錯的),但是採用了一個特殊的形式:
普通的mergesort直接將待排序的序列一分爲二,然後各自遞歸調用mergesort,再使用Merge算法用O(n)的時間將已排完序的兩個子序列歸併,從而總時間效率爲n*lg(n)。(mergesort是很好的排序算法,絕對時間很小,n*lg(n)之前的係數也很小,但是在內存中的排序算法中並不常見,我想可能主要還是因爲耗空間太多,也是O(n))
list_sort所使用的mergesort形式上大不一樣:將前兩個元素歸併,再將後兩個元素歸併,歸併這兩個小子序列成爲4個元素的有序子序列;重複這一過程,得到8個元素的有序子序列,16個的,32個的。。。,直到全部處理完。主要調用了swap和merge函數,而這些又依賴於內部實現的transfer函數(其時間代價爲O(1))。該mergesort算法時間代價亦爲n*lg(n),計算起來比較複雜。list_sort中預留了64個temp_list,所以最多可以處理2^64-1個元素的序列,這應該足夠了:)
爲何list不使用普通的mergesort呢?這比較好理解,因爲每次找到中間元素再一分爲二的代價實在太大了,不適合list這種非RandomAccess的容器。
爲何list不使用標準sort算法呢(標準sort要求RandomAccessIterator)?至少普通的quicksort我覺得應該是可以的,具體原因等查查標準算法實現再來說了。
下面把gcc4.02中list_sort的實現貼上:
template<typename _Tp, typename _Alloc>
void
list<_Tp, _Alloc>::
sort()
{
// Do nothing if the list has length 0 or 1.
if (this->_M_impl._M_node._M_next != &this->_M_impl._M_node
&& this->_M_impl._M_node._M_next->_M_next != &this->_M_impl._M_node)
{
list __carry;
list __tmp[64];
list * __fill = &__tmp[0];
list * __counter;
do
{
__carry.splice(__carry.begin(), *this, begin());
for(__counter = &__tmp[0];
__counter != __fill && !__counter->empty();
++__counter)
{
__counter->merge(__carry);
__carry.swap(*__counter);
}
__carry.swap(*__counter);
if (__counter == __fill)
++__fill;
}
while ( !empty() );
for (__counter = &__tmp[1]; __counter != __fill; ++__counter)
__counter->merge(*(__counter - 1));
swap( *(__fill - 1) );
}
}
對它的複雜度分析只能簡單說說了,實際工作在草稿紙上完成:
假設總元素個數N=2^n-1。
首先merge函數的複雜度爲O(m),因此最後一步的for循環複雜度爲 求和i:2~n{2^i-1}=O(N)的時間。
再看do_while循環,tmp[0]有1個元素,tmp[1]有2個元素,。。。,tmp[n-1]有2^(n-1)個元素,他們都是通過merge而後再swap(爲常數時間)到最終層次的,因此總複雜度爲:求和i:1~n{i*2^(i-1)}=O((n-1)*2^n)=O(N*lgN)。
因此總時間複雜度爲N*lgN。
SGI STL list::sort()—快速排序(非遞歸實現方式)
遇下面的源碼,簡單掃了下,暈頭轉向~
// list 不能使用STL 算法 sort(),必須使用自己的 sort() member function,
// 因爲STL算法sort() 只接受RamdonAccessIterator.
// 本函式採用 quick sort.
template <class T, class Alloc>
void list<T, Alloc>::sort() {
// 以下判斷,如果是空白串行,或僅有一個元素,就不做任何動作。
if (node->next == node || link_type(node->next)->next == node) return;
// 一些新的 lists,做爲中介數據存放區
list<T, Alloc> carry;
list<T, Alloc> counter[64];
int fill = 0;
while (!empty()) {
carry.splice(carry.begin(), *this, begin()); //取出list中的一個數據,存入carry
int i = 0;
while(i < fill && !counter[i].empty()) {
counter[i].merge(carry); //將carry中的數據,和 counter[i]鏈中原有數據合併
carry.swap(counter[i++]); //交換carry 和counter[i] 數據
}
carry.swap(counter[i]);
if (i == fill) ++fill;
}
for (int i = 1; i < fill; ++i) // sort之後,善後處理,把數據統一起來
counter[i].merge(counter[i-1]);
swap(counter[fill-1]);
}
你看出來了嘛?
詳細道來:
fill ------> 2^fill 表示現在能處理的數據的最大數目
counter[ fill ]---> 將處理完的2^fill個數據存入 counter[ fill ]中
carry---> 就像一個臨時中轉站, 在處理的數據不足 2 ^ fil l時,在counter[ i ] ( 0=<i<fill)之間移動數據(改變指針)
步驟如下:
1) 讀入一個數據(carry.splice),通過 carry 將數據存入 counter[0] 中;
隨後處理下一個數據, carry 保存數據
a. counter[0].merge(carry) , 此時 counter[0] 容納的數據個數 > 2^0
b. 將counter[0]鏈中的數據,通過carry,轉移到counter[1]鏈,.... 直至處理的數據個數達到 2 ^ fill
2) fill++ , 重複 1) 至處理完所有的數據。
非遞歸的快速排序實現方式,很巧妙!!!
counter 數組爲64 --- 所以此算法,一次最多能處理 2 ^ 64 -2 個數據
這是sgi stl的 list.sort源碼
侯捷的STL源碼剖析 有對很多源碼進行了解釋,但這個函數切只是翻譯了下原註釋幾乎沒有對核心進行解釋,而且貌似他的解釋還是錯誤的。他說此函數採用quick sort。但沒有發現快速排序的特徵,沒有找基準的位置,沒有劃分爲左右,也不是內部排序..等。
第6章的算法的泛化過程find例子 貌似也錯了。find函數返回比較不應該用數組的end()來比較,而應當用find函數的第2個參數做比較!
/* Written By MaiK */
STL中的list被實現爲環狀的雙向鏈表,設置一個“哨兵”node作爲end( )。鑑於list的內存分配模型,list不能使用通用的標準sort算法,而是實現自身的sort,但是list有自己的成員函數sort()可供其自身調用,其實際模型是基於合併排序的。普通的mergesort直接將待排序的序列一分爲二,然後各自遞歸調用mergesort,再使用Merge算法用O(n)的時間將已排完序的兩個子序列歸併,從而總時間效率爲n*lg(n)。(mergesort是很好的排序算法,絕對時間很小,n*lg(n)之前的係數也很小,但是在內存中的排序算法中並不常見,我想可能主要還是因爲耗空間太多,也是O(n)).
不過list_sort所使用的mergesort形式上大不一樣:將前兩個元素歸併,再將後兩個元素歸併,歸併這兩個小子序列成爲4個元素的有序子序列;重複這一過程,得到8個元素的有序子序列,16個的,32個的。。。,直到全部處理完。主要調用了swap和merge函數,而這些又依賴於內部實現的transfer函數(其時間代價爲O(1))。該mergesort算法時間代價亦爲n*lg(n),計算起來比較複雜。list_sort中預留了 64個temp_list,所以最多可以處理2^64-1個元素的序列,這應該足夠了。
/* Written By Lamar */
類似2進制,每一次進位都是相鄰高位數值的一半,所以是類2進制地。例如8,低位4滿之後會進4個到8的。
代碼:
- template <class _Tp, class _Alloc>
- template <class _StrictWeakOrdering>
- void list<_Tp, _Alloc>::sort(_StrictWeakOrdering __comp)
- {
- // Do nothing if the list has length 0 or 1.
- if (_M_node->_M_next != _M_node && _M_node->_M_next->_M_next != _M_node) {
- // 保存下層merge返回的結果
- list<_Tp, _Alloc> __carry;
- // 模擬merge sort使用的堆棧,保存部分有序的list
- // 64應該寫作sizeof(size_type) * 8,即最大的遞歸調用層次。
- list<_Tp, _Alloc> __counter[64];
- // 指示堆棧層次
- int __fill = 0;
- while (!empty()) {
- // 將begin處的元素從list取下,insert到carry中
- __carry.splice(__carry.begin(), *this, begin());
- int __i = 0;
- // 模擬遞歸時對下層的返回結果進行merge,並將結果交給carry
- while(__i < __fill && !__counter[__i].empty()) {
- __counter[__i].merge(__carry, __comp);
- __carry.swap(__counter[__i++]);
- }
- // carry將結果保存到堆棧
- __carry.swap(__counter[__i]);
- // 更新遞歸層次
- if (__i == __fill) ++__fill;
- }
- // 逐級獲取結果
- for (int __i = 1; __i < __fill; ++__i)
- __counter[__i].merge(__counter[__i-1], __comp);
- // 保存最終結果
- swap(__counter[__fill-1]);
- }
- }
- void merge(int *vector, size_t size)
- {
- // return if there's 0 or 1 element.
- if (size <= 1) {
- return ;
- }
- int half = size / 2;
- merge(vector, half);
- merge(vector + half, size - half);
- merge_aux(vector, size);
- return ;
- }
- static void merge_aux(int *vector, size_t size)
- {
- int i, j, k;
- int half = size / 2;
- int clone[half];
- for (i = 0; i < half; ++i) {
- clone[i] = vector[i];
- }
- for (i = 0, j = half, k = 0; (i < half) && (j < size); ++k) {
- if (clone[i] < vector[j]) {
- swap(vector[k], clone[i]);
- ++i;
- }
- else {
- swap(vector[k], vector[j]);
- ++j;
- }
- }
- while (i < half) {
- vector[k++] = clone[i++];
- }
- return ;
- }
1)遞歸形式:(圓圈內爲本層排序好的數據)
因爲遞歸形式的merge sort在每一層都對數據進行分解,所以遞歸樹是一棵完全二叉樹。
2)非遞歸形式:
因爲非遞歸形式的算法是自底向上來merge數據,原始數據都處在同一層上,所以部分數據缺少中間的層次,最後仍然需要對這些結果再做一次合併(圖2虛線引出的那些單元,就是由list::sort中最後那個for循環所處理)。
另外,由list本身來提供sort算法並不符合STL的設計原則,但若要將這樣一個嚴重依賴底層實現的算法抽象出來,又需要做哪些改動呢?歡迎大家討論。