简单易懂排序算法(七)【快速排序】

版权声明:本文为博主原创文章,转载请注明出处-- https://blog.csdn.net/qq_38790716/article/details/86143507

快速排序

快速排序基本思想是通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的

直接从代码来分析:

void QuickSort(vector<int>& vec)
{
	QSort(vec, 0, vec.size() - 1);
}

void QSort(vector<int>& vec, int low, int high)
{
	int pivot; //记录枢轴值pivot
	if (low < high)
	{
		pivot = Partition(vec, low, high);  //将序列分割为符合要求的两部分
											//算出枢轴值pivot
		QSort(vec, low, pivot - 1);       //对低子表递归排序
		QSort(vec, pivot + 1, high);       //对高子表递归排序
	} 
}

int Partition(vector<int>& vec, int low, int high)
{
	int pivotkey;         
	pivotkey = vec[low];   //对子表的第一个作枢轴记录
	while (low < high)     //从表的两端进行扫描
	{
		while (low < high && vec[high] >= pivotkey)  //从后找到第一个比枢轴小的值
			high--;
		swap(vec[low], vec[high]);  //交换该值与枢轴值
		while (low < high && vec[low] <= pivotkey) //从前找到第一个比枢轴大的值
			low++;
		swap(vec[low], vec[high]);  //交换该值与枢轴值
	}
	return low;    //返回枢轴下标
}

从代码来看,因未考虑诸多因素,所以实现比较简单,但不管其如何变化,中心思想仍然是将整个序列进行一个 p a r t i t i o n partition ,然后再递归对每个部分进行排序,明白了这一点代码实现就比较简单了

简单的测试
测试序列:50,10,90,30,70,40,80,60,20
在这里插入图片描述

从结果可以看出每次 p a r t i t i o n partition 所找的枢轴值

复杂度分析

快速排序的时间性能取决于快速排序递归的深度。

  • 最优情况下, P a r t i t i o n Partition 每次都划分得很均匀,如果排序n个关键字,其递归树得深度就为[ log 2 n \log_2{n} + 1],即仅需递归 l o g 2 n log_2{n} 次,需要时间为 T ( n ) T(n) 的话,第一次 P a r t i t i o n Partition 应该是需要对整个数组扫描一遍,做 n n 次比较。然后,获得的枢轴将数组一分为二,那么还需要 T n / 2 ) Tn/2) 的时间,于是不断进行划分,得出下面的不等式推断:

T(n) \leq 2T(n/2) + n, T(1) = 0
T(n) \leq 2(2T(n/4) + n/2) + n = 4T(n/4) + 2n
T(n) \leq 4(2T(n/8) + n/4) + 2n = 8T(n/8) + 3n

T(n) \leq nT(1) + ( log 2 n \log_2{n} ) ·n = O(n l o g n log{n} )

  • 最坏情况下,待排序的序为支正序或倒序,每次划分只得到一个比上一次划分少一个记录的子序列,注意另一个为空。如果递归数画出来,他就是一颗协树,此时需要执行一个 n 1 n-1 次递归调用, 且第 i i 次划分还需经过 n 1 n-1 次关键字的转换,也就是枢轴的位置,因此比较次数为:

i = 1 n 1 \sum_{i=1}^{n-1} = n-1 + n-2 +… + 1 = n ( n 1 ) 2 \frac{n(n-1)}{2}

最终其时间复杂度为O( n 2 n^2 )

平均情况下,设枢轴的关键字应该在第 k k 的位置,那么:

T(n) = 1 n \frac{1}{n} k = 1 n T ( k 1 ) + T ( n k ) ) + n \sum_{k=1}^nT(k - 1) + T(n-k)) + n = 2 n \frac{2}{n} k = 1 n T ( k ) + n \sum_{k=1}^nT(k) + n

由数学归纳法可以证明其数量级为O(n log n \log n )

就空间复杂度而言,主要递归造成的栈空间的使用,最好情况下,递归树的深度为 log 2 n \log_2{n} ,其空间复杂度也为O[ log n \log{n} ],最坏情况,需要进行 n 1 n - 1 次递归调用,其空间复杂度为 O ( n ) O(n) ,空间复杂度也为O[ log n \log{n} ]

由于关键字的比较与交换是跳跃的,因此,快速排序也是一种不稳定的排序算法

快速排序的优化

1. 优化选取枢轴

采用名为三数取中的方法进行优化:

int pivotkey;

int m = low + (high - low)/2;
if (vec[low] > vec[high])
	swap(vec[low], vec[high]);  //使左端值较小
if (vec[m] > vec[high])
	swap(vec[m], vec[high]);  //使中间值较小
