【数据结构】快速排序

1 基本思想
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

将区间按照基准值划分为左右两半部分的常见方式有:
1.hoare法 2.挖坑法 3.前后指针法
接下来将逐个介绍三种方法,假设取数据中最后一个元素作为基准值,排升序。
1.1 hoare法
在这里插入图片描述
让begin从前往后找比基准值大的元素,找到之后停止;让end从后往前找比基准值小的元素,找到之后停止;如果begin和end没有相遇:将begin位置上的元素和end位置上的元素进行交换;循环结束后将基准值和begin位置上的元素进行交换。
1.2 挖坑法
让begin从前往后找比基准值大的元素,找到之后停止,将begin位置上的元素覆盖掉end位置上的元素(begin填end的坑,填完之后begin形成新的坑);让end从后往前找比基准值小的元素,找到之后停止;将end位置上的元素覆盖掉begin位置上的元素,end形成新坑;最后用基准值填最后一个坑。
1.3 前后指针法
在这里插入图片描述

cur一直向前遍历,只有cur位置上的值比基准值小时,prev向前遍历。当cur和prev之间有差距,说明两者之间都为比基准值大的元素,交换cur和prev上的元素。最后将基准值和prev最后停下来位置上的元素进行交换。
2 快速排序的优化
2.1 三数取中法
上述三种方法的基准值默认是数据中的最后一个,这样取基准值拿到极值的概率非常高。例如:假设一组数据为1,2,3,4,5,6,7,8,9 我们现在要排降序,每次取基准值分别为9,8,7…效率非常低,时间复杂度为O(N^2)。最理想的情况是每次都能取到中间值(仅是理想情况)三数取中法就是从区间最左侧取一个数据,从区间最右侧取一个数据,再从中间取一个数据,三者比较取中间值,这样会大大降低取到极值的概率。
2.2 结合插入排序
以上采用递归实现快速排序,递归越往下,每个分组中的数据量越少,没有必要让区间中只有一个数据时再往回退,这个时候就比较适合插入排序。
3 代码实现
3.1 三数取中法

//三数取中法:三个数据最中间的数据作为基准值
int GetMiddleIndex(int array[], int left, int right)
{
	int mid = left + ((right - left) >> 1);
	//三个数据:left,mid,right-1
	if (array[left]<array[right - 1])
	{
		if (array[mid] < array[left])
			return left;
		else if (array[mid]>array[right - 1])
			return right - 1;
		else
			return mid;
	}
	else
	{
		if (array[mid] > array[left])
			return left;
		else if (array[mid] < array[right - 1])
			return right - 1;
		else
			return mid;
	}
}

3.2 hoare法

//hore法
int Partion1(int array[], int left, int right)
{
	int begin = left;
	int end = right - 1;
	int keyofindex = GetMiddleIndex(array, left, right);
	int key;
	if (keyofindex != right - 1)
		Swap(&array[keyofindex], &array[right - 1]);
	key = array[right - 1];
	while (begin<end)
	{
		//让begin从前往后找,找比基准值大的元素,找到了就停下来
		while (begin<end&&array[begin] <= key)
			begin++;
		//让end从后往前找,找比基准值小的元素,找到了就停下来
		while (begin<end&&array[end] >= key)
			end--;
		if (begin<end)
			Swap(&array[begin], &array[end]);
	}
	if (begin!=right-1)
		Swap(&array[begin], &array[right - 1]);
	return begin;
}

3.3 挖坑法

int Partion2(int array[], int left, int right)
{
	int begin = left;
	int end = right - 1;
	int keyofindex = GetMiddleIndex(array, left, right);
	int key;
	if (keyofindex != right - 1)
		Swap(&array[keyofindex], &array[right - 1]);
	key = array[right - 1];
	while (begin<end)
	{
		//让begin从前往后找,找比基准值大的元素,找到了就停下来
		while (begin<end&&array[begin] <= key)
			begin++;
		//让begin位置大的元素填end位置的坑
		if (begin < end)
		{
			array[end] = array[begin];
			end--;
		}
		//begin位置形成了一个新坑
		//让end从后往前找比基准值小的元素,填begin位置的坑
		//让end从后往前找,找比基准值小的元素,找到了就停下来
		while (begin<end&&array[end] >= key)
			end--;
		if (begin < end)
		{
			array[begin] = array[end];
			begin++;
		}
	}
	//用基准值填最后一个坑
	array[begin] = key;
	return begin;
}

3.4 前后指针法

int Partion3(int array[], int left, int right)
{
	int cur = left;
	int prev = cur - 1;
	int keyofindex = GetMiddleIndex(array, left, right);
	int key;
	if (keyofindex != right - 1)
		Swap(&array[keyofindex], &array[right - 1]);
	key = array[right - 1];
	while (cur<right)
	{
		if (array[cur] < key&&++prev != cur)
			Swap(&array[cur], &array[prev]);
		++cur;
	}
	if (++prev != right - 1)
		Swap(&array[right - 1], &array[prev]);
	return prev;
}

3.5 主体代码
采用递归方式

//[left,right)表示待排序元素的区间
void QuickSort(int array[], int left, int right)
{
	if (right - left < 16)
	{
		//[left,right)区间中的数据少到一定程度,使用插入排序优化
		InsertSort(array + left, right - left);
	}
	else
	{
		//Partion按照基准值对区间进行划分成两部分
		//左部分元素比基准值小,右侧部分比基准值大
		int div = Partion3(array, left, right);
		QuickSort(array, left, div);
		QuickSort(array, div + 1, right);
	}
}

这里不再赘述插入排序,之前的文章有详细讲解。
链接 插入排序
4 快速排序非递归方法
采用递归实现快速排序存在一定的不足之处,原因是:递归调用,最深层函数如果没有退出,上层的一些递归调用是不会结束的。而每次递归都是一次函数调用,每个函数调用都需要在栈上给函数划分一个栈帧(一段栈空间),递归调用深度越深,划分的栈空间越多,而栈是有大小限制的,如果递归层次深到一定程度,栈空间不足时,就无法给该次递归函数划分栈空间导致递归调用失败,即栈溢出。
如何解决呢?将递归转换为循环。有些算法可以直接转,例如Fac求阶乘(因为该问题就是迭代器问题);有些算法不能转,可以借助stack实现,都是后调用的先退出,特性符合。
代码实现如下:

//用栈来将递归转换为循环
void QuickSortNor(int array[], int size)
{
	int left = 0;
	int right = size;
	Stack s;
	StackInit(&s);
	StackPush(&s, right);
	StackPush(&s, left);
	while (!StackEmpty(&s))
	{
		//先按照基准值来进行划分
		left = StackTop(&s);
		StackPop(&s);
		right = StackTop(&s);
		StackPop(&s);
		if (right - left > 1)
		{
			int div = Partion(array, left, right);
			//排基准值的左半侧--将右半部分的区间入栈[div+1,right)
			StackPush(&s, right);
			StackPush(&s, div + 1);
			//排基准值的右半侧--将左半部分的区间入栈[left,div)
			StackPush(&s, div);
			StackPush(&s, left);
		}
	}
	StackDestroy(&s);
}

基准值的选取和区间的划分方式和上边一样,可直接参考。这里不再赘述栈实现的代码,之前的文章有详细的讲解。
链接 栈的实现
5 特性

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(logN)
  4. 稳定性:不稳定
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值