【排序-疑难杂谈】详细聊聊快速排序的细节

目录

本文将对快速排序进行详细的解读,耐心看完,你一定会有所收获

基本步骤:

一、先从待排序的数列中选出一个数;

二、分区处理,将比这个数大的数放到这个数右边,将比这个数小的数放到左边;

1.霍尔法

问题一:为什么能保证相遇位置一定小于等于key?

问题二:为什么左边是key,右边就要先走?

2.挖坑法

三、继续分区,直到区间缩小到只有一个数;

递归法:

非递归法:

时间复杂度:

优化:为了防止数据比较极端的情况,我们要采取两种方式对快速排序优化

1.三数取中:

2.随机取key:


基本步骤:

一、先从待排序的数列中选出一个数;

二、分区处理,将比这个数大的数放到这个数右边,将比这个数小的数放到左边;

三、继续分区,直到区间缩小到只有一个数;

一、先从待排序的数列中选出一个数;

这里先默认以最左边的数当作key,在分析复杂度的时候,我们会再次分析如何选数

二、分区处理,将比这个数大的数放到这个数右边,将比这个数小的数放到左边;

如何才能做到让选出key左边全是比key小的数,而右边全是比key大的数呢?即如何达到下图效果呢?

1.霍尔法

这里我们假设已经通过三数取中或随机选key,将最左边的数字换成了key,即key=7

那我们如何做到七的左边全是小于七的数字,七的右边全是大于七的数字呢

在这里我们定义头尾两个变量:

right向左边寻找,它的目的是找到比key要小的数字,如果找到就停止寻找;

left向右边寻找,它的目的是找到比key大的数字,如果找到就停止寻找;

在上图的例子中,right一开始的值就是1,1显然比key要小,因此right停止寻找

                          left向右寻找,直到找到9,9显然比key要大,因此left也停止寻找

到现在为止,left和right的位置如下图:

我们交换left和right指向的两个数字,这样比key大的9就去了right的后面,比key小的1,就来到了left左边,如图:

 

重复上述过程,直到left和right相遇,如图:

 此时我们让相遇位置和key交换,得到以下结果:

这样我们就做到了上面的要求.

问题一:为什么能保证相遇位置一定小于等于key?

首先我们要知道是如何相遇的,无非就两种情况:

第一种情况:左边没动,右边一直向左寻找,直到相遇

        右边找比key小的值,一直向左走,说明没有找到比key小的值就遇到了左边,停了下来

        左边要么是原来的key,要么曾经已经找到过一次比key大的值,并且完成了交换,所以左边一定小于等于key,此时相遇一定小于等于key

第二种情况:右边没动,左边一直向右寻找,直到相遇

        左边找比key大的,一直没有找到,而左边能开始找的前提是右边已经停下来了,也就是说右边已经找到了比key要小的值,此时相遇,相遇位置一定小于等于key

问题二:为什么左边是key,右边就要先走?

如图,如果左边先走,到这一步和右边先走是一样的,左边区域除了第一个key,其他数字已经全都小于等于了key,右边数字全都大于等于了key,按照要求,我们想让key左边都是小于等于key的数,所以,我们要把让left和key交换,这样我们就能明白为什么左边是key就要让右边先走了 

代码:

int PartSort1(int* a, int begin, int end)
{
	//霍尔大佬的方法
	int mid = getmidindex(a, begin, end);
	Swap(&a[begin], &a[mid]);

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

	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[left], &a[keyi]);
	keyi = left;

	return keyi;
}

需要注意:为了方便后续递归,我们返回的是key最终应在的位置

2.挖坑法

挖坑法比起霍尔的方法,更加直观

挖坑法的本质就是将left的值存起来,形成一个坑位,再像霍尔法一样将小的值填到左坑位,这样右边就是新的坑位了,再从左边找大的值填到右边

 最终left和right会在坑位相遇,再将一开始的key填入这个坑位即可

代码:

int partSort2(int* a, int begin, int end)
{
	//挖坑法
	int mid = getmidindex(a,begin,end);
	Swap(&a[begin],&a[mid]);

	int left = begin;
	int right = end;
	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;
}

三、继续分区,直到区间缩小到只有一个数;

完成一次排序后,我们要考虑如何多次排序以完成排序的功能

递归法:

在分好一个数字后,我们的数组被分成三部分:

我们需要将key左边进行相同的操作,将key右边也进行相同的操作

直到区间只剩下一个数字,因此递归条件是:区间存在

前面我们用霍尔法或挖坑法获得了keyi,这样我们将(begin,keyi-1)传入函数再进行一次排序

将(keyi+1,end)也传入函数,继续一次排序,这样递归进行下去,就实现了排序

代码:

void QuickSort(int* a, int begin, int end)
{
	//递归
	if (begin >= end)
	{
		return;
	}

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

非递归法:

递归算法虽然书写简单,但由于每次都要调用函数栈帧,若递归层数太多,性能会大幅度降低,因此我们要来书写非递归算法、

要将递归改造为非递归,我们首先要搞清楚,原来的递归算法是依靠左右两个边界来界定要排序的范围的,现在我们依然需要这两个边界,因此我们选择借用栈来保存边界数据,若对栈的知识仍有疑惑,请参考我的这篇文章,里面对有较详细的讲解:http://t.csdn.cn/Qdm74

void QucikSortNONR(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);

		int keyi = PartSort1(a, left, right);
		//成功划分出区间 begin~keyi-1,keyi,keyi+1~end
		if (left < keyi-1)
		{
			StackPush(&st, left);
			StackPush(&st, keyi - 1);
		}

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

时间复杂度:

关于快速排序的时间复杂度,在比较好的情况下,快速排序的递归像是一棵二叉树,时间复杂度约为O(n*logn),但在较差的情况下,树向一段严重倾斜,这样第一次需要遍历n此次,第二次遍历n-1次......依此类推时间复杂度约为O(n^2)

优化:为了防止数据比较极端的情况,我们要采取两种方式对快速排序优化

1.三数取中:

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

2.随机取key:

在三数取中的逻辑中,我们也没有毕业一定选择中间的值,可以进行随机优化

int getmidindex(int* a,int begin,int end)
{
	//int mid = (begin + end) / 2;
	int mid = begin + rand() % (end - begin);
	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])
		{
			return mid;
		}
		else if (a[end] < a[begin])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
	else // a[mid] < a[begin] 
	{
		if (a[begin] < a[end])
		{
			return begin;
		}
		else if (a[end] < a[mid])
		{
			return mid;
		}
		else
		{
			return end;
		}
	}
}

有关快速排序的实现和细节以及时间复杂度就跟大家分享完啦,认真看完的你一定有所收获,如果对你有所帮助,请给博主点赞收藏关注,这是我分享知识的最大动力,我们下次再见!

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蓝色学者i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值