快速排序(重溫經典算法系列)







原理簡述

隨機選取哨兵元素值;
經 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個版本(單雙三)的快排版本.
具體實現代碼和測試數據我會在後續文章中發表,並更新出來鏈接.


參考資料


快速排序的圖形化解說,可以參考:

C/C++實現三路快速排序算法原理

實驗精神( 😃 數據)可以參考:
快速排序及優化


快速排序算法的動畫過程演示,可以參考:
快速排序算法過程演示

交流方式
QQ —— 2636105163(南國爛柯者)

溫馨提示:

轉載請註明出處!!


文章最後更新時間: 2020年3月30日00:54:49
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章