【数据结构】浅析快速排序(QuickSort)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_41035588/article/details/82875467

一. 什么是快速排序

  • 1.快排的本质
    快速排序是Koare在1962年提出的一种二叉树结构的交换排序,它实际上是一种对于冒泡排序改进的一种方法。
  • 2.快排的思想
    在待排序序列中任意取一个元素作为基准元素,按照该基准元素将待排序序列分为两个子序列,左边子序列的值都小于基准值,右边子序列的值都大于基准值。然后把左右子序列当做一个子问题,以同样的方法处理左右子序列,直到所有的元素都排列在相对应的位置上为止。快排是一个递归问题,它是按照二叉递归树的前序路线去划分的。
  • 3.代码实现
//递归
void QuickSort(int* arr, int begin, int end)
{
	assert(arr);
	if (begin < end)
	{
		int ret = PartSort(arr,begin,end);
		QuickSort(arr,begin,ret);
		QuickSort(arr,ret+1,end);

	}
}
 

此为递归部分,具体递归操作PartSort为核心部分

二. 快排(PartSort)常见实现方式

  • 1.hoare版本
//方法一:
int PartSort1(int* arr, int begin, int end)
{
	assert(arr);
	int key = arr[end];
	int index = end;
	while (begin < end)
	{
		while (begin<end&&arr[begin] <= key)
		{
			begin++;
		}
		while (begin<end&&arr[end] >= key)
		{
			end--;
		}
		if (begin<end)
			Swap(&arr[begin],&arr[end]);
	}
	Swap(&arr[begin],&arr[index]);
	return begin;

}
  • 2.挖坑法
//方法二: 挖坑法
int PartSort2(int* arr, int begin, int end)
{
	assert(arr);

	int key = arr[end];
	while (begin < end)
	{
		while (begin < end&&arr[begin]<=key)
		{
			++begin;

		}
		arr[end] = arr[begin];
		while (begin<end&&arr[end]>=key)
		{
			--end;
		}
		arr[begin] = arr[end];

	}
	arr[begin] = key;
	return begin;

}
  • 3.前后指针法
//方法三:左右指针法
int PartSort3(int* arr, int begin, int end)
{
	assert(arr);
	int key = arr[end];

	int pCur = begin;
	int pPre = begin - 1;
	while (pCur < end)
	{
		if (arr[pCur] <= key&&++pPre != pCur)
		{
			Swap(arr+pCur,arr+pPre);
		}

		++pCur;

	}
	Swap(&arr[++pPre], &arr[end]);
	return pPre;
	
}

三.快排常见的几道面试题

  • 1.快排的最坏和最优场景?时间复杂度分别为多少?

最坏场景:待排序列是有序的。既每次选的基准元素将待排序列划分为左右子序列,而左右子序列中必定有一个为空,它的二叉递归树深度是N,所以时间复杂度为O(NN)。
最好场景:每次选的基准都是待排序列最中间的元素,二叉递归树深度为O(logN),所以时间复杂度为O(N
logN)。
在这里插入图片描述

  • 2.快排的空间复杂度?

快排是一个递归的过程,每次函数调用只使用了常数的空间,所以它的空间复杂度就是它递归的深度。
最好场景:O(logN)
最坏场景:O(N)

  • 3.快排的稳定性

(1).什么是排序的稳定性
排序的稳定性是指,在对待排序列排序后,是否改变相同关键字的前后顺序(既相对位置)。例如:对【2,3,1(第一个),1(第二个),5,6】序列排序,如果排序结果为【1(第一个),1(第二个),2,3,5,6】那么这个排序算法是稳定的;如果排序结果为【1(第二个),1(第一个),2,3,5,6】,那么这个排序算法是不稳定的,因为关键字1的相对位置变化了。
(2).快排是否稳定
以一个例子分析快排是否稳定:
在这里插入图片描述
(3).排序稳定性的应用场景
分析一个排序算法的稳定性到底有什么用呢?,下边用一个场景分析:
【Solution】:假设在一次考试中,有两个同学的考试成绩是相同的,那么我们到底把哪一个同学排在前面呢?这时,年级主任说把学号排在前面的同学的成绩放在前面,因为学号是唯一的,我们可以先用学号把所有同学的成绩排序,在用一个稳定的排序算法在对总成绩排序一次,这时两个成绩相同的同学一定是学号在前的他就排在前面

