[数据结构与算法] - 快速排序

快速排序:

基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

在这里插入图片描述
在这里插入图片描述

性能:

  1. 快速排序的时间复杂度:
    平均为O(N*logN)
    最坏为 O(N^2)

  2. 快速排序的稳定性:不稳定

优化:

  1. 三数取中法
    上面的代码思想都是直接拿序列的最后一个值作为枢轴,如果最后这个值刚好是整段序列最大或者最小的值,那么这次划分就是没意义的。
    所以当序列是正序或者逆序时,每次选到的枢轴都是没有起到划分的作用。快排的效率会极速退化。
    所以可以每次在选枢轴时,在序列的第一,中间,最后三个值里面选一个中间值出来作为枢轴,保证每次划分接近均等。

  2. 直接插入
    由于是递归程序,每一次递归都要开辟栈帧,当递归到序列里的值不是很多时,我们可以采用直接插入排序来完成,从而避免这些栈帧的消耗。

下面介绍一下三数取中的优化代码:

int GetMidIndex(int* a, int left, int right)
{
	//int mid = (left + right) / 2;
	int mid = left + ((right - left) >> 1); //防止溢出,并且效率也比除法要高
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])  //left mid right
		{
			return mid;
		}
		else if (a[left] > a[right]) // right left mid
		{
			return left;
		}
		else					//left right mid
		{
			return right;
		}
	}
	else //a[left] > a[mid]
	{
		if (a[mid] > a[right]) //left mid right
		{
			return mid;
		}
		else if (a[left] > a[right]) // mid left right
		{
			return left;
		}
		else   					// mid  right left
		{
			return right;
		}
	}
}

将区间按照基准值划分为左右两半部分的常见方式有:

  1. hoare版本
  2. 挖坑法
  3. 前后指针版本

动态演示:

在这里插入图片描述

方法:

版本1:hoare法

在这里插入图片描述
在这里插入图片描述

//单趟排序
int Partion1(int* a, int left, int right)
{
	//三数取中 -- 面对有序最坏情况:选中位数做key,变为最好情况
	int mini = GetMidIndex(a, left, right);
	Swap(&a[mini], &a[left]);

	int keyi = left;
	while (left < right)
	{
		//左边做key,右边先走,找小
		while (left < right && a[right] >= a[keyi])
		{
			--right;
		}

		//左边再走,找大
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}

		Swap(&a[left], &a[right]);
	}

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

版本2:挖坑法

在这里插入图片描述

//挖坑法
void Partion2(int* a, int left, int right)
{
	//三数取中 -- 面对有序最坏情况:选中位数做key,变为最好情况
	int mini = GetMidIndex(a, left, right);
	Swap(&a[mini], &a[left]);

	int key = a[left];
	int pivot = left;
	while (left < right)
	{
		//右边找小,放到左边的坑里面
		while (left < right && a[right] >= key)
		{
			--right;
		}
		a[pivot] = a[right];
		pivot = right;

		//左边找大,放到右边的坑里面
		while (left < right && a[left] >= key)
		{
			++left;
		}
		a[pivot] = a[left];
		pivot = left;
	}
	a[pivot] = key;
}

版本3:前后指针法

在这里插入图片描述

在这里插入图片描述

//前后指针
int Partion3(int* a, int left, int right)
{
	//三数取中 -- 面对有序最坏情况:选中位数做key,变为最好情况
	int mini = GetMidIndex(a, left, right);
	Swap(&a[mini], &a[left]);

	//选左边做key
	int keyi = left;
	int cur = left+1;
	int prev = left;
	
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[cur], &a[++prev]);
		}
		++cur;
	}
	Swap(&a[prev], &a[keyi]);
	return prev;
}

总代码:

递归版本:

//快速排序
//O(N*logN)
void QuickSort(int* a, int left,int right)
{
	if (left >= right)
	{
		return;
	}

	//小区间优化,当分割到小区间时,不再用递归分割思路让这段子区间有序
	//对于递归快排,减少递归的次数
	if (right - left + 1 < 10)
	{
		InsertSort(a + left, right - left + 1);
	}
	else
	{
		int keyi = Partion1(a, left, right);
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
}

非递归版本:

当递归程序的递归深度太深,容易造成stack overflow,因此只能考虑非递归。
当我们要用非递归时,要立刻想到栈,因为递归的原理符合栈的先进后出的规则,递归和栈在面试题和算法中总是联系在一起的

void QuickSortNonR(int* a, int left, int right)
{
	ST st;
	StackInit(&st);
	//将区间压入栈中
	//先入的left,后入的right
	//先出right,后出left
	//先处理右区间
	StackPush(&st, left);
	StackPush(&st, right);

	while (!StackEmpty(&st))
	{
		int end = StackTop(&st);//弹出right
		StackPop(&st);

		int begin = StackTop(&st);//弹出left
		StackPop(&st);

		int keyi = Partion3(a, begin, end);
		//[begin, keyi-1] keyi [keyi+1,end]

		//先压入右区间
		if (keyi + 1 < end)
		{
			StackPush(&st, keyi+1);
			StackPush(&st, end);
		}

		//再压入左区间
		if (begin < keyi - 1)
		{
			StackPush(&st, begin);
			StackPush(&st, keyi - 1);
		}
	}

	StackDestory(&st);
}

在这里插入图片描述

  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值