【数据结构与算法】快速排序

本文深入探讨了快速排序的三种实现方式(Hoare法、挖坑法和前后指针法),分析了其在有序或近乎有序数据下的不足,并介绍了随机选择key、三数取中优化和非递归版本的解决方案。通过实例讲解和改进策略,提升排序效率并降低栈溢出风险。
摘要由CSDN通过智能技术生成

目录

一、快速排序定义

二、排序思想

1.Hoare法

2.挖坑法 

3.前后指针法

三、不足及改进

1.不足

2.改进

四、由递归改为非递归


一、快速排序定义

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。

快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案:

快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

二、排序思想

1.Hoare法

Hoare的这个版本是最早的版本,首先要选择一个key作为基准,一般选最左边或者最右边的值,也可以选择中间值,单趟排完以后:要求左边比key小,右边比key大

以最左边为key举例,要求右边先走,寻找比key小的值,然后左边走,去寻找比key大的值,然后交换这两个值。左右两边的两个下标依次交替向中间靠拢直到它们二者相交,

然后将交点的值与key所指向的值交换。

这就是单趟排序之后的结果。

单趟排序后,会发现key左边的值都比key小,右边的值都比key大,但是[left,key-1]

[key+1,right]这两个区间不是有序的,采用分治的思想,key的左区间有序,key的右区间也有序,则整体有序。

void QuickSort(int* a, int begin, int end)
{

	if (begin >= end)
	{
		return;
	}

	int left = begin;
	int right = end;
	int keyi = left;

	while (left < right)
	{
		//右边先走找比key小的值
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}

		//左边走,找比key大的值
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		//交换
		Swap(&a[left], &a[right]);
	}

	Swap(&a[keyi], &a[left]);

	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

2.挖坑法 

挖坑法与第一个版本思想上是比较类似的,先将第一个数据存放在临时变量key中,形成一个坑位

然后右边走寻找比key小的值,复制到坑里,这个值的位置形成新的坑,左边寻找比key大的值,存放到坑里,这个值也形成新的坑,直到两者相交,然后将key里的值放到坑里,单趟就结束了。

然后递归左区间,右区间,让左右区间分别有序。

这是单趟排序,剩下的大框架与前面一致,快速排序递归版本的框架都是一样的,唯一不同的只是单趟排序。

//挖坑法
int PartSort2(int* a, int begin, int end)
{
	
	int key = a[begin];
	int piti = begin;


	while (begin < end)
	{
		while (begin < end && a[end] >= key)
		{
			end--;
		}
		a[piti] = a[end];
		piti = end;

		while (begin < end && a[begin] <= key)
		{
			begin++;
		}
		a[piti] = a[begin];
		piti = begin;
	}
	a[piti] = key;
	return piti;
}

3.前后指针法

前后指针的方法与前两种方法有略微不同

有两个指针一个是prev另一个是cur

如果cur指向的值小于key,那么就交换prev和cur的值,cur和prev都想后挪一位,

反之不然,cur向后挪一位

最后交换prev和key的值,让prev的值作为下一个key

//前后指针法
int PartSort3(int* a, int begin, int end)
{
	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;


	while (cur <= end)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[prev], &a[cur]);
		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	keyi = prev;

	return keyi;
}

三、不足及改进

1.不足

快速排序在有序或者接近有序时时间复杂度为O(N^2)

上述三种方法,都要寻找key,然后让区间中的值与key进行比较,如果每次寻找key都是区间的最小值或者是最大值时,第一行要交换N次。第二行要交换N-1次,依次往后推算到最后一行要交换1次,交换次数是一个等差数列,大致推算出它的时间复杂度为O(N^2)

同时数据量较大时会因为递归的深度过高出现栈溢出

快速排序递归类似于二叉树,一个具有N个节点的二叉树的深度是LogN,也就是说快速排序的深度是LogN,极有可能出现栈溢出现象。

2.改进

1.随机选取key

2.运用三数取中,选取既不是最大的也不是最小的数来作为key

3.在数据量较小时采用插入排序

上述三种方法是用来应对第一个缺点的处理方法

先说明三数取中逻辑比较简单取出三个数中间大小的那个数

这样可以避免每次取到最大或者最小的值

int GetMidIndex(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (begin < mid)
	{
		if (a[mid] < a[end])
		{
			return mid;
		}
		else if (a[begin] < a[end])
		{
			return end;
		}
		else
		{
			return begin;
		}
		
	}
	else   //begin >= mid
	{
		if (a[mid] > a[end])
		{
			return mid;
		}
		else if (a[begin] < a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
}

接下来是区间优化

当递归划分小区间时,区间比较小的时候,就不再递归划分去排序这个小区间,考虑直接用其它排序对小区间处理

//hoare版本  加入三数取中,和小区间优化
int PartSort1(int* a, int begin, int end)
{
	int left = begin;
	int right = end;
	int keyi = left;

	int mini = GetMidIndex(a, begin, end);
	Swap(&a[mini], &a[keyi]);

	while (left < right)
	{
		//右边先走找小
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		//左边走找大
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		//交换
		Swap(&a[left], &a[right]);
	}

	Swap(&a[keyi], &a[left]);
	keyi = left;
	return keyi;
}
//挖坑法
int PartSort2(int* a, int begin, int end)
{
	
	int key = a[begin];
	int piti = begin;

	int mini = GetMidIndex(a, begin, end);
	Swap(&a[mini], &a[piti]);

	while (begin < end)
	{
		while (begin < end && a[end] >= key)
		{
			end--;
		}
		a[piti] = a[end];
		piti = end;

		while (begin < end && a[begin] <= key)
		{
			begin++;
		}
		a[piti] = a[begin];
		piti = begin;
	}
	a[piti] = key;
	return piti;
}

//前后指针法
int PartSort3(int* a, int begin, int end)
{
	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;

	int mini = GetMidIndex(a, begin, end);
	Swap(&a[mini], &a[keyi]);

	while (cur <= end)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[prev], &a[cur]);
		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	keyi = prev;

	return keyi;
}

四、由递归改为非递归

可以借助栈这个数据结构,来模拟递归过程

快速排序递归过程是先处理左区间,然后处理右区间

因为栈的特点,后进先出,要先将右区间入栈,然后将左区间入栈

同时区间的两个端点的顺序也是同理,先入右端点,再入左端点

如果区间至少有一个值,那么就继续入栈,直到栈为空

与二叉树的层序遍历类似。

void QuickSortNonR(int* a, int begin, int end)
{
	ST st;
	StackInit(&st);

	StackPush(&st, end);
	StackPush(&st, begin);

	while (!StackEmpty(&st))
	{
		int left = StackTop(&st);
		StackPop(&st);

		int right = StackTop(&st);
		StackPop(&st);

		int keyi = PartSort3(a, left, right);

		if (keyi + 1 < right)
		{
			StackPush(&st, right);
			StackPush(&st, keyi + 1);
		}

		if (left < keyi - 1)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, left);
		}
	}
	StackDestroy(&st);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值