:要相信自己哈 :)
原理簡述
隨機選取哨兵元素值;
經 partition 劃分操作,將原數組元素劃分爲左右兩部分:
arr[low...p-1] <= arr[p] ;
arr[p+1...high] >= arr[p];
p 爲當前哨兵值在整個數組元素有序時自己位置處的索引.
經典的快速排序算法爲單路快排.
具體實現如下:
(單路)快速排序
//(單路)快速排序;
// 對arr[low...high]部分進行partition操作;
// 返回p, 使得arr[low...p-1] <= arr[p] ; arr[p+1...high] >= arr[p];
template <typename T>
int __Partition(T arr[], const int low, const int high) {
// 隨機在arr[low...high]的範圍中, 選擇一個數值作爲標定點pivot;
std::swap(arr[low], arr[std::rand() % (high - low + 1) + low]);
T temp = arr[low];
int pos = low;
for (int i = low + 1; i <= high; i++)
if (arr[i] < temp) {
++pos;
std::swap(arr[pos], arr[i]);
}
std::swap(arr[low], arr[pos]);
return pos;
}
// 對arr[low...high]部分進行快速排序;
template <typename T>
void __QuickSort(T arr[], int low, int high) {
// 對於小規模數組, 使用插入排序進行優化;
if (high - low <= 15) {
DirectInsertionSort(arr, low, high);
return;
}
//// 調用快速排序的partition;
//出現未知問題;問題已查清,設計出錯導致———有無符號數比較大小BUG問題;
int pos = __Partition(arr, low, high);
__QuickSort(arr, low, pos - 1);
__QuickSort(arr, pos + 1, high);
}
template <typename T>
void QuickSort1Ways(T arr[], const std::size_t n) {
//srand(time(NULL));//修改如下;
std::random_device rd;//隨機種子;
srand(rd());
__QuickSort(arr, 0, n - 1);
}
溫馨提示:
1.隨機化函數的調用,需要補充 random 頭文件.
即:
#include <random>
2.代碼中間採用了小規模時,可以通過使用直接插入排序算法實現優化;
該部分代碼鏈接在我的另外一篇文章裏,鏈接如下:
//重載直接插入排序;
template <typename T>
void DirectInsertionSort(T* arr, const int low, const int high) {//high極可能會傳入負數值,故而不可使用無符號數;
assert(arr);
//i索引遍歷待排序元素;
for (int i = low + 1; i <= high; ++i) {
T temp = arr[i];
int j;
for (j = i; j > low && arr[j - 1] > temp; --j) {
arr[j] = arr[j - 1];//把前邊的元素往後挪,用這一操作替換掉每一次的數值交換swap,能減少開銷;
}
//找到合適插入位置j,第二次循環提前終止;
if (j != i) {
arr[j] = temp;
}
}
}
算法優化:改造單路爲雙路快排
類似前邊的算法優化思想:
單向掃描往往可以改造成爲雙向同時進行掃描,這似乎是個普適的優化方式.
遞歸實現
// 雙路快速排序的partition;
// 返回p, 使得arr[low...p-1] <= arr[p] ; arr[p+1...high] >= arr[p];
// 雙路快排處理的元素正好等於arr[p]的時候要注意,詳見下面的註釋:);
template <typename T>
int __Partition2(T arr[], int low, int high) {
// 隨機在arr[low...high]的範圍中, 選擇一個數值作爲標定點pivot;
// 隨機標定點處理遞歸樹不平衡問題(極端不平衡時,算法複雜度由NlgN退化爲N^2);
std::swap(arr[low], arr[std::rand() % (high - low + 1) + low]);
T temp = arr[low];
// arr[low+1...i) <= temp; arr(j...high] >= temp;
int i = low + 1, j = high;
while (true) {
while (i <= high && arr[i] < temp)
i++;
while (j >= low + 1 && arr[j] > temp)
j--;
// 注意這裏的邊界, arr[i] < temp, 不能是arr[i] <= temp;
// arr[j] > temp, 不能是arr[j] >= temp, 思考一下爲什麼?;
// 左右部分劃分的平衡問題;
if (i > j)
break;
std::swap(arr[i], arr[j]);
i++;
j--;
}
std::swap(arr[low], arr[j]);
return j;
}
// 對arr[low...high]部分進行快速排序;
template <typename T>
void __QuickSort2(T arr[], int low, int high) {
// 對於小規模數組, 使用插入排序進行優化;
if (high - low <= 15) {
DirectInsertionSort(arr, low, high);
return;
}
//// 調用單路快速排序的partition;
//int pos = __Partition(arr, low, high);
// 調用雙路快速排序的partition;
int pos = __Partition2(arr, low, high);
__QuickSort2(arr, low, pos - 1);
__QuickSort2(arr, pos + 1, high);
}
template <typename T>
void QuickSort2Ways(T arr[], const std::size_t n) {
//srand(time(NULL));//修改如下;
std::random_device rd;//隨機種子;
srand(rd());
__QuickSort2(arr, 0, n - 1);
}
算法進一步優化:三路快排
雙路快排已經提到了一個問題:
那就是中間部分等於哨兵數值的所有元素,
如若一個數組存在着大量的重複元素,那麼極有可能會對劃分造成不利影響.
即劃分出左右兩部分長度相差極大、類似“遞歸樹”不平衡的問題;
這也正是雙向快排代碼,
partition2的兩個while循環的條件之所以不能取等號的原因:
arr[i] < temp;
arr[j] > temp.
改進辦法:
將數組劃分爲三部分,中間等於哨兵值的元素單獨成爲一個區間;
如此改進,便產生了三路快排算法.
三路快排(遞歸版)代碼
// 基於遞歸實現的三路快速排序算法;
template <typename T>
void __QuickSort3Ways(T arr[], int low, int high) {
// 對於小規模數組, 使用插入排序進行優化;
if (high - low <= 15) {
DirectInsertionSort(arr, low, high);
return;
}
// 隨機在arr[low...high]的範圍中, 隨機選擇一個數值作爲標定點pivot;
// 遞歸樹不平衡問題;
std::swap(arr[low], arr[rand() % (high - low + 1) + low]);
T temp = arr[low];
int lt = low - 1; // less than, arr[low...lt] < temp,初始狀態爲空;
int gt = high + 1; // greater than, arr[gt...high] > temp,初始狀態爲空;
int i = low; // arr[lt+1...i) == temp
while (i < gt) {
if (arr[i] < temp) {
std::swap(arr[i++], arr[++lt]);
}
else if (arr[i] > temp) {
std::swap(arr[i], arr[--gt]);
}
else { // arr[i] == temp
++i;
}
}
__QuickSort3Ways(arr, low, lt);
__QuickSort3Ways(arr, gt, high);
}
template <typename T>
void QuickSort3Ways(T arr[], const std::size_t n) {
std::random_device rd;//隨機種子;
srand(rd());
__QuickSort3Ways(arr, 0, n - 1);
}
三路快排(迭代版)代碼
上述三路快排給出的代碼是基於遞歸實現的,
迭代版本的代碼實現,可以參考文章:
三路快排能否進一步改進?
三路快排的優勢
由上邊的文字解說已經知道:
當一個待排序的數組存在大量的重複元素時,
三路快排優過經典快排(即單路快排)和雙路快排(或者叫雙向快排).
那麼,反過來思考一下?
這到底是三路快排的優點呢,還是它的缺陷???
三路快排的缺陷
考慮這麼集中反向的極端情況:
如果一個待排序的數組任意兩個元素都不重複,
那麼三路快排的優化部分的代碼不僅無效,反而增加了算法的開銷;
此時三路快排退化爲雙路快排,但是由於這部分額外開銷的存在,
使得三路快排的性能低過雙路快排.
這邊是三路的缺陷所在.
那麼,該如何改進呢?
辦法總比困難(問題)多!!!
三路快排的改進
如此設想一下:
既然沒有相等的元素,
那好,
總有相鄰的元素吧?
這是肯定的.
如果中間部分不是等於哨兵值的元素組成的區間,
而是一個以哨兵數值大小爲中點上下浮動的一個數值範圍呢?
如此設計,
既可以處理存在大量重複元素的待排序的隨機數組,
也可以處理任意兩個元素都不重複的數組;
這樣便使得一個普適版本的快速排序產生了,即
無論處理的是什麼極端數據,相對於經典的單路快排和雙路快排而言,它都
存在有優化的空間;
這樣一種加強型的三路快排甚至遠勝過經典的三路快排,
如果你能夠在每一次數組劃分時都能夠劃出3等分的話.
理想狀態下的加強型三路快排,改進之後會使得每一輪的partition操作
都能夠劃分出 3等分的數據.
畢竟是理想哈,:)
該如何使得現實的情況儘可能地接近理想狀態,這便已經成功了.
改進後的加強型三路快排,
經測試,
確實遠勝過其他 3個版本(單雙三)的快排版本.
具體實現代碼和測試數據我會在後續文章中發表,並更新出來鏈接.
參考資料
快速排序的圖形化解說,可以參考:
實驗精神( 😃 數據)可以參考:
快速排序及優化
快速排序算法的動畫過程演示,可以參考:
快速排序算法過程演示
交流方式
QQ —— 2636105163(南國爛柯者)
溫馨提示:
轉載請註明出處!!
文章最後更新時間: 2020年3月30日00:54:49