快速排序(霍尔法、挖坑法、双指针法、非递归法以及优化)

目录

前言:

一、霍尔法

霍尔法的思路:

霍尔法代码:

二、挖坑法

挖坑法的思路:

挖坑法代码:

三、双指针法

双指针法思路:

双指针法代码:

 四、非递归

非递归快排思想:

非递归代码:

优化:

三数取中:

代码:

小区间优化:

三区间划分法:

代码:


前言:

        今天也算是我🐏了之后快要康复的第一天吧,真是一天也不敢歇息写代码,不对,是我爱学习,我要进步。

        今天我给大家带来的是对快速排序的各种写法、讲解还有优化,其中包括了霍尔法、挖坑法、双指针法、非递归实现快速排序、还有防止占空间开太多的优化,取Key值不合理的情况,还有有大量重复数据出现的极端情况处理,内容有点多,请各位看官耐心看下去。


一、霍尔法

        霍尔法本身并没有什么含义,只是有一个叫做霍尔的大佬发明了快速排序这种方法,所以以他命名。

霍尔法的思路:

       霍尔排序首先需要注意的就是三个东西,左区间、右区间、Key值,只要我们控制好了这两个之间的对应关系,和转换关系,就能很轻易地理解快速排序。

        可能大伙有点疑惑,那么请先看下图。

         首先看到它的初始状态,我们默认将Key值定为最左边的哪一个值,然后有两个变量指向这一串数字的最左边和最右边,表示整个区间大小。

        这里我需要先提一下,如果将Key定义在右边,那么移动箭头的顺序就需要与我相反。

         因为Key值定义在左边,所以先移动右边的箭头,找到了比Key小的值停下。

         右边停下之后就开始移动左边的箭头,当找到了一个大于Key的值停下,此时还是有效区间,就可以交换两个箭头对应的值。

         然后再重复之前的移动箭头操作,先移动右边的箭头找到比Key小的停下,再移动左边的箭头找到比Key大的停下,然后交换。最终结果如下:

         最后在这一组数据当中右箭头会撞上左箭头,其他数据可能是左箭头撞上右箭头,不过这不是重点,重点是我们已经将一组数据比较完成了,只差最后一步,那就是将相遇位置值与Key位置的值交换得:

         以上,就是快速排序霍尔法单趟排序得完整思想,你们再看上面的那一组数据发现了什么?是不是比Key大的都在右边,比Key小的都在左边?而且最重要的是,Key也就是4到了正确的位置,接下来我们就可以划分区间,递归重复实现该操作了。

         整体来说霍尔法的完整排序就是单趟排序加上递归操作罢了,只要控制区间在不断缩小,且出现不存在区间时就不再继续递归就能控制好整段程序。至于为什么能行,还记得我们单趟排序实现了什么吗,那就是将Key回到了正确的位置,然后左边比Key小,右边比Key大,分别递归,不会影响现有的逻辑关系。

霍尔法代码:

void QuickSort(int* arr, int begin, int end)
{
	//当出现无效区间,或者只有一个值的情况,表示已经排序完成
	if (begin >= end)
	{
		return;
	}

	//定义左边下标和右边下标
	int Left = begin;
	int Right = end;

	//定义key值为左边第一个数据
	int key = arr[begin];

	//在左下标小于右下标时,继续比较
	while (Left < Right)
	{
		//再次比较两个下标防止越界风险,因为有一直找不到的风险
		//当右下标大于等于key值,会一直往后走,直到找到,才会停下来
		while (Left < Right && arr[Right] >= key)
		{
			Right--;
		}
		//左下标于key值比较,如果小于等于key,一直走,直到找到,停下来
		while (Left < Right && arr[Left] <= key)
		{
			Left++;
		}
		//当两个都停下来的时候,交换两个下标对应的值,让其继续走下去
		//有可能停下的条件是两个下标相等了,但是由于是同一个值,所以交换也没事
		Swap(&arr[Left], &arr[Right]);
	}
	//最后相交点与key交换,区分两个大小区间,并使key到达正确位置
	Swap(&arr[begin], &arr[Left]);

	//让后将上述操作细分为子问题,把左右两个无序区间再次按照该操作执行
	QuickSort(arr, begin, Left - 1);
	QuickSort(arr, Left + 1, end);
	
}

