一篇文章带你彻底搞懂十大经典排序之——快速排序

一、递归实现快速排序 

1.基本思想

通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比两一部分的关键字小,则课分别对这两部分记录继续进行排序,已达到整个序列有序。

2.算法描述

快速排序使用分治法来吧一个“串”分成两个“子串”。集体算法描述如下:

(1) 从数组中跳出一个元素,称为“基准”(key)

(2) 重新排序数组,所有元素比key值小的放在key值前面,所有元素比key值大的放在key值的后面(相同的数可以在任一边)。这个分区退出后,该基准就出去数列的中间位置。这个称为分区操作

(3) 递归,把小于key值元素的子数列和大于key值元素的子序列排列。

总结来说,该一趟该算法的效果 

(1) 确定key值的位置

(2) 分区间

3. hoare法(左右指针法)

hoare法是分区间的方法

(1)动图演示

(2) 具体步骤

● 先选定key值,一般选最左边或左右边,以key值在最左侧,且排升序为例

● 右指针 R 先向前移动,找比key小的值位置,找到之后,保持不动

● 左指针 L 再向后移动,找比key大的值的位置,找到之后,保持不动

● 交换 R 和 L 位置的数值

● 重复上述步骤23,知道 R 与 L 相遇,将相遇位置的值与key值进行交换(下面解释原因)

一趟排序结束,此时key左边的都是比key小的元素,右边都是比key大的元素

(3)相遇位置值与key交换

在上述 key值是最左边的值,且我们要得到的是升序数组的情况下,要将相遇位置的值与key值进行交换,说明:

  相遇位置的值一定比key小

证明: 

相遇的场景分析:

● L遇R :由于最左边是key,所以R先走,停下来(R停下的条件是遇到比key小的值)R停留的位置一定比key小,而L没有找到大的,遇到R才停下来。

● R遇L :R先走,找小于key的值,没有找到,直接与L相遇了。L停留的位置是上一轮交换的位置,上一轮交换,把比key小的值换到L的位置了。

相反:如果让最右边为key,则左边先走,可以保证相遇位置的值比key大 

(4)递归图解 

分区间:[ left , keyi - 1] keyi [ keyi + 1 , right ]

递归结束条件:

● 区间是一个值

● 不存在区间

(5)代码实现

void swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//简单快排
void QuickSort(int* arr, int left, int right)
{
	if (left >= right)
		return;
	int keyi = left;
	int begin = left, end = right;
	while (begin < end)
	{//先找end再找begin,顺序不能反
		//右边找小于arr[keyi]的数
		while (begin < end && arr[end] >= arr[keyi])
		{
			--end;
		}
		//左边找大于arr[keyi]的数
		while (begin < end && arr[begin] <= arr[keyi])
		{
			++begin;
		}
		swap(&arr[begin], &arr[end]);
	}
	//相遇位置的值一定比arr[keyi]小
	swap(&arr[keyi], &arr[begin]);
	keyi = begin;
	//划分区间,递归
	//[left,keyi - 1] keyi [keyi + 1,right]
	QuickSort(arr, left, keyi - 1);
	QuickSort(arr, keyi + 1, right);
}

 同样100万个随机数,不同排序的效率对比

(6)快排优化1(避免效率退化)

但该方法存在缺陷,当要排序的数组已经是有序的时候,调用该函数,会使递归的深度太深,有栈溢出的风险,而且效率也大大减小,直接由原来O(N*logN)的算法变成O(N^{2})的算法。

如图所示:

要避免有序情况下的效率退化:

1.随机选key

2.三数取中(更科学):取最左边、最右边和中间的数的做比较,选出中位数作key

注意:重新选出来的中位数下标所对应数组的值要与最左边的数进行交换,以不影响函数整体逻辑!