四.快排的使用场景

不同条件下,排序方法的选择:

当n较小,可采用直接插入或直接选择排序。当记录规模较小并且基本有序时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序。
当待排序列初始状态基本有序,则应选用直接插人、冒泡或快速排序;
当n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
快速排序是目前基于比较的内部排序中被认为是最好的方法,
当待排序的关键字是随机分布时,快速排序的平均时间最短;堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。若要求排序稳定,则可选用归并排序。优先级队列通常用堆排序来实现

五.快排的优化

  • 优化一:三数取中

思想:这里的优化主要针对选择基准元素的优化。在选择基准的时候,当时最坏场景的时候,左边是最小,右边是最大,我们可以先根据最小和最大元素的下标确定中间的元素下标,然后在和要选择的基准元素交换位置;如果是最优场景,三数取中也可以优化,三个数中取一个中间值,必然要比随机选择基准好一些。

代码实现

//三数取中优化法(找三个值中间大的那个)
int GetMidKey(int* arr, int begin, int end)
{
	 assert(arr);
	 int mid = begin + (end - begin) / 2;
	 if (arr[begin] < arr[mid])
	 {
	  if (arr[mid] < arr[end])
	   return mid;
	  else
	  {
	   if (arr[begin]>arr[end])
	    return begin;
	   else
	    return end;
	  }
	 }
	 else
	 {
	  if (arr[begin] < arr[end])
	   return begin;
	  else
	  {
	   if (arr[mid]>arr[end])
	    return mid;
	   else
	    return end;
	  } 
 	}
}

//hoare版本(左边找大于基准,右边找小于基准,交换)
int PartSort1(int* arr, int begin, int end)
{
	assert(arr);
	//三数取中优化
	int index = GetMidKey(arr, begin, end);
	//和要选的基准交换
	Swap(&num[index], &num[end]);
	int key = arr[end];
	int index = end;
	while (begin < end)
	{
		while (begin<end&&arr[begin] <= key)
		{
			begin++;
		}
		while (begin<end&&arr[end] >= key)
		{
			end--;
		}
		if (begin<end)
			Swap(&arr[begin],&arr[end]);
	}
	Swap(&arr[begin],&arr[index]);
	return begin;

}


  • 优化二:小区间优化(把底层的递归替换掉)

快排对n较大的待排序列排序是很快的,但是对于n较小的序列时间复杂度和之间插入排序是差不多的,而且快排的递归算法还存在函数调用和返回的开销,所以我们可以考虑将快排递归算法的底层递归用直接插入排序给替换掉。

代码实现


 void QuickSort(int* arr, int begin, int end)
{
	assert(arr);
	if (begin < end)
	{
		 //小区间优化(替换掉后边几层的递归)
	    	if (right - left + 1 < 10)
	 	{
	 		InsertSort(arr, right - left + 1);	
	 	}
		int ret = PartSort(arr,begin,end);
		QuickSort(arr,begin,ret);
		QuickSort(arr,ret+1,end);

	}

 

六.将递归快排转换为循环快排

1.算法思想

递归算法是对快排的递归二叉树按照前序的路线来排列的,那么我们要把递归算法转换为循环算法,就要利用到栈的后进先出的特性

//非递归
void QuickSort(int* arr, int left, int right)
{
	assert(arr);

	stack<int> s;
	s.push(left);
	s.push(right);

	if (!s.empty())
	{
		int end = s.top();
		s.pop();
		int begin = s.top();
		s.pop();

		int ret = PartSort(arr,begin,end);

		if (begin<ret-1)
		{
			s.push(begin);
			s.push(ret-1);

		}
		if (end>ret + 1)
		{
			s.push(ret+1);
			s.push(end);
		}

	}
}

猜你喜欢

转载自blog.csdn.net/qq_41035588/article/details/82875467