排序【选择排序和快速排序】

1.选择排序

1.1基本思想

每次选出最小(或最大)的一个元素,存放在数组的起始位置,直到所有元素都排完。

1.2直接插入排序:

  • 在数组arr[i]到arr[n-1]中选出最大(小)的元素。
  • 若该元素不是数组的最好一个(第一个)元素,那么就与数组中的最后一个(第一个)元素进行交换。
  • 然后在剩余的arr[i]到arr[n-2]或者arr[i+1]到arr[n-1]的数组中重复上述步骤,直到只剩下一个元素。

注意:本文讲解的是已经优化过一遍的版本(一次选出最大和最小)。
我们先设立好区间,使用begin作为遍历的起点,end为终点;由于begin和end是区间,所以我们在遍历的时候不能改变他们的值,所以我们再建立两个变量(mini和maxi),用来存放最大和最小元素的下标。

void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;

	int mini = begin;
	int maxi = begin;
	
		//找最大和最小
	for (int i = mini; i < end; i++)
	{
		if (a[mini] > a[i])
		{
			maxi = i;
		}
		if (a[maxi] < a[i])
		{
			maxi = i;
		}
	}
	
}

然后再进行交换,最小的元素和第一个元素交换,最大的元素和最后一个元素交换。

但是,有一个特殊情况:当首元素(下标为begin的元素)是最大的元素的时候。

这时候我如果先交换最小的,那么我的首元素就不是最大的元素了,而是最小的元素,但是我的maxi存放的是下标,那么我在交换maxi和最后元素的时候,我就会把最小的元素交换到最后去。
在这里插入图片描述

解决方法:由于我先交换了最小的元素,所以这时最大的元素就在mini下标处,那我在进行最大元素的交换前,先判断maxi是否等于begin,如果等于,我就将mini赋给maxi(由于mini和maxi存放的都是下标,所以在交换前并不会影响数组的原本顺序

		//交换
		Swap(&a[mini], &a[begin]);
		if (maxi == begin)
		{
			maxi = mini;
		}
		Swap(&a[maxi], &a[end]);

最后从单次排序转换成多次排序(改变区间的值)

void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int mini = begin;
		int maxi = begin;

		//找最大和最小
		for (int i = mini; i < end; i++)
		{
			if (a[mini] > a[i])
			{
				maxi = i;
			}
			if (a[maxi] < a[i])
			{
				maxi = i;
			}
		}

		//交换
		Swap(&a[mini], &a[begin]);
		if (maxi == begin)
		{
			maxi = mini;
		}
		Swap(&a[maxi], &a[end]);

		//调整
		begin++;
		end--;
	}
}

1.3直接插入排序的总结:

尽管是优化了,但效率依旧不高。

  1. 直接插入的排序很好理解,但是效率不高,基本没用使用的价值
  2. 时间复杂度: O ( N 2 ) O(N^2) O(N2)
  3. 空间复杂度: O ( 1 ) O(1) O(1)
  4. 稳定性:不稳定

堆排序也是选择排序的一种,但是效率很高,由于堆排序在前文已经讲解过了,在本文就不讲解了(链接:堆排序)。

2.快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序

2.1底层思想(交换排序)

所谓交换,就算根据序列中两个记录键值的比较结果来交换这两个记录在序列中的位置。
交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

2.2快速排序的基本思想

任取待排序序列中的某个元素为基准值,按照该排序码将待排序集合分割成两个子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右子序重复上的过程(找基准值->分割),直到所有元素都排列在相应的位置上。

主体框架为:

void QuickSort(int* a, int begin, int end)
{
	//只剩下一个元素或者区间不存在
	if (begin >= end)
	{
		return 0;
	}

	//基准值(存放的是下标)
	int keyi;

	//排序主体
	//{
	//	。。。。。。
	//}

	//分割
	// 
	//左子序:排[begin, keyi)
	QuickSort(a, begin, key);

	//右子序:排[keyi+1, end)
	QuickSort(a, key + 1, end);
}

我们从框架可以发现,这与二叉树的前序遍历的规则非常相似,所以我们根据前序遍历可以很快的写出框架,后续只需要分析如何按照基准值来对区间内的数据进行划分即可。

2.3划分区间内数据的方式

2.3.1 Hoare版本

在这里插入图片描述
创建俩个值,一个叫left,位于待排序列的起始位置,由于记录比基准值大的元素,另一个叫right,位于待排序列最后一个元素的位置,由于记录比基准值小的元素。

具体情况如下:
先让right走,遇到比基准值小的停下来,然后让left走,遇到比基准值大的停下来,当他们停下来后,将他们两个所对应的元素进行交换,重复此操作,直到他们两个相遇,相遇之后将基准值与相遇位置所对应的元素进行交换。