//避免有序情况下的的效率降低
//三数取中
int Middle(int* arr, int left, int right)
{
	int midi = (left + right) / 2;
	if (arr[midi] < arr[left])
	{
		if (arr[right] < arr[midi])
		{
			return midi;
		}
		else if (arr[right] < arr[left])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	if (arr[midi] > arr[left])
	{
		if (arr[right] > arr[midi])
		{
			return midi;
		}
		else if (arr[left] > arr[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}
//hoare法
int PartSort1(int* arr, int left, int right)
{
	//三数取中后得到的数,仍交换到最左边,为不影响整体逻辑
	int midi = Middle(arr, left, right);
	swap(&arr[midi], &arr[left]);

	int keyi = left;
	int begin = left, end = right;
	while (begin < end)
	{
		//右边找小于arr[keyi]的数
		while (begin < end && arr[end] >= arr[keyi])
		{
			--end;
		}
		//左边找大于arr[keyi]的数
		while (begin < end && arr[begin] <= arr[keyi])
		{
			++begin;
		}
		swap(&arr[begin], &arr[end]);
	}
	//相遇位置的值一定比arr[keyi]小
	swap(&arr[keyi], &arr[begin]);
	return begin;
}
//简单快排
void QuickSort(int* arr, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort1(arr,left,right);
	//划分区间,递归
	//[left,keyi - 1] keyi [keyi + 1,right]
	QuickSort(arr, left, keyi - 1);
	QuickSort(arr, keyi + 1, right);
}

经过改良,同样对100万个随机数进行排序,快排的效率瞬间提升

(7)快排优化2(小区间优化)

经过上面的学习,我们知道快排是递归实现的,其过程近似于我们学过的二叉树,如果把函数递归的过程理想化想象成完全二叉树的递归过程,那么递归到最后一层的个数,相当于全部递归次数的一半,倒数第二层的递归数相当于全部的四分之一...如果后几层不进行递归排序,是不是可以大大提高排序的效率!

下图展示的是,不同层数组快排的次数:

解决方案:

小区间优化,小区间内不再递归分割排序而是进行插入排序,减少递归次数

我们规定一下小区间:

当(right- left + 1) < 10 时,[right,left]属于小区间

//避免有序情况下的的效率降低
//三数取中
int Middle(int* arr, int left, int right)
{
	int midi = (left + right) / 2;
	if (arr[midi] < arr[left])
	{
		if (arr[right] < arr[midi])
		{
			return midi;
		}
		else if (arr[right] < arr[left])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	if (arr[midi] > arr[left])
	{
		if (arr[right] > arr[midi])
		{
			return midi;
		}
		else if (arr[left] > arr[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

//插入排序
void InsertSort(int* arr, int n)
{
	for (int i = 0; i < n - 1; i++)
	{//[0,end]是有序的,end + 1位置的值插入到[0,end],保持有序
		int end = i;
		int tmp = arr[end + 1];
		while (end >= 0)
		{
			if (tmp < arr[end])
			{
				arr[end + 1] = arr[end];
				end--;
			}
			else
			{
				break;
			}
		}
		arr[end + 1] = tmp;
	}
}
//hoare法
int PartSort1(int* arr, int left, int right)
{
	//三数取中后得到的数,仍交换到最左边,为不影响整体逻辑
	int midi = Middle(arr, left, right);
	swap(&arr[midi], &arr[left]);

	int keyi = left;
	int begin = left, end = right;
	while (begin < end)
	{
		//右边找小于arr[keyi]的数
		while (begin < end && arr[end] >= arr[keyi])
		{
			--end;
		}
		//左边找大于arr[keyi]的数
		while (begin < end && arr[begin] <= arr[keyi])
		{
			++begin;
		}
		swap(&arr[begin], &arr[end]);
	}
	//相遇位置的值一定比arr[keyi]小
	swap(&arr[keyi], &arr[begin]);
	return begin;
}

//简单快排
void QuickSort(int* arr, int left, int right)
{
	if (left >= right)
		return;

	if ((right - left + 1) < 10)
	{
		InsertSort(arr + left, right - left + 1);
	}
	else
	{
		int keyi = PartSort1(arr,left,right);
		//划分区间,递归
		//[left,keyi - 1] keyi [keyi + 1,right]
		QuickSort(arr, left, keyi - 1);
		QuickSort(arr, keyi + 1, right);
	}
	
}

100万个随机数,不同排序的效率:

 

4.挖坑法

挖坑法是对hoare法的优化,没有效率提升,但是可以不用分析:

1.左边做key,右边先走的问题

2.相遇位置为什么比key小的问题,因为它的相遇位置是坑

1.动图演示 

2.具体步骤

● 先将第一个数据存放在临时变量key中,形成一个坑位

● 右指针R开始向前移动,找到比key值小的位置

● 找到后,将该位置的值放入坑位,该位置形成新的坑位

● 左指针L开始向后移动,找到比key值大的位置

● 找到后,将该位置的值放入坑位,该位置形成新的坑位

● 重复上述步骤2345,直到L与R相遇,最后将key的值放入相遇位置的坑位中

结束,此时坑位左边的值都比坑位的值小,右边的值都比坑位的值大。

3.递归图示 

4.代码实现 

将之前所提到过的优化方法一并带入:

//插入排序
void InsertSort(int* arr, int n)
{
	for (int i = 0; i < n - 1; i++)
	{//[0,end]是有序的,end + 1位置的值插入到[0,end],保持有序
		int end = i;
		int tmp = arr[end + 1];
		while (end >= 0)
		{
			if (tmp < arr[end])
			{
				arr[end + 1] = arr[end];
				end--;
			}
			else
			{
				break;
			}
		}
		arr[end + 1] = tmp;
	}
}

//避免有序情况下的的效率降低
//三数取中
int Middle(int* arr, int left, int right)
{
	int midi = (left + right) / 2;
	if (arr[midi] < arr[left])
	{
		if (arr[right] < arr[midi])
		{
			return midi;
		}
		else if (arr[right] < arr[left])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	if (arr[midi] > arr[left])
	{
		if (arr[right] > arr[midi])
		{
			return midi;
		}
		else if (arr[left] > arr[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

//挖坑法
void QuickSort2(int* arr, int left, int right)
{
	if (left >= right)
		return;
	//小区间优化
	if ((right - left + 1) < 10)
	{
		InsertSort(arr + left, right - left + 1);
	}
	else
	{
		//避免效率退化
		//三数取中后得到的数,仍交换到最左边,为不影响整体逻辑
		int midi = Middle(arr, left, right);
		swap(&arr[midi], &arr[left]);

		//key是临时变量,记录最左边的元素
		int key = arr[left];
		int begin = left, end = right;
		//pit是坑位
		int pit = left;
		while (begin < end)
		{
			//右边找小于key的数
			while (begin < end && arr[end] >= key)
			{
				--end;
			}
			//将找到的比key小的数填到坑位,刷新新坑位
			arr[pit] = arr[end];
			pit = end;
			//左边找大于arr[keyi]的数
			while (begin < end && arr[begin] <= key)
			{
				++begin;
			}
			//将找到的比key大的数填到坑位,刷新新坑位
			arr[pit] = arr[begin];
			pit = begin;
		}
		//最后将key值填充到坑位
		arr[pit] = key;
		key = arr[begin];
		//划分区间,递归
		//[left,pit - 1] pit [pit + 1,right]
		QuickSort(arr, left, pit - 1);
		QuickSort(arr, pit + 1, right);
	}
}

5.前后指针法

(1)动图演示

(2)具体步骤

● 初始时,prev指针指向序列开头,cur指向prev指向的下一位

● 判断cur指针指向的数据是否小于key,若小于,则prev指针后移一位,并将cur与prev所指向的内容交换,然后cur++

● 若cur指向的数据大于key,cur++

● 重复步骤23,直到cur越界

 将prev所指向的值与key交换

此时key左边的数都比key小,右边的值都比key大

(3)代码实现

//插入排序
void InsertSort(int* arr, int n)
{
	for (int i = 0; i < n - 1; i++)
	{//[0,end]是有序的,end + 1位置的值插入到[0,end],保持有序
		int end = i;
		int tmp = arr[end + 1];
		while (end >= 0)
		{
			if (tmp < arr[end])
			{
				arr[end + 1] = arr[end];
				end--;
			}
			else
			{
				break;
			}
		}
		arr[end + 1] = tmp;
	}
}
//三数取中
int Middle(int* arr, int left, int right)
{
	int midi = (left + right) / 2;
	if (arr[midi] < arr[left])
	{
		if (arr[right] < arr[midi])
		{
			return midi;
		}
		else if (arr[right] < arr[left])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	if (arr[midi] > arr[left])
	{
		if (arr[right] > arr[midi])
		{
			return midi;
		}
		else if (arr[left] > arr[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}
//前后指针法
int PartSort2(int* arr, int left, int right)
{
	//三数取中后得到的数,仍交换到最左边,为不影响整体逻辑
	int midi = Middle(arr, left, right);
	swap(&arr[midi], &arr[left]);

	int keyi = left;
	int prev = left, cur = prev + 1;
	while (cur <= right)
	{
		//cur找小,与prev位置的值交换
		if (arr[cur] < arr[keyi] && ++prev != cur)
			swap(&arr[cur], &arr[prev]);
		//找不到就向后移一位
		cur++;
	}
	swap(&arr[keyi], &arr[prev]);
	return prev;
}
//快排
void QuickSort(int* arr, int left, int right)
{
	if (left >= right)
		return;

	if ((right - left + 1) < 10)
	{
		InsertSort(arr + left, right - left + 1);
	}
	else
	{
		int keyi = PartSort2(arr, left, right);
		//划分区间,递归
		//[left,keyi - 1] keyi [keyi + 1,right]
		QuickSort(arr, left, keyi - 1);
		QuickSort(arr, keyi + 1, right);
	}
}

二、非递归实现快排

数据结构的栈是在堆上开辟的空间,比函数调用所开辟的栈大很多。

 具体方法如图所示:

#include"Stack.h"
//快排(用栈非递归)— dfs
void QuickSortNonR(int* arr, int left, int right)
{
	Stack st;
	StackInit(&st);
	//右左区间入栈
	StackPush(&st, right);
	StackPush(&st, left);
	//循环每走一次(相当于之前的一次递归),取栈顶区间,单趟排序,右左子区间入栈
	while (!StackEmpty(&st))
	{
		int begin = StackTop(&st);
		StackPop(&st);
		int end = StackTop(&st);
		StackPop(&st);

		int keyi = PartSort2(arr, begin, end);
		//[begin,keyi - 1] keyi [keyi + 1,end]
		//先右后左
		if (end > keyi + 1)
		{
			StackPush(&st, end);
			StackPush(&st, keyi + 1);
		}
		if (begin < keyi - 1)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, begin);
		}
	}
	StackDestroy(&st);
}

三、注意

面试时手撕,不用三数取中和小区间优化,讲一下思路就行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值