二、挖坑法

        挖坑法的思想其实与我们的霍尔法差不多,甚至就我而言,我认为挖坑法就是霍尔法的易理解版本。

挖坑法的思路:

     挖坑法和霍尔法相同,也有左区间、右区间、Key值,不过它多了一个东西,那就是坑,请看下图:

         我们把初始位置的值保留给了Key值,那么原来那个位置我们就可以当作没有数据了,把他看作为一个坑。

        此时也是先移动右箭头,找到了比Key小的值,把该值放入坑当中,这个时候右箭头所指的位置就是一个新的坑,然后再移动左箭头,找到了比Key大的值,又将该值放入新坑当中,然后又会产生一个坑在左箭头下面,如此反复,不断地挖坑,填坑,其实细细品味就会觉得它就是将霍尔法的交换分为两步走完。

        最后在相遇的时候也会剩下一个坑,这个坑就是留给我们的Key值的。然后就完成了我们的单趟排序,它的完成排序和霍尔排序一样,完全没变,就是在单趟排序完成之后递归两个区间。

挖坑法代码:

void PitQuickSort(int* arr, int begin, int end)
{
	//划分到最小区间
	if (begin >= end)
	{
		return;
	}

	//指向前方,后方,key值
	int front = begin;
	int rear = end;
	int mid = Get_Medium_Num(arr, front, rear);
	Swap(&arr[front], &arr[mid]);
	int key = arr[begin];

	//还没相遇,一直走
	while (front < rear)
	{
		//大于key,往前走
		while (front<rear && arr[rear]>=key)
		{
			rear--;
		}
		//与坑交换
		arr[front] = arr[rear];

		//小于key,向后走
		while (front < rear && arr[front] <= key)
		{
			front++;
		}
		//与坑交换
		arr[rear] = arr[front];
	}
	//最后将key值放入坑中,此时坑就是它的正确位置
	arr[front] = key;

	//进入下一次递归啦
	PitQuickSort(arr, begin, front - 1);
	PitQuickSort(arr, front + 1, end);

}

三、双指针法

        双指针发不同于前面两种方式,它是通过将大的往后面推,将小的往前丢的方式进行排序,当有一个指针指向结束,该次单趟结束。

双指针法思路:

         首先,需要定义两个指针变量,指向第一个数据和第二个数据,不用担心有没有可能不存在两个数据,然后定义最左边为Key值。

        当前箭头大于Key值不做变动,直接指向下一个值。

        当前箭头找到了比Key小的值,后箭头先往前移动一位,然后与前箭头交换得:

        交换完成之后,前箭头因为逻辑会自己往前走一步,然后与下一次循环对应得:

         交换两个箭头的值。

         交换之后前箭头会自己向前移动,然后进入下一次循环,找到了比Key大的,不移动,前箭头再往前走一步,越界退出。

         然后再把后箭头的值与Key值位置交换得:

         此时,单趟排序结束,左右区间划分完毕,Key值回到正确位置,可以进行递归了。

双指针法代码:

//双指针法
void PointQuickSort(int* arr, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	int front = begin+1;
	int rear = begin;
	int key = begin;

	while (front <= end)
	{
		//同一句话执行也有先后逻辑之分,如果前面已经错误,那么后面的就不会执行
		//在&&的条件下
		if (arr[front] < arr[key] && ++rear != front)
		{
			Swap(&arr[front], &arr[rear]);
		}
		front++;
	}
	Swap(&arr[rear], &arr[key]);

	PointQuickSort(arr, begin, rear - 1);
	PointQuickSort(arr, rear + 1, end);
}

 四、非递归

非递归快排思想:

        我们的非递归实现快排需要用到另外一种数据结构栈,用来代替我们的递归过程。

        注意,我们这里压入栈的数据是区间,也就是每一个区间需要压入两个边界,取值时也需要注意,不能取反了。具体思想就是,每一个旧区间被出栈,就有两个新区间被压入,当区间不存在,就跳过本次循环。

        同时需要注意压栈的顺序为后区间先压,前区间后压,虽然更换顺序不会影响实际的排序,但是不过不更换,就逻辑上而言它变得不再合理。

