一定不能错过的快排呦(两种交换排序实现)

目录

写在前面的话

一,冒泡排序

1.1思路实现

1.2思路优化

1.3代码实现

二,快速排序

2.1快排的第一种实现

2.1.1递归函数主要思路实现

2.1.2第一种方法代码实现

2.2快排的第二种实现

2.2.1思路实现

 2.2.2代码实现

2.3快排的第三种实现

2.3.1思路实现(第一种)

2.3.2思路实现(第二种)

2.3.3思路优化

2.3.4源码实现 

2.4快排主函数实现

2.4.1递归主函数

2.4.2递归主函数的优化

2.5快排主函数非递归实现


写在前面的话

小伙伴们大家好啊!小菜鸡森哩又来了,今天依旧更新有关排序的内容。那么今天首先是冒泡排序,然后是快速排序的三种实现以及快排的非递归实现。           

                  

一,冒泡排序

1.1思路实现

冒泡排序,顾明思议就是找出最大或者最小的数,然后将其与最后一个或者第一个元素交换,然后在不包括这个元素的一组数中,再次找出次大值或者次小值,然后再将其与倒数第二个或者第二个元素交换。

然后一直重复,直到需要排序的数只剩一个,这样最后得到的便是一个有序的序列了。

因为冒泡排序思路是比较简单的,所以其实现是比较冗余的,故其时间复杂度是很高的为O(N^2),所以一般我们在排序的时候,是不会用它去实现排序的。

1.2思路优化

当然,如果我们一定要用它来排序的话,那么一些小小的优化我们还是需要做的,比如在某一次排序之后我们发现没有元素移动,此时说明当前序列已经有序了,此时我们就不需要再排序了,直接结束即可!

那么对于这种情况来说,我们使用一个标杆来记录,如果有排序,就改变标杆的值,那么每次一趟排序之后,如果我们监测到标杆的值没有发变化,则直接退出循环即可。

1.3代码实现

//冒泡排序
void bubbleSort(int* a, int n)
{
	//flag的意义是 当数组进行到中途的时候,发现数组已经有序了,那么我们就不需要再进行下一次循环了
	int flag = 0;
	//多层循环嵌套,每次将最后一个元素不包括在范围内
	for (int j = n; j > 0; j--)
	{
		//一次循环,从 0 位置开始到 j-1 位置结束。
		for (int i = 0; i < j - 1; i++)
		{
			if (a[i] > a[i + 1])
			{
				swap(&a[i], &a[i + 1]);
				flag = 1;
			}
		}
		if (flag == 0)
			break;
	}
}

二,快速排序

那么因为快速排序对于其他排序来说是比较有优势的,所以这里我们对于快速排序的几种实现都了解一下。

2.1快排的第一种实现

2.1.1递归函数主要思路实现

那么首先我们是最初始的快排思路,如下图所示,首先我们需要两个下标指针 begin 和 end ,以及一个对比元素 key。

第一步,首先让 end 指针从最后一个元素开始往左走,第一次找出一个比 key 值小的元素的时候,就停下;然后 begin 再从第一个元素开始找,第一次找出比 key 值大的元素就停下。

第二步,将现在 begin 和 end 指向的两个元素交换。重新执行第一步。

然后重复上面两个步骤,直到两个指针指向同一个位置,此时我们再将该位置元素元素和 key 元素交换,同时将 key 的位置移动到当前它移动之后的位置。就得到了下面这个结果。 

如下图所示:

那么经过上面这次排序之后,对于原数组的变化,可以看到,在 key 值左边的元素都是小于等于改值的,而对于 key 值右边的元素,则是全部大于 key 的。接下来我们将 key 值下标返回,然后递归调用 key 之前的一部分,和 key 之后的另一部分,直到递归主函数的 begin 大于等于  end 的时候,此时整个排序结束。

tips:

1.对于递归主函数我们在实现了三种思路之后,总体实现,因为三种实现都需要递归。

2.如果是左 key 的话,首先得从右边找小值;而如果是右 key 的话,则需要首先先从左边找大,这是一个规律,而且必须这样做,至于为什么这样实现,大家可以自行理解哦!我们在最后回答! 

2.1.2第一种方法代码实现

int Partion1(int *a,int begin,int end)
{
	//一层循环
	int mini = GetMidIndex(a,begin,end);
	swap(&a[mini], &a[begin]);
	int key = begin;
	while (begin < end)
	{
		//左key,先走右边
		//快排里面的两个条件,一个都不能缺,而且一个是将等于它自己的数不算做不符合条件的数,继续往下走了。
		while (begin<end && a[end] >= a[key])
		{
			end--;
		}
		//再走左边
		while (begin<end && a[begin] <= a[key])
		{
			begin++;
		}
		swap(&a[begin],&a[end]);
	}
	swap(&a[end],&a[key]);
	return end;
}

那么对于当前代码而言,最开始的几行代码是选出了一个 key 值,那么为什么那样去选,因为三种方式都需要这样去选,所以我们在后面统一处理。 

2.2快排的第二种实现

第二种思路我们将其简称为挖坑法。

2.2.1思路实现

如下图所示,对于指针以及 key 值的设定,以及那个指针先移动,都是和第一种方法一样的。

那么接下来我们首先将 key 值记录下来,然后将该位置作为 “坑位”。接着同样的,首先移动 end 指针找大于 key 值的数,第一次找到了之后,将该数填入到 “坑位”,然后它自己的位置变成新   “坑位”;

接着 begin 指针开始移动找小于 key 值的第一个元素,找到了则将其填入刚才的“坑位”,然后自己又变成了新的“坑位”。

