7大排序的亿点细节(详解快排)

目录

重要思路:

原版快排: 

原版快排常见问题和疑惑:

原版一趟排序的代码:

原版一趟排序的易错提醒:

比较好理解的快排算法,挖坑法:

容易控制的快排算法,前后指针法:

特别注意:前后指针法右边取keyi的情况 

未有化快排的缺点和优化 

提升最大的优化

 小区间优化


重要思路:

要在每一次排序后,一个数到达正确的位子,而且左边的数都小于他,右边的数都大于他。

由原版快排的动画引入:

原版快排: 

原版快排常见问题和疑惑:

1.会不会奇数个就遇不到了?

不会,因为是L或者R先走,另一个再走。

2.会不会相遇到一个比key大的数呢?

这里要注意key在左边则右边先走,可以保证相遇在比key小的位置;key在右边则左边先走,就可以保证相遇在比key大的位置。这是与key选在左边还是在右边有关的。

相遇有两种情况,左遇右;右遇左。

左遇右:左边去碰右边,左边走的时候右边一定停在了比key大的位置,如果相遇就是在比key大的位置。

右遇左:如果左边停在了一个比key大的位置那么下一步是交换L和R位置的数。交换完后L的数是小于key的。然后R再走,如果这时候相遇也是在比key小的位置。

原版一趟排序的代码:

int PartSort(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		//找小
		while (left < right && a[right] >= a[keyi])
			right--;
		//找大
		while (left < right && a[left] <= a[keyi])
			left++;
		Sweap(&a[left], &a[right]);
	}
	Sweap(&a[keyi], &a[right]);
	return left;
}

原版一趟排序的易错提醒:

int Err_PartSort(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		//找小
		while ( a[right] > a[keyi])
			right--;
		//找大
		while (a[left] < a[keyi])
			left++;
		Sweap(&a[left], &a[right]);
	}
	Sweap(&a[keyi], &a[right]);
	return left;
}

这样的代码有两种情况不能适配:

1.

left和right的数都等于keyi的数,这样就会陷入死循环。我们只是要求大的在右边,小的在左边,相等的在哪一边都可以的。

然后就有人会改成下面的代码:

int Err_PartSort(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		//找小
		while ( a[right] >= a[keyi])
			right--;
		//找大
		while (a[left] <= a[keyi])
			left++;
		Sweap(&a[left], &a[right]);
	}
	Sweap(&a[keyi], &a[right]);
	return left;
}

同样还会有问题:

 有可能发生越界访问。

再来看整体的排序:

经历了一次排序之后,key已经到了正确的位置,这个时候就分成了两部分[left,key-1]和[key+1,right],而key的左边全是小于他的,右边全是大于他的。

如何让左边和右边有序呢?分治递归。

//快速排序
void QuickSort(int* a, int begin, int end)//这个时候就不要传元素的个数了,传左右边界(闭区间)
{
	//结束条件:子区间相等或只有一个值就是结束,return
	if (begin >= end)
		return;

	int key = PartSort(a, begin, end);
	//左边有序
	QuickSort(a, 0, key - 1);
	//右边有序
	QuickSort(a, key + 1, end);
}

比较好理解的快排算法,挖坑法:

//挖坑法
int PartSort1(int* a, int left, int right)
{
	int key = a[left];
	//坑位
	int pit = left;
	while (left < right)
	{
		//右边先走,找小
		while (left < right && a[right] >= key)
			right--;
		//把坑用a[right]填上,right成了新的坑   
		//坑的意思并不是这个位置没有数了而是这个位置可以被别的数覆盖了
		a[pit] = a[right];
		pit = right;
		//左边再走,找大
		while (left < right && a[left] <= key)
			left++;
		//把坑用a[left]填上,left成了新的坑
		a[pit] = a[left];
		pit = left;
	}
	//把坑用key填上
	a[pit] = key;
	return pit;
}
void QuickSort1(int* a, int begin,int end)
{
	if (begin >= end)
		return;
	int keyi = PartSort1(a, begin, end);
	QuickSort1(a, begin, keyi - 1);
	QuickSort1(a, keyi + 1, end);
}

挖坑法更便于理解,很好的避开了为什么left和right相遇的位置一定比(keyi选在左边)keyi小等让自学者困惑的问题。但是在实际的运行速度上和原版并没有多大不同。

容易控制的快排算法,前后指针法:

下面在学习一种跟容易掌控思想也更常用的算法。这个算法并没有公认的名字,这里为了便于介绍就用了前后指针法。

