【数据结构】--快速排序

👉快速排序递归法

快速排序递归的基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该程,直到所有元素都排列在相应位置上为止

也就是说,分为若趟排序,每一趟排序都把一个元素排到它的指定位置。再以这个元素为分界,分成左右两个数列,再分别对左右两个数列循环重复使用该方法,每一次都确定一个元素的位置,直至数列全部有序

🍗hoare法

这个方法的基本思想:利用左右两个指针,假设每一趟需要确定位置的元素为key,key为最左边的元素,左指针指向最左边,右指针指向最右边。利用右指针找比key小的元素,找到后右指针先不动。再利用左指针找比key大的元素,找到后左右指针指向的元素交换。直至左右指针相遇,再把相遇点的元素和key交换,这样排完一趟后,key的左边都是比它小的元素,右边都是比它大的元素
在这里插入图片描述
这样单趟排完后,可以看到5(key)这个元素的左边都是比它小的,右边都是比它大的。
单趟排完后,以5作为分割,先排序5左边的数列,每次生成的
key
都作为分割,先把左边的数列排列有序,之后再排序key右边的数列直至整个数列全部有序即可。
利用递归法,当右指针等于或小于左指针的时候,该次递归结束

下图为全程演示
在这里插入图片描述

//hoare
int PartQuickSort1(int* a, int left, int right) {
	//单趟排序能够将一个数据排好,不需要再动了
	int key = left;
	while (left < right) {
		//右指针找小
		while (left < right && a[right] >= a[key])
			right--;

		//左指针找大
		while (left < right && a[left] <= a[key])
			left++;

		//左右都找到后交换两个数据
		//要注意避免左指针比右指针大了也交换
		if (left < right)
			Swap(&a[left], &a[right]);
	}
	
	//交换key和左右指针相遇点的数据
	int meet = left;
	Swap(&a[meet], &a[key]);

	//返回相遇点
	return meet;
}

void QuickSort(int* a, int begin, int end) {
	//递归结束的条件:当begin大于end时,或者等于end时
	if (begin >= end)
		return;

	int key = PartQuickSort1(a, begin, end);

	//单趟排序能够将一个数据排好,不需要再动了
	//所以递归实现每一次key位置的左边区间和右边区间
	QuickSort(a, begin, key - 1);
	QuickSort(a, key + 1, end);
}

🍗挖坑法

挖坑法和hoare法差不多,个人感觉挖坑法比hoare法的出错率更低
基本思想:先用key记录下最左边的值,然后最左边的位置形成一个坑位,右指针往前找比key要小的元素,找到之后填入坑位,右指针的位置变成一个新的坑位;接着左指针往后找比key大的元素,找到后填入右指针指向的坑位,左指针的位置变成一个新的坑位,依次往复循环,直到左指针和右指针相遇,相遇点就是一个坑位,再把key填入该坑位即可。

一趟排序过后,key的左边都是比它小的元素,右边都是比它大的元素
在这里插入图片描述
递归思想和hoare法一样,每一次都以新的key为分割,先把左子区间排列有序,再把右子区间排列有序

下图为全程演示
在这里插入图片描述

//挖坑法
int PartQuickSort2(int* a, int left, int right) {
	int key = a[left];
	//记录坑位
	int hole = left;

	while (left < right) {
		//右边找小
		while (left < right && a[right]>key)
			right--;

		//找到之后放入坑位
		//并且更新坑位
		a[hole] = a[right];
		hole = right;

		//左边找大
		while (left < right && a[left] < key)
			left++;

		//找到之后放入坑位
		//并且更新坑位
		a[hole] = a[left];
		hole = left;
	}

	//将key填入最后一个坑位
	a[hole] = key;

	//返回相遇点
	return hole;
}

void QuickSort(int* a, int begin, int end) {
	//递归结束的条件:当begin大于end时,或者等于end时
	if (begin >= end)
		return;

	int key = PartQuickSort2(a, begin, end);

	//单趟排序能够将一个数据排好,不需要再动了
	//所以递归实现每一次key位置的左边区间和右边区间
	QuickSort(a, begin, key - 1);
	QuickSort(a, key + 1, end);
}

🍗前后指针法

基本思想:key的值为最左边的元素,利用前后指针,后指针先走找比key小的元素,如果找到了前指针往后走一步,当前指针比key大,后指针同时比key小时,交换两个指针的元素。当后指针比数列最右边的位置还后时,前指针指向的位置和key的位置交换。一趟排序后,key的左边都是比它小的元素,右边都是比它大的元素
在这里插入图片描述
这个方法的递归思想和前面的都一样,每一次都以新的key为分割,先把左子区间排列有序,再把右子区间排列有序

//前后指针法
int PartQuickSort3(int* a, int left, int right) {
	int key = left;
	//创建前后指针
	int prev = left;
	int cur = left + 1;

	while (cur <= right) {
		//如果后指针小于key位置的值
		//前指针往前走一步
		if (a[cur] < a[key])
			prev++;

		//如果后指针的值小于key位置的值
		//并且前指针的值大于key位置的值
		//则两个位置的数据交换
		//小的到前面,大的到后面
		if (a[prev] > a[key] && a[cur] < a[key])
			Swap(&a[prev], &a[cur]);

		cur++;
	}

	//走到最后,交换前指针和key位置的值
	Swap(&a[key], &a[prev]);

	return prev;
}

