快速排序--排序村的上忍

本文详细介绍了快速排序的三种递归实现版本(霍尔版、挖坑法、前后指针法),以及优化策略如三数取中和三路划分,针对大量重复数据的处理方法,并探讨了非递归实现及时间复杂度分析。
摘要由CSDN通过智能技术生成

快速排序

递归实现快排的三个版本

version1–霍尔版

霍尔法的核心思想是: 先取左边第一个数为key,先从数组右边找小于key的数,再从左边
找大于key的数,找到就交换,直到左右相遇在同一个数,再将相遇点的数与key交换。 如此,便实现了 目标:key左边为小于key的数,key右边为大于key的数。 再将key左边的数组和右边的数组同样如此操作,便最终实现整体有序。

问:为什么能保证最后相遇点的数一定比key小(或等于)?
答:情况一,key即为最小的数,那么先走的right一路狂奔找到key,同时也是left,那么直接循环结束,swap自身即可,依然实现目标。情况二,key不为最小数,那么right依然先找到比key小的数。如果是left主动遇right,那么相遇点即right位置。如果是right主动碰left,且排除了第一种情况,那么一定是交换后,此时left是交换前right的值,即比key小的值。

注:霍尔是快排之父。

int PartSort1(int* a, int left, int right)
{
	//int midi = GetMidIndex(a, left, right);此为三数取中
	//Swap(&a[midi], &a[left]);

	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
			right--;
		while (left < right && a[left] <= a[keyi])
			left++;
		Swap(&a[right], &a[left]);
	}
	Swap(&a[keyi], &a[left]);
	return left;
}

代码有如下细节

1. 如果选左边为key,则需先走right。
2. 第二层while里,须先判断left<right,同时right,left与key的比较应带=。

version2–挖坑法

挖坑法的核心思想: 依然取左边第一个数为key,其下标记为hole。依然先从右边找比key小的值,找到即将right的值挖出填到hole里。此时,right位置形成新的坑hole。再从左找大,同样形成新坑。最终相遇后将hole处赋为key,也能实现目标。

int PartSort2(int* a, int left, int right)
{
	//int midi = GetMidIndex(a, left, right);
	//Swap(&a[midi], &a[left]);

	int hole = left;
	int key = a[hole];
	while (left < right)//如果左边k,依然需要右边先走,否则left移到的right位可能比key小则error
	{
		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;
	}
	a[hole] = key;
	return hole;
}

细节处同版本一。

version3–前后指针法

前后指针法的核心思想: 前指针cur指向第二个数,后指针prev指向第一个数,仍取第一个数下标为keyi。cur前进时,遇到比key大即跳过,于是prev与cur之间隔了比key大的数。遇到比key小的数则与prev的大数交换即可。因为prev最终指向小于等于key的位置,最后交换keyi与prev即可,如此也能实现目标。

问:为什么prev最终指向比key小(或等于)的值?
答:如果发生了交换,则prev指向小值。如没发生,则指向keyi位置本身。

int PartSort3(int* a, int left, int right)
{
	//int midi = GetMidIndex(a, left, right);
	//Swap(&a[midi], &a[left]);

	int prev = left, cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)//防止与自身交换
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	return keyi;
}

三个版本在效率上并无本质差别。

快排的优化

三数取中

三数取中旨在使key尽可能是数组的中间值,因为当key为最小值或最大值时,时间复杂度均接近O(N2),只有当key不大不小时,效率最高。
因此采取在数组的头尾与N/2处取中间值的做法进行优化。

int GetMidIndex(int* a, int left, int right)//三数取中优化qsort
{
	//int mid = left + rand() % (right - left);//随机取中--防针对(记得srand)
	int mid = (left + right) / 2;
	if (a[mid] > a[left])
	{
		if (a[mid] < a[right])
			return mid;
		else
			return a[left] > a[right] ? left : right;
	}
	else
	{
		if (a[mid] > a[right])
			return mid;
		else
			return a[left] < a[right] ? left : right;
	}
}

三路划分----针对大量重复数据

核心思想:将小数放在左边,大数放在右边,等于key的就在中间。如此针对重复数据,时间复杂度为O(N)。

//快排--三路划分(小的在左,大的在右,等于的在中间)--针对大量重复数据
void QuickSort3(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	
	int midi = GetMidIndex(a, begin, end);
	Swap(&a[midi], &a[begin]);

	int key = a[begin];
	int left = begin;
	int right = end;
	int cur = begin;
	while (cur <= right)
	{
		if (a[cur] < key)
		{
			Swap(&a[cur], &a[left]);
			++left;
			++cur;
		}
		else if (a[cur] > key)
		{
			Swap(&a[cur], &a[right]);
			--right;
		}
		else
		{
			++cur;
		}
	}
	QuickSort3(a, begin, left);
	QuickSort3(a, right, end);
}

随机取中

针对的是oj题的针对----oj故意使头尾中间的数均为大数或小数,使三数取中失效。

随机取中即三数取中的代码注释部分。如下

int mid = left + rand() % (right - left);//随机取中--防针对(记得srand)

快排的非递归实现

本质是用自制栈类比模拟递归,从而实现非递归。

void QuickSortNonR(int* a, int begin, int end)//用自制栈类比递归,实现非递归
{
	ST st;
	STInit(&st);
	STPush(&st, end);
	STPush(&st, begin);
	while (!STEmpty(&st))
	{
		int left = STTop(&st);
		STPop(&st);
		int right = STTop(&st);
		STPop(&st);
		int keyi = PartSort3(a, left, right);
		if (left < keyi - 1)
		{
			STPush(&st, keyi - 1);
			STPush(&st, left);
		}
		if (keyi + 1 < right)
		{
			STPush(&st, right);
			STPush(&st, keyi + 1);
		}
	}
	STDestroy(&st);
}

快排的时间复杂度

快排本质上有二分的思想,通过将左右区间有序(时间复杂度为N),重复分割区间(时间复杂度为近似log2N)。

时间复杂度平均为O(N*log2N),这个段位足以让他成为一个优秀的排序方法,堪称排序村的上忍,与希尔排序和归并排序并列,是为“三忍”。
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值