//前后指针法(keyi选在左边)
int  PartSort2(int* a, int left, int right)
{
	int midi = MidSearch(a, left, right);
	Sweap(&a[left], &a[midi]);
	int keyi = left;
	int prev = left, cur = left + 1;
	//cur<=right right是有效的下标
	while (cur <= right)
	{
		//找小的交换到prev++的位置
		//很多人对于 a[++prev] != a[cur]这一步不是很了解
		if (a[cur] < a[keyi] && a[++prev] != a[cur])
			Sweap(&a[cur], &a[prev]);
		//我们把它翻译一下:
		// 遇到小于a[keyi]的数 prev跟进,然后a[cur]和a[prev]交换
		//if (a[cur] < a[keyi])
		//{
		//	prev++;
		//	Sweap(&a[prev], &a[cur]);
		//}

		//无论a[cur]怎样都要走到下一个位置
		cur++;
	}
	Sweap(&a[prev], &a[keyi]);
	return prev;
}

特别注意:前后指针法右边取keyi的情况 

然后还要重点说一下这里keyi取到右边的情况,这里有点不同 ,主要是在一趟排序的地方:

int PartSortRight(int* a, int left, int right)
{
	int keyi = right;
	int prev = left - 1, cur = left;
	//这里是不能到最右边的 ; cur<right + prev = left - 1,cur = left这样才能调节到所有除了keyi的元素
	while (cur < right)
	{

		if (a[cur] < a[keyi] && a[++prev] != a[cur])
			Sweap(&a[cur], &a[prev]);
		cur++;
	}
	Sweap(&a[++prev], &a[keyi]);
	return prev;
}

未有化快排的缺点和优化 

到这里快排的几种实现方法就已经介绍完了,但是只是这样快排还是不快。我们可以看出快排之所以快就是因为他的单趟排序不仅拍好了其中一个元素的而且还分好了比那个元素大和比那个元素小的区间。而且这两个区间大多数情况下是存在且包含的元素个数不唯一。请大家想象升序和降序这两种有序的情况。

快了个寂寞 ..

提升最大的优化

所以呢就有人做出了一个重要的优化:

为了避免每次选左或者选右都是最大或者最小的情况,我们从a[left],a[right],a[mid]{mid = (left+right)/2}三个数中选出中间的数做keyi具体代码如下:

int MidSearch(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[right])
	{
		if (a[mid] > a[right])
			return right;
		else if (a[left] > a[mid])
			return left;
		else
			return mid;
	}
	else//a[left]>a[right]
	{
		if (a[mid] > a[left])
			return left;
		else if (a[right] > a[mid])
			return mid;
		else
			return right;
	}
}
int  PartSort2(int* a, int left, int right)
{
	int midi = MidSearch(a, left, right);
	//这里为了方便直接让这个不大不小的数和a[left]交换
	Sweap(&a[left], &a[midi]);

	int keyi = left;
	int prev = left, cur = left + 1;

	//cur<=right right是有效的下标
	while (cur <= right)
	{
		//找小的交换到prev++的位置
		//很多人对于 a[++prev] != a[cur]这一步不是很了解
		if (a[cur] < a[keyi] && a[++prev] != a[cur])
			Sweap(&a[cur], &a[prev]);
		//我们把它翻译一下:
		// 遇到小于a[keyi]的数 prev跟进,然后a[cur]和a[prev]交换
		//if (a[cur] < a[keyi])
		//{
		//	prev++;
		//	Sweap(&a[prev], &a[cur]);
		//}


		//无论a[cur]怎样都要走到下一个位置
		cur++;
	}
	Sweap(&a[prev], &a[keyi]);
	return prev;
}

 小区间优化

 但是在我们的库里还做了一个优化,叫小区间优化,这个优化的效果不如上一个明显。

 标红的区间要么不存在要么区间中只有一个数据,但还是进行了递归操作,增加了运行时间。聪明的程序员们发现这些元素个数较少的区间用其他的排序更快,所以就有了下面的代码:

void QuickSort2(int* a, int begin, int end)
{
	// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
	if (begin >= end)
		return;

	// 小区间直接插入排序控制有序
	if (end - begin + 1  <= 30)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int keyi = PartSort3(a, begin, end);
		// [begin, keyi-1]keyi[keyi+1, end]
		QuickSort2(a, begin, keyi - 1);
		QuickSort2(a, keyi + 1, end);
	}
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值