排序算法(2)——快速排序

目录

[1. 实现方式](#1. 实现方式)

[1.1 霍尔法](#1.1 霍尔法)

[1.2 挖坑法](#1.2 挖坑法)

[1.3 前后指针法](#1.3 前后指针法)

[2. 时间复杂度分析](#2. 时间复杂度分析)

[3. 快速排序优化](#3. 快速排序优化)

[3.1 三数取中](#3.1 三数取中)

[3.2 小区间使用插入排序](#3.2 小区间使用插入排序)

[3.3 非递归实现](#3.3 非递归实现)


快速排序是英国计算机科学家托尼・霍尔(C. A. R. Hoare)在 1960 年年提出的一种二叉树结构的交换排序方法。
基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

1. 实现方式

1.1霍尔法

基本步骤:

  • 定义一个小兵right从数组最后一个元素开始向前遍历,如果找到比key小的值就停下来。
  • 定义一个小兵left从数组第一个元素开始向后遍历,如果找到比key大的值就停下来。
  • 交换left小兵和right小兵所在位置的值。
  • 当left小兵和right小兵相遇后,将第一个元素(key)与它们相遇位置的值交换。
  • 相遇位置的左区间和右区间继续递归上述步骤。

代码实现

//快速排序------霍尔法
void QuickSort(int* a, int left, int right)
{
	if (left >= right) //数组长度为0、1 时递归停止
		return;

	int keyi = left; //第一个位置的元素作为基准值
	int begin = left, end = right;
	while (begin < end)
	{
		//右边找比keyi小的值,right小兵先走
		while (begin < end && a[end] >= a[keyi])//注意判断条件,防止相遇没停继续找小情况发生!
		{
			--end;
		}
		//左边找比keyi大的值
		while (begin < end && a[begin] <= a[keyi])
		{
			++begin;
		}

		Swap(&a[begin], &a[end]);
	}	   
	Swap(&a[keyi], &a[begin]); //begin和end相遇,交换
	keyi = begin;	  //更新基准值的索引

	//分割区间 递归	
	QuickSort(a, left, keyi-1);//排列比基准值小的左子数组	
	QuickSort(a, keyi + 1, right);//排列比基准值大的右子数组
}

递归推演

我们可能会产生这样的疑问,相遇位置的值如果比基准值大怎么办?
不会出现这种情况,因为相遇位置的值一定比基准值小,无非就以下面两种情况,可以发现相遇位置的值都比基准值要小,我们无需担心。

  1. right小兵先走,停下来,right小兵停下来位置的值一定比基准值小,left小兵没有找到比基准值大的值,就与left小兵相遇了。
  2. right小兵先走,找小,没有找到比基准值小的,就与left小兵相遇了,而left小兵停留的位置就是上一轮交换的位置,经过上一轮的交换把right小兵下比基准值小的值交换到left小兵所在位置了。


推论:如果让最右边的元素作为基准值,要让left小兵先走,可以保证相遇位置的值比基准值要大。(一边做基准值,要让另一边先走)

1.2 挖坑法

基本步骤:

  • 将第一个元素的位置作为第一个坑位,将基准值保存到变量key中。
  • 定义一个小兵right从数组最后一个元素开始向前遍历,如果找到比key小的值就停下来,将right下标对应的值赋给上一个坑位,并将right所在位置作为新的坑位。
  • 定义一个小兵left从数组第一个元素开始向后遍历,如果找到比key大的值就停下来,将left下标对应的值赋给上一个坑位,并将left所在位置作为新的坑位。
  • 当left小兵和right小兵相遇后,相遇的位置就是坑位,再将key的值放到坑位中。

代码实现

//快速排序------挖坑法
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int begin = left, end = right;
	int key = a[left]; //将基准值保存下来
	int hole = left; //建第一个坑
	while (begin < end)
	{
		while (begin < end && a[end] >= key)
		{
			end--;
		}
		a[hole] = a[end];//填坑
		hole = end;//变换坑的位置

		while (begin < end && a[begin] <= key)
		{
			begin++;
		}
		a[hole] = a[begin];
		hole = begin;
	}

	a[hole] = key;//相遇位置的坑填基准值
	
	QuickSort(a, left, hole - 1);
	QuickSort(a, hole + 1, right);
}

1.3 前后指针法

//快速排序------前后指针法
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	int keyi = left;   //基准值位置
	int prev = left;   //定义prev指针
	int cur = left + 1;//定义cur指针

	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)// cur指针找到小,prev指针+1,再交换,如果prev=cur就不用进行交换
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}

	Swap(&a[prev], &a[keyi]); //交换prev和keyi位置上的值	
	keyi = prev;

	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);	
}

2. 时间复杂度分析

最好情况下,每次把数组都平均分成两个部分,每一部分在下一轮又分别被拆分成两部分,这样递归下去会得到一个完全二叉树,如果排n个数字,递归的深度即二叉树的深度约logn,每次递归都需要遍历一次区间中的每一个元素,每一层的时间复杂度为O(n),总的时间复杂度为O(n*logn)
最坏情况,每一轮只确定基准元素的位置,选取的基准值刚好是这个区间的最大值或最小值。比如:对n个数排序,最小值被放在了第一个位置,经过调换元素顺序操作,最小值被放在了第一个位置,剩余n-1个数放在第2-n个位置,这样递归下去,都只能将最小的数放到第一个位置,剩下的元素没有任何变化,所以对n个数排序,需要n-1次递归调用,递归树画出来是一颗斜树,比较次数为n-1+n-2+......+1=n(n-1)/2,时间复杂度为O(n^2)。

3. 快速排序优化

3.1 三数取中

在快速排序中,基准值的选择对算法的性能有很大影响。如果选择得当,可以将数组较为均匀地划分为两部分,从而提高排序效率。反之,如果选择不当,可能会导致数组划分极不均衡,从而退化为O(n²)的时间复杂度。
固定选取第一个位置的元素作为基准值会出现问题,比如有序的情况下,会出现递归深度太深,可能会导致爆栈问题,三数取中的好处是,即使数组已经部分有序(如完全有序或逆序),也能通过选择一个相对"居中"的元素作为枢纽,来保持数组划分的平衡性。
三数取中: 在待排序数组的第一个、最后一个和中间三个元素中选取中间值作为基准值,例如对数组{9,1,5,8,3,7,4,6,2},取左9、中3、右2来比较,使得midi=3。

代码实现

//三数取中实现
int GetMidi(int* a, int left, int right) //返回三个关键字里中间值的下标
{
	int midi = (left + right) / 2;//算出中间位置的下标
	if (a[left] < a[midi])
	{
		if (a[midi] < a[right])
		{
			return midi;
		}
		else if (a[left] < a[right]) // a[left] < a[midi] && a[right] < a[midi] && a[left] < a[right]
		{
			return right;
		}
		else// a[left] < a[midi] && a[right] < a[midi] && a[left] > a[right]
		{
			return left;
		}
	}

	else  //a[left] > a[midi]
	{
		if (a[midi] > a[right])
		{
			return midi;
		}
		else if (a[left] > a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}

快排优化------三数取中
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
    //三数取中
	int midi = GetMidi(a, left, right);
	Swap(&a[left], &a[midi]);//交换,将三数取中取到的值作为基准值
    int keyi = left;

	int begin = left, end = right;
	while (begin < end)
	{
		while (begin < end && a[end] >= a[keyi])
		{
			--end;
		}
		while (begin < end && a[begin] <= a[keyi])
		{
			++begin;
		}
		Swap(&a[begin], &a[end]);
	}
	Swap(&a[keyi], &a[begin]);
	keyi = begin;//更新keyi的位置

	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

三数取中对于非常大的待排序序列来说不足以保证能够选出一个好的基准值,还有一个办法就是九数取中,从数组中分三次取样,每次取三个数,三个样品里个取出中数,然后再从这三个中数里面取出一个中数作为基准值。

3.2 小区间使用插入排序

由于快排递归类似于二叉树结构,越到后面几层递归次数越多,每次递归调用都会耗费一定的栈空间,如果能减少递归,将会大大提高性能。我们对其进行优化,让它不进行后面几层的递归,递归分割成小区间后就不再让它递归,使用直接插入排序对小区间进行排序,(直接插入排序是简单排序中性能最好的)。


基本思想:增加一个判断,当区间不大于某个常数时(有资料认为7比较合适,也有资料认为50更合理,实际可调整),就用直接插入排序,下面我们选10作为小区间的判断条件 。

代码实现

//快排优化------小区间使用插入排序
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	//小区间优化,不再递归分割排序,减少递归次数,提高运算效率
	if ((right - left + 1) < 10)
	{
		InsertSort(a + left, right - left + 1);//直接插入排序
	}
	else
	{
		//三数取中
		int midi = GetMidi(a, left, right);
		Swap(&a[left], &a[midi]);

		int keyi = left;
		int begin = left, end = right;
		while (begin < end)
		{
			while (begin < end && a[end] >= a[keyi])
			{
				--end;
			}
			while (begin < end && a[begin] <= a[keyi])
			{
				++begin;
			}
			Swap(&a[begin], &a[end]);
		}
		Swap(&a[keyi], &a[begin]);
		keyi = begin;
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}	
}

3.3 非递归实现

递归算法存在着栈溢出的风险,可以把它改造成非递归的形式,递归算法主要是在划分子区间,如果想采用非递归的方式实现快排,只需要用一个来保存区间就可以了。一般将递归程序改成非递归首先想到的就是使用栈,因为递归本身就是一个压栈的过程。
基本步骤:

  • 入栈的时候先入右,再入左,这样出栈的时候就会先出左,再出右。
  • 取两次栈顶元素作为区间的左和右,并弹出栈顶元素。
  • 再对该区间进行单趟排序。
  • 重复上述过程直到栈为空。
//快速排序------前后指针法
int PartSort(int* a, int left, int right)
{
	int keyi = left;   //基准值位置
	int prev = left;   //定义prev指针
	int cur = left + 1;//定义cur指针

	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)// cur指针找到小,prev指针+1,再交换,如果prev=cur就不用进行交换
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}

	Swap(&a[prev], &a[keyi]); //交换prev和keyi位置上的值	
	keyi = prev;

	return keyi;
}

//非递归实现快速排序
#include"Stack.h"
void QuickSortNonRecur(int* a, int left, int right)
{
	ST st; //定义一个栈
	STInit(&st); //初始化栈
	STPush(&st, right); //入栈
	STPush(&st, left);  //入栈
	while (!STEmpty(&st)) //栈为空时结束循环
	{
		int begin = STTop(&st); //取栈顶元素
		STPop(&st); //出栈
		int end = STTop(&st);
		STPop(&st);

		//单趟排序
		int keyi = PartSort(a, begin, end);
		 //[begin,keyi-1] keyi[keyi+1,end]

		if (keyi + 1 < end) //如果区间只有一个值或者没有值就不入栈了
		{
			STPush(&st, end);
			STPush(&st, keyi + 1);
		}
		if (begin < keyi - 1)
		{
			STPush(&st, keyi - 1);
			STPush(&st, begin);
		}
	}
	STDestroy(&st);
}
相关推荐
曲奇是块小饼干_32 分钟前
leetcode刷题记录(七十三)——543. 二叉树的直径
java·数据结构·算法·leetcode·职场和发展
Yuleave1 小时前
大型语言模型(LLM)在算法设计中的系统性综述
大数据·算法·语言模型
曲奇是块小饼干_1 小时前
leetcode刷题记录(七十二)——146. LRU 缓存
java·算法·leetcode·链表·职场和发展
tt5555555555551 小时前
每日一题-数组中的逆序对
数据结构·算法·排序算法
夏尔Gaesar6 小时前
pcm | Parity Check Matrix(奇偶校验矩阵)
算法·矩阵·pcm
m0_dawn9 小时前
《贪心算法:原理剖析与典型例题精解》
python·算法·职场和发展·贪心算法·蓝桥杯
invincible_Tang9 小时前
贪心算法(题2)最大不相交区间数量
算法·贪心算法
AIzealot无10 小时前
力扣hot100之螺旋矩阵
算法·leetcode·矩阵
兑生10 小时前
力扣面试150 长度最小的子数组 滑动窗口
算法·leetcode·面试
miilue10 小时前
[LeetCode] 链表I — 704#设计链表 | 203#移除链表元素 | 206#反转链表 | 递归法
java·开发语言·c++·算法·leetcode·链表