C++ STL LIST SORT 排序算法圖解

最近看 <<C++性能優化指南>> 留意到上面說 std::list::sort 算法能做到 O(nlog2(n)) 複雜度,而直接對 std::list 套用 std::sort 只能做到 O(n²)

思考後發現如果把 std::sort 套到 std::list 上由於是 Bidirectional Iterator 的原因,計算距離的時候需要一步一步的移動,經典的 intro sort/quick sort 無法在這個雙端鏈表的結構上面施展,而 heap sort 需要先 make_heap,雙端鏈表由於無法快速定位到中間的元素,第一步的 make_heap 都很難操作。

參考了網上資料以及<<STL源碼剖析>> 發現,該排序算法使用的是類似 merge sort 的思想,代碼比較簡短,第一次看的時候不是很好理解,在此做個記錄

一開始也排除了 merge sort, 因爲傳統的 merge sort 需要不停地對半,歸併,對半,歸併,在一個雙端鏈表要怎麼實現這個對半的過程呢?

這是 <<STL源碼剖析>> 上面的 list::sort 編輯過後的代碼

#include <iostream>
#include <sstream>
#include <list>
template <class T, class Alloc>
void pr_arr(const std::list<T, Alloc> &lst, std::string hint = "")
{
		std::cout << hint << ": ";
		for (const auto &i : lst)
		{
				std::cout << i << " ";
		}
		if (!lst.size())
				std::cout << "empty list";
		std::cout << std::endl;
}

template <class T, class Alloc = std::allocator<T>>
struct my_list: public std::list<T, Alloc>
{
		void my_sort()
		{
				std::ostringstream str_os;
				std::list<T, Alloc> carry;
				std::list<T, Alloc> counter[64];
				int fill = 0;
				std::list<T, Alloc> &base_lst = *this;
				while (! this->empty())
				{
						pr_arr(base_lst, "before splice, base");
						pr_arr(carry, "carry");
						carry.splice(carry.begin(), base_lst, this->begin());
						pr_arr(base_lst, "after splice, base");
						pr_arr(carry, "carry");
						int i = 0;
						while (i < fill && !counter[i].empty())
						{
								std::cout << "i: " << i << " fill: " << fill << "\n";
								counter[i].merge(carry);
								std::cout << "after counter[i].merge(carry)\n";
								pr_arr(counter[i], "counter[i]");
								pr_arr(counter[i+1], "counter[i+1]");
								pr_arr(carry, "carry");
								carry.swap(counter[i++]);
						}
						std::cout << "\ni: " << i << " before carry.swap(counter[i]) " << std::endl;
						pr_arr(counter[i], "counter[i]");
						pr_arr(carry, "carry");
						carry.swap(counter[i]);
						std::cout << "i: " << i << " after carry.swap(counter[i]) " << std::endl;
						pr_arr(counter[i], "counter[i]");
						pr_arr(carry, "carry");
						if (i == fill) ++fill;
				}
				for (int i = 1; i < fill; ++i)
				{
						std::cout << "i: " << i << "fill: " << fill << "\n";
						pr_arr(counter[i], "counter[i] before merge i - 1");
						counter[i].merge(counter[i-1]);
						pr_arr(counter[i], "counter[i] after merge i - 1");
				}
				this->swap(counter[fill-1]);
		}
};

int main()
{
		my_list<int> m;
		m.push_back(3);
		m.push_back(5);
		m.push_back(4);
		m.push_back(1);
		m.push_back(2);
		pr_arr(m, "before my_sort\n\n\n");
		m.my_sort();
		pr_arr(m, "\n\n\nafter my_sort");
		return 0;
}

配合程序裏的輸出,大概理解了如何對雙端鏈表進行排序

第一次 while (! this->empty()) 循環之前是這樣的

第一次 splice 之後:

此時 i = 0, fill = 0, i < fill 不成立, 直接跳過中間的 while, 進入到下面部分 carry 和 counter[0] 進行 swap, 此時

 

第二次 while (! this->empty()) 循環, splice 之後:

此時, i = 0, fill = 1, counter[0] 不爲空,進入 while (i < fill && !counter[i].empty()) 中,這是剛進入的這個 while block 第一次結束前的狀態

此時, i = 1, carry 剛用 swap 把 counter[0] merge 之後的內容換了出來,如上圖所示,而 fill 也 爲 1, 和 i 的值相同,所以這個 while block 執行一次就完成了,下面是 ++fill 這一句之後的狀態

第三次 while (! this->empty()) 循環, splice 之後:

此時, i = 0, fill = 2, counter[0] 爲空,while (i < fill && !counter[i].empty()) 不符合,故不會進入block, 下面是 if (i == fill) ++fill 之後的狀態

第四次 while (! this->empty()) 循環, splice 之後:

此時, i = 0, fill = 2, counter[0] 不爲空,進入 while (i < fill && !counter[i].empty()) , 該 while block 第一次結束前狀態

第二次進入 while (i < fill && !counter[i].empty()) , 此時, i = 1, fill = 2, counter[1] 不爲空,結束前:

此時 i = 2, fill = 2, 結束 while (i < fill && !counter[i].empty()) 循環,這是 if (i == fill) ++fill; 之後的狀態:

最後一次 while (! this->empty()) 循環, splice 之後:

此時, i = 0, fill = 3, counter[0] 爲空,不會進入 while (i < fill && !counter[i].empty()) , 這是 if (i == fill) ++fill; 之後的狀態:

此時, while (! this->empty()) 結束,進入 如下循環

for (int i = 1; i < fill; ++i)
{
        counter[i].merge(counter[i-1]); 
}

i = 1 結果:

 

i = 2 結果:

最後,再把 list 和 counter[2] swap 一次,就大功告成了

簡單來說,就是每湊成 2的平方個元素,就進行一次 merge, 總共進行 log2(n) 次 merge, 每次 merge 進行 n(實際上是2, 4, 8, 16... 次) 次操作,得出時間複雜度爲 O(nlog2(n))

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章