void QuickSort(int* a, int begin, int end) {
	//递归结束的条件:当begin大于end时,或者等于end时
	if (begin >= end)
		return;

	int key = PartQuickSort3(a, begin, end);

	//单趟排序能够将一个数据排好,不需要再动了
	//所以递归实现每一次key位置的左边区间和右边区间
	QuickSort(a, begin, key - 1);
	QuickSort(a, key + 1, end);
}

👉对于快速排序递归法的优化

🍗三数取中作key

对于以上递归的几种方法都有一个公共点那就是都得取一个key值,但是会有特殊的情况;例如如果是排序一个逆序时,如果还是按照取最左边的话效率就会低下一点,这时候我们可以对key这个值进行优化一下。
对最左值,最右值,中间位置的值,这三个元素进行比较,取三数中第二大的元素,然后和最左边的元素进行交换
这样一来就能够提升效率了,下面以挖坑法为例

int GetMIdIndex(int* a, int left, int right) {
	//取最左边,中间,最右边,三数中的中间值
	int mid = left + (right - left) / 2;
	if (a[left] < a[mid]) {
		if (a[mid] < a[right])
			return mid;
		else if (a[left] > a[right])
			return left;
		else
			return right;
	}

	else {
		if (a[mid] > a[right])
			return mid;
		else if (a[left] < a[right])
			return left;
		else
			return right;
	}
}

//挖坑法
int PartQuickSort2(int* a, int left, int right) {
	//三数取中优化,将取出的key值和最左边交换
	int mid = GetMIdIndex(a, left, right);
	Swap(&a[left], &a[mid]);

	int key = a[left];
	//记录坑位
	int hole = left;

	while (left < right) {
		//右边找小
		while (left < right && a[right]>key)
			right--;

		//找到之后放入坑位
		//并且更新坑位
		a[hole] = a[right];
		hole = right;

		//左边找大
		while (left < right && a[left] < key)
			left++;

		//找到之后放入坑位
		//并且更新坑位
		a[hole] = a[left];
		hole = left;
	}

	//将key填入最后一个坑位
	a[hole] = key;

	//返回相遇点
	return hole;
}

其他的两个方法也是一样的,取出中间值后,再进行和最左值交换

🍗小区间优化法

对于递归来说,越往下深度越深,也就是次数会越多。如果递归到需要排序的数列只有几个元素的时候,再使用递归法就会极大的消耗时间,所以我们到不如可以判断一下,如果需要排序的数列只有几个元素了,我们直接使用插入排序将它排好,这样可以极大的减少递归的深度

void QuickSort(int* a, int begin, int end) {
	//递归结束的条件:当begin大于end时,或者等于end时
	if (begin >= end)
		return;

	//进行小区间优化
	if (end - begin <= 8)
		InsertSort(a + begin, end - begin + 1);

	else {
		int key = PartQuicksort3(a, begin, end);

		//单趟排序能够将一个数据排好,不需要再动了
		//所以递归实现每一次key位置的左边区间和右边区间
		QuickSort(a, begin, key - 1);
		QuickSort(a, key + 1, end);
	}
}

👉快速排序非递归法

既然有了递归法,那我们也需要学会使用非递归法
快速排序的非递归法需要用到栈的先入后出的性质,这里就不写栈的创建了,具体可以看之前的文章 栈和队列

模拟递归的流程,我们可以先把数列的最左位置和最右位置放入栈里,然后依次取出,取出来后再进行任意方法的单趟排序,将第一次的key排好,然后更新key,再分成两个部分,对最左和key-1的位置入栈,对key+1和最右的位置入栈,这样取出来的时候就呼应上了。循环反复这样执行,但是要注意如果最左的位置不小于最右的位置时,那么就不再进行入栈操作,也就是说这个时候栈就为空了,那我们的循环就结束。

//快速排序非递归法
void QuickSortNonR(int* a, int begin, int end) {
	ST st;
	StackInit(&st);
	//先入左再入右
	StackPush(&st, begin);
	StackPush(&st, end);

	while (!StackEmpty(&st)) {
		//取出最右
		int right = Stacktop(&st);
		StackPop(&st);

		//取出最左
		int left = Stacktop(&st);
		StackPop(&st);

		if (left >= right)
			continue;

		int key = PartQuickSort2(a, left, right);

		//对key位置进行分割,分别入栈

		StackPush(&st, key + 1);
		StackPush(&st, right);

		StackPush(&st, left);
		StackPush(&st, key - 1);
	}

	StackDestroy(&st);
}

👉总结

快速排序相较于上几个排序思路也没有复杂很多,但是方法很大,最好是全部都能理解吃透,整体而言还是比较有难度的,需要勤加练习。
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值