快速排序優化
快速排序是一種以分治為基本思想的排序演算法,它將待排序陣列分成兩個子陣列,將兩部分再獨立地排序。其中涉及到三個重要操作:選擇基準元素、切分、對切分得到的子陣列進行快排(分治)。因此對快速排序的優化也應針對這三部分進行。
優化基準元素選擇
快速排序最理想的情況是每次都能將序列切分成相同大小的兩部分,因此切分所用的基準元素如何選擇就成了影響演算法效能的關鍵。最簡單的實現就是採用相對位置固定的基準元素——一般是序列的第一個或最後一個元素。但這樣往往會導致糟糕效能的出現,因為這樣很容易出現很不平衡的切分序列。例如針對已經有序的序列或含有大量重複元素的序列。
(1)隨機選擇基準元素
選取固定位置的元素作為基準對於已經有序的序列(不一定是初始序列就全部有序,子序列有序也可能會導致效能降低)是糟糕的選擇,隨機選取基準元素就可以大大改善對此類序列的排序效能。
#include <vector> #include <random> using namespace std::vector; using std::random_device; using std::uniform_int_distribution; void QuickSort(vector<int> &arr, vector<int>::size_type first, vector<int>::size_type last) { if (first < last) { vector<int>::size_type i = first, j = last; random_device rd; uniform_int_distribution<int> d(1, arr.size()-1); vector<int>::size_type index = d(rd); swap(arr[first], arr[index]); int key = arr[first]; while (i != j) { while (j > i && arr[j] > key) --j; arr[i] = arr[j]; while (i < j && arr[i] < key) ++i; arr[j] = arr[i]; } arr[i] = key; QuickSort(arr, first, i-1); QuickSort(arr, i+1, last); } }
(2) 中位數取樣
既然我們想要快排在每一次切分中都能獲得兩個等長的子序列,那麼自然會想到用中位數。最簡單的方法是在序列的第一個元素、最後一個元素和中間元素中選出中位數來作為基準元素。我們也可以從由左中右三個中選取擴大到五個元素中或者更多元素中選取,一般的,會有(2t+1)平均分割槽法(median-of-(2t+1),三平均分割槽法median-of-three)。以下原始碼採用最簡單的實現。
#include <vector> using namespace vector; void QuickSort(vector<int> &arr, vector<int>::size_type first, vector<int>::size_type last) { if (first < last) { vector<int>::size_type i = first, j = last, mid = first+((last-first)>>1); if (arr[mid] > arr[last]) swap(arr[mid], arr[last]); if (arr[first] > arr[last]) swap(arr[first], arr[last]); if (arr[mid] > arr[first]) swap(arr[mid], arr[first]); int key = arr[first]; while (i != j) { while (j > i && arr[j] > key) --j; arr[i] = arr[j]; while (i < j && arr[i] < key) ++i; arr[j] = arr[i]; } arr[i] = key; QuickSort(arr, first, i-1); QuickSort(arr, i+1, last); } }
中位數取樣法對於部分或全部已有序的序列也有很好的效能。
優化小序列
對於小序列來說,快速排序比插入排序要慢,因此在快排切分到子序列足夠小時改呼叫插入排序能很好地提升效能。
#include <vector>
using namespace vector;
InsertionSort(vector<int> &arr, vector<int>::size_type first, vector<int>::size_type last) {
if (first < last) {
for (vector<int>::size_type i = 1; i < arr.size(); ++i)
if (a[i] < a[i-1])
for (vector<int>::size_type j = i; j < arr.size() && a[j] < a[j-1]; --j)
swap(a[j], a[j-1]);
}
}
void QuickSort(vector<int> &arr, vector<int>::size_type first, vector<int>::size_type last) {
if (first < last) {
if (last <= first+15) {
InsertionSort(arr, first, last);
return ;
}
vector<int>::size_type i = first, j = last;
int key = arr[first];
while (i != j) {
while (j > i && arr[j] > key)
--j;
arr[i] = arr[j];
while (i < j && arr[i] < key)
++i;
arr[j] = arr[i];
}
arr[i] = key;
QuickSort(arr, first, i-1);
QuickSort(arr, i+1, last);
}
}
尾遞迴優化
如果待排序的子序列劃分極不平衡,遞迴的深度將趨近於n,而棧的大小是很有限的,每次遞迴呼叫都會耗費一定的棧空間,函式的引數越多,每次遞迴耗費的空間也越多,很容易導致棧溢位,並且系統在維護函式棧及棧跳轉等方面的時間和空間開銷都很大。優化後,可以縮減堆疊深度,由原來的O(n)縮減為O(nlogn),將會提高效能。
#include <vector>
using namespace vector;
void QuickSort(vector<int> &arr, vector<int>::size_type first, vector<int>::size_type last) {
while (first < last) {
vector<int>::size_type i = first, j = last, key = arr[first];
while (i != j) {
while (j > i && arr[j] > key)
--j;
arr[i] = arr[j];
while (i < j && arr[i] < key)
++i;
arr[j] = arr[i];
}
arr[i] = key;
QuickSort(arr, first, i-1);
first = i+1;
}
}
三向切分
前面說到的優化思路都無法解決對含有大量重複元素的序列的排序效能低下的問題。針對這一類序列,我們希望避免對於重複元素進行排序,但之前的優化都不能解決這個問題,快速排序的遞迴性會使元素全部重複的子序列經常出現。
一個簡單的做法是將序列切分為三部分,分別對應小於、等於和大於基準元素的序列元素。實現這一點需要維護三個指標:一個指標lt使得arr[first, lt-1]的元素都小於基準元素,一個指標gt使得[gt+1, last]的元素都大於基準元素,最後一個指標使得[lt, i-1],也是[lt, gt]的元素都等於基準元素。
#include <vector>
using namespace vector;
void QuickSort(vector<int> &arr, vector<int>::size_type first, vector<int>::size_type last) {
if (first < last) {
vector<int>::size_type lt = first, gt = last, i = first;
while (i <= gt) {
if (arr[i] < key) {
swap(arr[i], arr[lt]);
++lt, ++i;
}
else if (arr[i] > key) {
swap(arr[i], arr[gt]);
--gt;
}
else
++i;
}
QuickSort(arr, first, lt-1);
QuickSort(arr, gt+1, last);
}
}
然而三向切分快排對於沒有(或很少)重複元素的序列的效能反而會比普通快排要慢,因為它的交換次數要比普通快排多。
總結
以上都是對單一優化方式進行分析,如果將這些優化思路合理地組合起來得到的效果會更好,例如:中位數取樣+小序列插排+三向切分+尾遞迴的組合效率就比任何一種單一優化方式效能要好。
總之,沒有最好的演算法,只有最合適的演算法——從以上對快排的優化可以得出這樣的結論 。根據資料特點因地制宜選擇合適的演算法才是我們要做的。