if (vec[m] > vec[low])
	swap(vec[m], vec[low];   //使左端值较小

pivotkey = vec[low];

2. 优化不必要的交换

将交换改为替换:

int Partition(vector<int>& vec, int low, int high)
{
	int pivotkey;         
	pivotkey = vec[low];   //对子表的第一个作枢轴记录
	int tmp = pivotkey;
	while (low < high)     //从表的两端进行扫描
	{
		while (low < high && vec[high] >= pivotkey)  //从后找到第一个比枢轴小的值
			high--;
		vec[low] = vec[high];  //交换该值与枢轴值
		while (low < high && vec[low] <= pivotkey) //从前找到第一个比枢轴大的值
			low++;
		vec[high]  = vec[low];  //交换该值与枢轴值
	}
	vec[low] = tmp;
	return low;    //返回枢轴下标
}

3. 优化小数组时的排序方案

即在小数组时采用不同的排序方法:

#define MAX_LENGTH_INSERT_SORT 7

void QSort(vector<int>& vec, int low, int high)
{
	int pivot;
	if ((high - row) > MAX_LENGTH_INSERT_SORT)  //当high - low大于常数采用快速排序
	{
		pivot = Partition(vec, low, high);

		QSort(vec, low, pivot - 1);
		QSort(vec, pivot + 1, high);
	}
	else
		InsertSort(vec);   //小于常数采用插入排序
}

4. 优化递归操作

Q S o r t QSort 实施尾递归优化:

void QSort(vector<int>& vec, int low, int high)
{
	int pivot;
	if ((high - row) > MAX_LENGTH_INSERT_SORT)  //当high - low大于常数采用快速排序
	{
		while (low < high) 
		{
			pivot = Partition(vec, low, high);

			QSort(vec, low, pivot - 1);        //对低子表进行递归排序
			low = pivot + 1;                 //尾递归
		}
	}
	else
		InsertSort(vec);
}

从实现来说,将 p i v o t + 1 pivot + 1 赋值给 l o w low ,然后将 i f if 改为了 w h i l e while ,这样在进行下一次循环时,得到的 P a r t i t i o n ( v e c , l o w , h i g h ) Partition(vec, low, high) 实际上就相当于 Q S o r t ( v e c , p i v o t + 1 , h i g h ) QSort(vec, pivot + 1, high) ,与之前的并无差别,但这样修改实际将递归修改为了迭代,采用迭代而非递归的方法可以缩减堆栈深度,从而提高了整体性能

补充

  • S G I S T L SGI STL 中采用的sort在早期版本里使用的是完全的 Q u i c k S o r t QuickSort ,但后来考虑到不当的枢轴选择导致的不当分割造成算法性能下降,如今, S G I S T L SGI STL 采用的是一种名为 i n t r o s o r t introsort (内省式排序)的算法,其行为在大部分情况下与 Q u i c k S o r t QuickSort 完全相同,但当分割行为有恶化二次行为的倾向时,能够自我侦测,转而改用 H e a p S o r t Heap Sort ,使其效率能维持在 H e a p S o r t Heap Sort 的O( N log N N\log{N} )

测试代码:

#include <iostream>
#include <vector>
using namespace std;

void QuickSort(vector<int>& vec);
void QSort(vector<int>& vec, int low, int high);
int Partition(vector<int>& vec, int low, int high);
void PrintStep(vector<int> vec, int n, int i);
void PrintResult(vector<int> vec, int n);

int count = 1;

void QuickSort(vector<int>& vec)
{
	cout << "--------------快速排序--------------" << endl;
	QSort(vec, 0, vec.size() - 1);
}

void QSort(vector<int>& vec, int low, int high)
{
	int pivot;
	if (low < high)
	{
		pivot = Partition(vec, low, high); 
		
		QSort(vec, low, pivot - 1);
		QSort(vec, pivot + 1, high);
	} 
}

int Partition(vector<int>& vec, int low, int high)
{
	int pivotkey;
	pivotkey = vec[low];
	while (low < high)
	{
		while (low < high && vec[high] >= pivotkey)
			high--;
		swap(vec[low], vec[high]);
		while (low < high && vec[low] <= pivotkey)
			low++;
		swap(vec[low], vec[high]);
	}
	PrintStep(vec, vec.size(), count);
	++count;
	return low;
}

void PrintStep(vector<int> vec, int n, int i)
{
	cout << "第" << i << "轮排序结果: ";
	for (int j = 0; j < n; ++j)
		cout << vec[j] << ' ';
	cout << endl;
}
void PrintResult(vector<int> vec, int n)
{
	for (int j = 0; j < n; ++j)
		cout << vec[j] << ' ';
	cout << endl;
}
int main(int argc, char **argv)
{
	int a[] = {50,10,90,30,70,40,80,60,20};
	vector<int> vec(a, a+9);
	QuickSort(vec);
	cout << "最终排序结果为:";
	PrintResult(vec, vec.size());
	return 0;
} 

猜你喜欢

转载自blog.csdn.net/qq_38790716/article/details/86143507
今日推荐