那么我们重复进行刚才的两个步骤,直到两个指针指向同一个地方,此时表示本趟排序结束。

然后就是同样的,通过递归主函数将这组数分为两部分,然后对这两部分分别进行排序。然后就是一直递归,直到排完序。 

 那么如果有小伙伴对于这部分具体的细节还不是很了解的话,可以通过下面的图再梳理哦!

 2.2.2代码实现

//挖坑法
int Partion2(int* a, int begin, int end)
{
	int ret = GetMidIndex(a, begin, end);
	swap(&a[ret], &a[begin]);
	int key = a[begin];
	int pivot = begin;

	while (begin < end)
	{
		//先走右边
		if (begin < end && a[end] >= key)
		{
			end--;
		}
		a[pivot] = a[end];
		pivot = end;

		//再走左边
		if (begin < end && a[begin] <= key)
		{
			begin++;
		}
		a[pivot] = a[begin];
		pivot = begin;
	}
	//最后一个坑位补满
	a[pivot] = key;
	return pivot;
}

2.3快排的第三种实现

第三种我们将其称为前后指针法,相对于前两种实现,前后指针法是比较简洁的。

那么首先如下图所示:

对于上面两种思路实现,其实都是差不多的,接下来我们首先以第一种为例。

2.3.1思路实现(第一种)

如上图所示,首先我们需要将 cur 移动找小于 key 值的元素,第一次在 “ 2 ” 的位置找到了。所以接下来我们再让 prve 自增一位,然后将 prev 位置的元素和 cur 位置元素交换。这就是一趟排序中的一次循环。

接着我们进行同样的操作,直到如上图所示 cur 超过数组范围表明本趟排序结束,然后就得到一个新序列,同样的和上面两种操作是一样的,我们将其分为两部分,再分别进行排序,直到所有元素排完。

2.3.2思路实现(第二种)

那么上面图所示的是,第一种情况下的,排序的思路。同样的,选择第二种的右 key 实现也是一样的,只不过此时 cur 的找的是小于 key 的数,然后接下来的步骤都是一样的,交换 cur 的值和 prev 的值,最后再将 key 值和 prev 的值交换。 

这里我们不在赘述。

2.3.3思路优化

那么当然了我们在排序中有可能出现以下问题,cur  从移动的时候开始第一个数就是满足条件的数,那么如果指针 prev 移动一位,则直接指向了 cur 的位置,这样的交换是没有必要的,所以如果是这样的情况,那么我们可以直接跳过。

2.3.4源码实现 

这里我们实现的是第一种思路的代码,但是两种思路都是差不多的。

//前后指针法
int Partion3(int *a,int begin,int end)
{
	int ret = GetMidIndex(a, begin, end);
	swap(&a[ret], &a[begin]);
	int prev = begin;
	int key = begin;
	int cur = prev + 1;
	while (cur <= end)
	{
		//if语句中也可以做自增,第一次遇到
		if (a[cur] < a[key] && ++prev!=cur)
		{
			swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	swap(&a[key], &a[prev]);
	return prev;
}

2.4快排主函数实现

那么对于快排的三种思路我们在上面都做了介绍,但是上面介绍的都是一趟排序,那么对于主函数而言,这里我们有两种思路实现。

2.4.1递归主函数

那么我们知道,如果用递归去实现的话,思路是比较简单的。对于上面每一种思路,因为每次一趟排序结束之后,都会返回 key 值的下标,此时我们就可以通过 key 的下标得到两边的两个小序列,然后将其再进行排序,最后递归结束的条件是当 begin 的值大于 end 的时候。

代码实现:

void QuickSort(int *a, int begin,int end)
{
	if (begin >= end)
		return;
	//小区间优化
	//当只有十个数的时候,使用直接插入更加高效
	if (end - begin + 1 < 10)
	{
		InsertSort(a+begin,end-begin+1);
	}
	else
	{
		int ret = Partion3(a, begin, end);
		QuickSort(a, begin, ret - 1);
		QuickSort(a, ret + 1, end);
	}
}

2.4.2递归主函数的优化

那么对于快速排序而言,因为追求的是比较高的效率,所以这里当 begin 和 end 之间的数为十个以内的时候,因为对于小范围的数直接插入排序的效率是相对比较高的,所以我们选择直接插入排序来进行最后小范围的排序。

2.5快排主函数非递归实现

那么对于非递归而言,其实和递归思路是差不多的,只是这里我们用栈的先进后出的特性去实现。

首先就是我们将 begin 和 end 这两个数依次插入栈中,然后将其出栈,送入排序函数去排序,接着会返回一个 key 值,然后我们只需要和递归一样,将该数组分为两半,然后分别将两个小数组的begin 和 end 入栈,然后重复进行上面的步骤,直到栈为空,此时表明所有数都排完序了。 

代码实现:

//类似于递归,只不过是通过栈的先进后出
//每一次将begin和end的值入栈,然后将放入一趟排序函数排序
void QuickNonr(int* a, int begin, int end)
{
	ST st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);
	while (!StackEmpty(&st))
	{
		int end = StackTop(&st);
		StackPop(&st);
		int begin = StackTop(&st);
		StackPop(&st);

		int key = Partion3(a,begin,end);
		if (key + 1 < end)
		{
			StackPush(&st, key+1);
			StackPush(&st, end);
		}

		if (begin < key)
		{
			StackPush(&st, begin);
			StackPush(&st, key-1);
		}
	}
}

好的,那么对于两种交换排序就结束啦!如有问题,还请指正呀!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值