非递归代码:

        非递归需要添加栈的源代码,有需求去找我的博客。

//非递归实现快排
void NoneRecursionQuickSort(int* arr, int begin, int end)
{
	Stack ps;
	StackInit(&ps);
	StackPush(&ps, begin);
	StackPush(&ps, end);

	while (!StackEmpty(&ps))
	{
		int right = StackTop(&ps);
		StackPop(&ps);
		int left = StackTop(&ps);
		StackPop(&ps);
		int key = left;
		if (left >= right)
		{
			continue;
		}
		int front = left + 1;
		int rear = left;
		while (front <= end)
		{
			if (arr[front] < arr[key] && ++rear != front)
			{
				Swap(&arr[front], &arr[rear]);
			}
			front++;
		}
		Swap(&arr[rear], &arr[key]);
		key = rear;

		StackPush(&ps, key + 1);
		StackPush(&ps, end);
		StackPush(&ps, left);
		StackPush(&ps, key - 1);
	}
}

优化:

三数取中:

        我们可以看到,每一次我们的Key值都被默认选择为了最左边的哪一个,但是这么做就会出现一个问题,那就是,这个数比所有数都小,或都大怎么办,这样划分的区间会让我们的快排重新回到O(n^2)的效率的,所以这个时候就需要对数据进行Key值进行整改,我们将最左边,最右边,和中间三个值进行比较,谁是中间值就与我们的最左边的值更改,此时的最左边还是Key,但是Key值已经变得合理。

代码:

        在选Key之前,调用此函数。

int Get_Medium_Num(int* arr, int left, int right)
{
	int mid = left+rand()%(right-left);
	if (arr[left] > arr[right])
	{
		if (arr[left] < arr[mid])
		{
			return left;
		}
		else
		{
			if (arr[right]>arr[mid])
			{
				return right;
			}
			else
			{
				return mid;
			}
		}
	}

	else
	{
		if (arr[right] < arr[mid])
		{
			return right;
		}
		else
		{
			if (arr[mid]>arr[left])
			{
				return mid;
			}
			else
			{
				return left;
			}
		}
	}
}

小区间优化:

        小区间优化是为了防止整个排序过程当中,栈空间开的太多从而导致程序崩溃。具体优化过程就是递归结束条件需要更改,我们知道,在最后一层两层其实没有必要再递归下去了,所以结束条件就是区间小于5或者其它数都行,合理即可。然后再用我们的其它排序手段为其完成最后的排序。代码我就不列写了,灵活性很高。

三区间划分法:

        三区间划分法,用于防止一串数据当中,全是相同数据,或者大量的重复数据。它的事项其实和我们的双指针法有一点相识,但是这一次,我们将单次比较完成之后的数据划分成为了三个区间,小于Key,等于Key,大于Key。

        单趟实现方式就是有三个指针,分别指向头,尾,第二个数据,我们定义为front,rear,cur,当cur比rear小的时候,整个操作继续,当cur对应的值比Key小交换cur和front的值,front++,cur++,当比Key大时,交换rear和cur的值,rear--,cur之所以不++了是因为与rear交换之后不能确定此时cur所占位置的值的大小,当cur等于Key时,cur++。整个操作就实现了将大的往右边丢,小的往左边丢,等于的排到中间。

代码:

//三区间划分法
void Quick_Sort_Three(int* arr, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	int mid = Get_Medium_Num(arr, begin, end);
	Swap(&arr[begin], &arr[mid]);
	int key = arr[begin];

	int front = begin;
	int rear = end;
	int cur = front + 1;
	while (cur <= rear)
	{
		if (cur <= rear && arr[cur] < key)
		{
			Swap(&arr[cur], &arr[front]);
			front++;
			cur++;
		}
		if (cur <= rear && arr[cur] > key)
		{
			Swap(&arr[cur], &arr[rear]);
			rear--;
		}

		if (cur <= rear && arr[cur] == key)
		{
			cur++;
		}
	}

	Quick_Sort_Three(arr, begin, front - 1);
	Quick_Sort_Three(arr, rear+1, end);
}

        以上就是我对快排的全部理解,能帮到你最好,谢谢。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值