快速排序(重温经典算法系列)







原理简述

随机选取哨兵元素值;
经 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章