他们相遇位置的元素一定是 ≤ \le 基准值,所以不需要判断基准值与相遇元素的大小

void QuickSort(int* a, int begin, int end)
{
	//只剩下一个元素或者区间不存在
	if (begin >= end)
	{
		return;
	}

	//基准值(存放的是下标)
	int keyi = begin;

	//Hoare:
	int left = begin;
	int right = end - 1;
	while (left < right)
	{
		while (a[keyi] < a[right] && left < right)
		{
			right--;
		}

		while (a[keyi] >= a[left] && left < right)
		{
			left++;
		}

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

	keyi = left;
	

	//分割
	// 
	//左子序:排[begin, keyi)
	QuickSort(a, begin, keyi);

	//右子序:排[keyi+1, end)
	QuickSort(a, keyi + 1, end);
}

在这里插入图片描述

关于元素一定 ≤ \le 基准值的证明

他们相遇只有两种情况,left 遇到 right,或者 right 遇到 left
这里的遇到指的是,移动的一方 遇到 静止的一方

  1. 情况一:right不动,left遇到right,前面我们说了,我们会先让right先走,right不动了,就代表此时right位置所对应元素一定是小于基准值的,然后left遇到了right那么就代表left走过的区域已经没有比基准值大的元素了而后面已经被right走过或者交换过了,那么相遇位置之前的元素就都小于基准值了。
  2. 情况二:right一直走到left的位置,那么这时就有两种情况,left没有移动过和left移动过;
    left没有移动的话,那么他所在的值是基准值本身,这时就相遇元素等于基准值
    left移动过的话,由于是right先移动,那么他们必定发生了交换,且left位置的值一定是交换过后的值,这就相遇元素就一定小于基准值。

2.3.2 挖坑法

随着快排的不断发展,人们优化了Hoare方法,用挖坑法,虽然这种方法没有效率的提升,但提高了代码的可读性,也不要纠结相遇元素与基准值的大小关系。

在这里插入图片描述
这和Hoare方法是类似的,只是将交换变成了填补坑洞,我们定义一个临时变量pit来记录坑洞的位置,最后他们相遇,就让基准值自然填补就好了。

void QuickSort2(int* a, int begin, int end)
{
	//只剩下一个元素或者区间不存在
	if (begin >= end)
	{
		return;
	}

	//基准值(存放的是值)
	int key = a[begin];

	//挖坑法:
	int left = begin;
	int right = end - 1;

	//坑
	int pit = left;

	while (left < right)
	{
		while (key < a[right] && left < right)
		{
			right--;
		}

		a[pit] = a[right];
		pit = right;

		while (key >= a[left] && left < right)
		{
			left++;
		}

		a[pit] = a[left];
		pit = left;

	}
	a[pit] = key;

	//分割
	// 
	//左子序:排[begin, pit)
	QuickSort2(a, begin, pit);

	//右子序:排[pit+1, end)
	QuickSort2(a, pit + 1, end);

}

在这里插入图片描述

2.3.3 前后指针法

快速排序还有另一种方法,也是可读性最好的,我们可以定义两个指针(prev和cur),prev一个指向开头,cur指向prev的下一个数,然后让cur一直向前走,直到找比key小的数,然后让prev走一步并进行交换,然后继续让cur找比key小的值(遇到大于key的就++cur),重复以上操作,直到cur越界(cur >= n),当cur越界的时候,将key与prev指向的元素进行交换。
具体如图:

在这里插入图片描述

void QuickSort3(int* a, int begin, int end)
{
	//只剩下一个元素或者区间不存在
	if (begin >= end)
	{
		return;
	}

	//基准值(存放的是下标)
	int keyi = begin;

	//快慢指针法:
	int prev = begin;
	int cur = begin+1;

	while (cur < end)
	{
		if (a[keyi] > a[cur] && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	keyi = prev;
	

	//分割
	// 
	//左子序:排[begin, keyi)
	QuickSort2(a, begin, keyi);

	//右子序:排[keyi+1, end)
	QuickSort2(a, keyi + 1, end);

}

2.4 快速排序的优化

这个优化和上面的挖坑法、前后指针是不一样的,上述两种方法是优化了代码可读性和简化了思想复杂性,他们的效率都是一样的。
而这个优化的是快排的性能。

这个排序有个致命的问题,当有10w个元素的有序序列在经过快排后,快排会有栈溢出。
我们用希尔排序先将产生的随机树排好序,然后让快排再排序一遍。
在这里插入图片描述
这是为什么呢?
问题就出现在了选key上,由于我们选key都是直接选择数组的开头元素,但是面对一个已经排好序的数组来说,开头元素就是我最小的元素,那么right就会在第一个元素就与left相遇,然后分割出 [ k e y , k e y ) , [ k e y + 1 , n ) [key,key) ,[key+1,n) [key,key),[key+1,n)的左右子区间,然后 [ k e y + 1 , n ) [key+1,n) [key+1,n)这个右区间又会被分割成 [ k e y + 1 , k e y + 1 ) , [ ( k e y + 1 ) + 1 , n ) [key+1,key+1),[(key+1)+1,n) [key+1,key+1),[(key+1)+1,n)这样的左右子区间,然后右区间又会分成左右子区间,直到右区间只剩下一个元素。

也就是说,由于每次排序,key值都是整个数组中最小的数,因此,每次分割都会分割出一个只有key的左区间和剩余元素的右区间。但由于内存中的栈空间无法同时容纳十万个函数,所以会出现栈溢出。

解决方法(三数取中):先提取出数组的开头元素、结尾元素、中间元素,然后在他们三个中选择中间值,这样就能解决快排在有序情况下的栈溢出的问题,同时也能提升快排的效率

2.4.1 三数取中

int GetMidi(int* a, int left, int right)
{
	int midi = (left + right) / 2;
	// left midi right
	if (a[left] < a[midi])
	{
		if (a[midi] < a[right])
		{
			return midi;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else // a[left] > a[midi]
	{
		if (a[midi] > a[right])
		{
			return midi;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

放入快排:

void QuickSort1(int* a, int begin, int end)
{
	//只剩下一个元素或者区间不存在
	if (begin >= end)
	{
		return;
	}

	// 三数取中
	int midi = GetMidi(a, begin, end);
	Swap(&a[begin], &a[midi]);

	//基准值(存放的是下标)
	int keyi = begin;

	//Hoare:
	int left = begin;
	int right = end - 1;
	while (left < right)
	{
		while (a[keyi] < a[right] && left < right)
		{
			right--;
		}

		while (a[keyi] >= a[left] && left < right)
		{
			left++;
		}

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

	keyi = left;
	

	//分割
	// 
	//左子序:排[begin, keyi)
	QuickSort1(a, begin, keyi);

	//右子序:排[keyi+1, end)
	QuickSort1(a, keyi + 1, end);

}

这时运行就能通过了
在这里插入图片描述
有了三数取中,快排效率就明显提高,但是还是有人觉得快排不够快,因为随着递归的深入,效率会越来越慢(最后一层占了整个排序的50%),所以为了加快效率,我们可以进行小区间优化
在这里插入图片描述

2.4.2 小区间优化

小区间优化,就是当这个区间的大小到达一定程度时,直接用插入排序。

	// 小区间优化,不再递归分割排序,减少递归的次数
	if ((end - begin + 1) < 10)
	{
		InsertSort(a + begin, end - begin + 1);
	}

放入快排:

void QuickSort1(int* a, int begin, int end)
{
	//只剩下一个元素或者区间不存在
	if (begin >= end)
	{
		return;
	}
	// 小区间优化,不再递归分割排序,减少递归的次数
	if ((end - begin + 1) < 10)
	{
		InsertSort(a + begin, end - begin + 1);
	}

	else
	{
		// 三数取中
		int midi = GetMidi(a, begin, end);
		Swap(&a[begin], &a[midi]);

		//基准值(存放的是下标)
		int keyi = begin;

		//Hoare:
		int left = begin;
		int right = end - 1;
		while (left < right)
		{
			while (a[keyi] < a[right] && left < right)
			{
				right--;
			}

			while (a[keyi] >= a[left] && left < right)
			{
				left++;
			}

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

		keyi = left;


		//分割
		// 
		//左子序:排[begin, keyi)
		QuickSort1(a, begin, keyi);

		//右子序:排[keyi+1, end)
		QuickSort1(a, keyi + 1, end);
	}

}

2.5 快速排序的特性总结

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  2. 时间复杂度: O ( N ∗ l o g N ) O(N*logN) O(NlogN)在这里插入图片描述
  3. 空间复杂度: O ( l o g N ) O(logN) O(logN)
  4. 稳定性:不稳定

3.总结

本文主要讲解了直接选择排序和快速排序,直接选择排序还有一个上位版本(堆排序),但是堆排在之前就讲解过了,在本文就没有讲解(链接:堆排序)。

而快速排序我们讲解了Hoare版本,挖坑法和前后指针法,这三个方法并没有效率的差别,还讲解快排在有序情况下的优化(三数取中),和小区间优化。
其中小区间优化用到了直接插入排序,这个在前文也有讲到(链接:排序【插入排序】)。

最后感谢您能阅读完此片文章,如果有任何建议或纠正欢迎在评论区留言,也可以前往我的主页看更多好文哦(点击此处跳转到主页)。
如果您认为这篇文章对您有所收获,点一个小小的赞就是我创作的巨大动力,谢谢!!!

  • 16
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值