排序算法 | 快速排序及其优化

快速排序(Quick Sort)

快速排序(Quick Sort)是对冒泡排序的一种改进。快排的真谛在于极端情况下每次将概率等分 1/2   每次小于这个数的放在前面大于的放在后面,即每次排序都找出了一个正确位置,使得下一次排序个数减少一半。快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分的值均比另一部分的值小。之后分别对这两部分递归或者非递归的方式继续进行快速排序,以达到整个序列有序的目的。


目录

快速排序(Quick Sort)

1、算法描述

2、算法分析

2.1、选基准pivot的三种方式

2.1.1、固定位置选取基准

2.1.2、随机化位置选取基准

2.1.3、三分取中法选取基数

2.2、划分partition的三种方式

2.2.1、挖坑法(固定左位置为基准+挖坑法)

2.2.2、左右指针法(固定左位置为基准+左右指针法)

2.2.3、快慢指针法(固定左位置为基准+快慢指针法)

2.3、求解调用快排的两种方式

2.3.1、递归recursive

2.3.2、非递归(迭代iteration,栈stack实现递归功能)

2.4、复杂度分析

2.4.1、时间复杂度

2.4.2、空间复杂度

2.5、算法稳定性

3、算法实现与优化

3.1、基础版

3.2、优化,当待排列序列的长度分割到一定大小时,使用插入排序

3.3、优化,聚集相同基准法;处理重复的数组元素,减少递归的次数

4、算法示例

5、注意


1、算法描述

快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。快排的真谛在于 极端情况下每次将概率等分 1/2   每次小于这个数的放在前面 大于的放在后面 即每次排序都找出了一个正确位置,使得下一次排序个数减少一半。

分治法的基本思想是:将原问题分解为若干个规模更小但结构与原问题相似的子问题sub。递归地解这些子问题,然后将这些子问题的解组合为原问题的解。

  1. 选基准pivot:在A[low..high]中任选一个记录作为基准(Pivot)(选取基准的三种方式:固定位置、随机化、三分取中)
  2. 划分partition: 以此基准将当前无序区划分为左、右两个较小的子区间R[low..pivotpos-1)A[pivotpos+1..high],并使左边子区间中所有记录的关键字均小于等于基准记录(不妨记为pivot)的关键字pivot.key,右边的子区间中所有记录的关键字均大于等于pivot.key,而基准记录pivot则位于正确的位置(pivotpos)上,它无须参加后续的排序。(划分实现的三种方式:挖坑法、左右指针法、快慢指针法)
  3. 求解:通过递归recursive或者非递归(迭代iteration,栈stack实现递归功能),调用快速排序对左、右子区间R[low..pivotpos-1]和R[pivotpos+1..high]快速排序。
  4. 组合:因为当"求解"步骤中的两个递归调用结束时,其左、右两个子区间已有序。对快速排序而言,"组合"步骤无须做什么,可看作是空操作。

2、算法分析

2.1、选基准pivot的三种方式

原因是无论是哪种取基的方法,最终用的还是固定位置法,也就是还是要拿首部的数据开始进行比较调整

2.1.1、固定位置选取基准

基本思想:固定选取第一个或最后一个元素作为基准值。

注意:基本的快速排序,固定选取第一个或最后一个元素作为基准。但是,这是一直很不好的处理方法,因为在序列有序的情况下,排序最容易退化成冒泡的方法,效率很低

固定选取第一个作为基准

int parttitionpotholing(vector<int> &a, int low, int high)
{
	//以第一个数组元素作为pivot基准值
	int pivot = a[low];
	//挖坑填数
	while (low < high)
	{
		//从右往左找第一个比基准值小的值
		while (low < high && a[high] >= pivot)high--;
		//挖坑,将土填到前面low的坑
		a[low] = a[high];
		
		//从左往右找第一个比基准值大的值
		while (low < high && a[low] <= pivot)low++;
		//挖坑,将土填到后面high的坑
		a[high] = a[low];
	}
	//low==high跳出循环,low的位置放的是基准值pivot
	a[low] = pivot;
	return low;
}

固定选取最后一个作为基准

int parttitionpotholing(vector<int> &a, int low, int high)
{
	//以最后一个数组元素作为pivot基准值
	int pivot = a[high];
	//挖坑填数
	while (low < high)
	{
		//从右往左找第一个比基准值小的值
		while (low < high && a[high] >= pivot)high--;
		//挖坑,将土填到前面low的坑
		a[low] = a[high];
		
		//从左往右找第一个比基准值大的值
		while (low < high && a[low] <= pivot)low++;
		//挖坑,将土填到后面high的坑
		a[high] = a[low];
	}
	//low==high跳出循环,low的位置放的是基准值pivot
	a[low] = pivot;
	return low;
}

2.1.2、随机化位置选取基准

基本思想:选取待排序列中任意一个数作为基准值,选出的任意值与low下标元素交换,可继续使用Partition函数

每次在要排序的序列区间内 随意找到一个定为基准(这种方法看人品随机的好就接近于1/2

  •     //随机化位置选取基准,在固定化前加一个生成任意数,通用公式:a+rand() % n;
  •     swap(a[low], a[low + rand() % (high - low + 1)]);
  •     //然后与low位置交换继续固定位置的Partition函数。
  •     //产生不同的随机种子
  •     srand((unsigned)time(NULL));
  •     //产生指定范围内的随机数,
int parttitionpotholing(vector<int> &a, int low, int high)
{
	//随机化位置选取基准,在固定化前加一个生成任意数,
	//然后与low位置交换继续固定位置的Partition函数。
	//产生不同的随机种子
	srand((unsigned)time(NULL));
	//产生指定范围内的随机数,通用公式:a+rand() % n;
	swap(a[low], a[low + rand() % (high - low + 1)]);


	//以第一个数组元素作为pivot基准值
	int pivot = a[low];
	//挖坑填数
	while (low < high)
	{
		//从右往左找第一个比基准值小的值
		while (low < high && a[high] >= pivot)high--;
		//挖坑,将土填到前面low的坑
		a[low] = a[high];
		
		//从左往右找第一个比基准值大的值
		while (low < high && a[low] <= pivot)low++;
		//挖坑,将土填到后面high的坑
		a[high] = a[low];
	}
	//low==high跳出循环,low的位置放的是基准值pivot
	a[low] = pivot;
	return low;
}

2.1.3、三分取中法选取基数

基本思想:a[mid] <= a[low] <= a[high],使得arr[low]处于三数中的中间值,可继续使用Partition函数

int parttitionpotholing(vector<int> &a, int low, int high)
{
	//三分取中法选取基准a[mid] <= a[low] <= a[high]
	int mid = (low + high) >> 1;
	if (a[mid] > a[low])
	{
		swap(a[mid], a[low]);
	}
	if (a[low] > a[high])
	{
		swap(a[low], a[high]);
	}
	if (a[mid] > a[high])
	{
		swap(a[mid], a[high]);
	}

	//以最后一个数组元素作为pivot基准值
	int pivot = a[high];
	//挖坑填数
	while (low < high)
	{
		//从右往左找第一个比基准值小的值
		while (low < high && a[high] >= pivot)high--;
		//挖坑,将土填到前面low的坑
		a[low] = a[high];
		
		//从左往右找第一个比基准值大的值
		while (low < high && a[low] <= pivot)low++;
		//挖坑,将土填到后面high的坑
		a[high] = a[low];
	}
	//low==high跳出循环,low的位置放的是基准值pivot
	a[low] = pivot;
	return low;
}

2.2、划分partition的三种方式

划分partition: 以此基准将当前无序区划分为左、右两个较小的子区间R[low..pivotpos-1)A[pivotpos+1..high],并使左边子区间中所有记录的关键字均小于等于基准记录(不妨记为pivot)的关键字pivot.key,右边的子区间中所有记录的关键字均大于等于pivot.key,而基准记录pivot则位于正确的位置(pivotpos)上,它无须参加后续的排序。(划分实现的三种方式:挖坑法、左右指针法、快慢指针法)

2.2.1、挖坑法(固定左位置为基准+挖坑法)

基本思想:

  1. 设置两个变量low、high,排序开始的时候:low=0,high=N-1;
  2. 以第一个数组元素a[low]作为基准,赋值给pivot,即pivot=a[low];
  3. 从 high开始向前搜索,从右往左找第一个比基准值小的值,挖坑,将土填到前面low的坑;即由后开始向前搜索( high-- ),找到第一个小于 pivot 的值a[ high ],将a[high]赋值a[low];
  4. 从 low 开始向后搜索,从左往右找第一个比基准值大的值,挖坑,将土填到后面high的坑;即由前开始向后搜索( low++),找到第一个大于pivot的a[ low ],将a[low]赋值a[high];
  5. 重复第3、4步,直到 low=high,将 pivot 填入 a[low] 中

注意:当以第一个数组元素为基准数时,搜索必须先从后向前进行,当以最后一个数组元素为基准数时,搜索必须先从前向后进行。

int parttitionpotholing(vector<int> &a, int low, int high)
{
	//以第一个数组元素作为pivot基准值
	int pivot = a[low];
	//以最后一个数组元素作为pivot基准值
	//int pivot = a[high];
	//挖坑填数
	while (low < high)
	{
		//从右往左找第一个比基准值小的值
		while (low < high && a[high] >= pivot)high--;
		//挖坑,将土填到前面low的坑
		a[low] = a[high];
		
		//从左往右找第一个比基准值大的值
		while (low < high && a[low] <= pivot)low++;
		//挖坑,将土填到后面high的坑
		a[high] = a[low];
	}
	//low==high跳出循环,low的位置放的是基准值pivot
	a[low] = pivot;
	return low;
}

2.2.2、左右指针法(固定左位置为基准+左右指针法)

与挖坑法不同,这里没有坑要填,所以直接是数的交换

左指针从左往右找比基数大的数,右指针从右往左找比基数小的数

基本思想:

  1. 以第一个数组元素 a[left] 作为基准,赋值给pivot,即pivot=a[left];记住基准的指针 pointerpivot = left,最后交换时使用;
  2. 从left一直向后走,直到找到一个大于pivot的值,right从后至前,直至找到一个小于 pivot 的值,然后交换这两个数;
  3. 重复第2步,直到 left 和 right 相遇,这时将 a[pointerpivot] 放置 a[left] 的位置即可。
//左右指针法
//左指针从左往右找比基数大的数,右指针从右往左找比基数小的数
int parttitionleftright(vector<int> &a, int left, int right)
{
	//以第一个数组元素作为pivot基准值
	int pointerpivot = left;
	int pivot = a[left];
	while (left < right)
	{
		while (left < right && a[right] >= pivot)right--;
		while (left < right && a[left] <= pivot)left++;
		if (left < right)swap(a[left], a[right]);
	}
	//与挖坑法不同,这里没有坑要填,所以直接是数的交换
	swap(a[left], a[pointerpivot]);
	return left;
}

2.2.3、快慢指针法(固定左位置为基准+快慢指针法)

定义两个指针,一慢一快,慢指针找比基数小的数,快指针找比基数大的数

当以第一个数组元素为基准数时,搜索必须先从后向前进行,当以最后一个数组元素为基准数时,搜索必须先从前向后进行。

基本思想:

  1. 以最后一个数组元素 a[right] 作为基准,赋值给pivot,即 pivot = a[right];
  2.  slow 和 fast 一直向后走;slow 直到找到一个小于 pivot 的值,fast 直至找到一个大于 pivot 的值,然后交换这两个数;
  3. 重复第2步,直到 fast 大于right 越界。
//快慢指针法
//定义两个指针,一慢一快,慢指针找比基数小的数,快指针找比基数大的数
int parttitionslowfast(vector<int> &a, int left, int right)
{
	int slow = left - 1;
	int fast = left;
	//因为指针从左边移动,所以要选右边的为基准数
	//当以第一个数组元素为基准数时,搜索必须先从后向前进行,当以最后一个数组元素为基准数时,搜索必须先从前向后进行。
	int pivot = a[right];
	while (fast <= right)
	{
		//若a[fast] >= pivot ,则fast++
		//a[fast] >= pivot时,slow不动,直到fast找到小于pivot的数字,然后交换
		if ((a[fast] <= pivot) && (++slow != fast))
		{
			swap(a[slow], a[fast]);
		}
		fast++;
	}
	return slow;
}

2.3、求解调用快排的两种方式

通过递归recursive或者非递归(迭代iteration,栈stack实现递归功能),调用快速排序对左、右子区间R[low..pivotpos-1]和R[pivotpos+1..high]快速排序。

2.3.1、递归recursive

通过Partition来实现划分,并递归实现前后的划分。

//递归recursive
void quicksortrecursive(vector<int> &a, int low, int high)
{
	int pointerpivot = parttitionpotholing(a, low, high);
	//左边
	if (low < pointerpivot - 1)quicksortrecursive(a, low, pointerpivot - 1);
	//右边
	if (high > pointerpivot + 1)quicksortrecursive(a, pointerpivot + 1, high);
}

2.3.2、非递归(迭代iteration,栈stack实现递归功能)

递归的本质是栈,借助栈来保存左右两部分的首尾指针,实现非递归

非递归,迭代iteration,栈实现递归功能,通过Pritation函数划分之后分成左右两部分的首尾指针

//非递归,迭代iteration,栈实现递归功能
void quicksortiteration(vector<int> &a, int low, int high)
{
	//用栈实现递归的功能
	stack<int>s;
	//确定第一个枢轴
	int pointerpivot = parttitionslowfast(a, low, high);
	//如果枢轴左边有需要排序的子序列,压栈
	if (low < pointerpivot - 1)
	{
		s.push(low);
		s.push(pointerpivot - 1);
	}
	//如果枢轴右边有需要排序的子序列,压栈
	if (high > pointerpivot + 1)
	{
		s.push(pointerpivot + 1);
		s.push(high);
	}
	//直到栈空为止
	while (!s.empty())
	{
		//高位先出栈,计算枢轴值
		high = s.top(); s.pop();
		low = s.top(); s.pop();
		pointerpivot = parttitionslowfast(a, low, high);
		if (low < pointerpivot - 1)
		{
			s.push(low);
			s.push(pointerpivot - 1);
		}
		if (high > pointerpivot + 1)
		{
			s.push(pointerpivot + 1);
			s.push(high);
		}
	}
}

2.4、复杂度分析

2.4.1、时间复杂度

产生n的原因:调用栈的高度;产生logn的原因:完成每层的时间

最好情况O(nlogn):每次都恰好五五分,一次递归共需比较n次,递归深度为logn

最坏情况O(n^2):最坏性能(worst case behavior)和插入排序相同,也是O(n^2)。比如一个序列5,4,3,2,1,要排为1,2,3,4,5。按照快速排序方法,每次只会有一个数据进入正确顺序,不能把数据分成大小相当的两份,很明显,排序的过程就成了一个歪脖子树,树的深度为n,那时间复杂度就成了O(n^2)。尽管如此,需要排序的情况几乎都是乱序的,自然性能就保证了。据书上的测试图来看,在数据量小于20的时候,插入排序具有最好的性能。当大于20时,快速排序具有最好的性能,归并(merge sort)和堆排序(heap sort)也望尘莫及,尽管复杂度都为nlog2(n)。

平均情况O(nlogn):

2.4.2、空间复杂度

O(logn),创建栈

2.5、算法稳定性

快排是不稳定的算法因为如果出现和pivot值相同的元素,它都会被作为交换对象而移动到pivot的前面或者后面,这就出现了值相同的元素会交换顺序的问题,因而是不稳定的。

3、算法实现与优化

3.1、基础版

见4、算法示例

3.2、优化,当待排列序列的长度分割到一定大小时,使用插入排序

优化一:当待排列序列的长度分割到一定大小时,使用插入排序
原因:对于很小和部分有序的数组,快排不如插入排序好。当待排序序列的长度分割到一定大小后,继续使用分割的效率比插入排序要差; 但是三分取中+插入排序还不能处理重复数组;

代码待我整理后再上传>_>

3.3、优化,聚集相同基准法;处理重复的数组元素,减少递归的次数

优化二:聚集相同基准法;处理重复的数组元素,减少递归的次数
原因:在一次分割结束后,可以把与par相等的元素聚集在一起。下次分割时,不用在对和par相等元素进行分割

代码待我整理后再上传>_>

4、算法示例

#include "stdafx.h"
#include <vector>
#include <iostream>
#include <stack>
#include<time.h>
using namespace std;

//(划分实现的三种方式:挖坑法、左右指针法、快慢指针法)
//挖坑法
int parttitionpotholing(vector<int> &a, int low, int high)
{
	//选基准pivot的三种方式
	/*
	//随机化位置选取基准,在固定化前加一个生成任意数,
	//然后与low位置交换继续固定位置的Partition函数。
	//产生不同的随机种子
	srand((unsigned)time(NULL));
	//产生指定范围内的随机数,通用公式:a+rand() % n;
	int temp = low + rand() % (high - low + 1);
	swap(a[low], a[temp]);
	cout << '\n' << "随机化位置: " << temp << '\t' << "随机化位置选取基准值: " << a[temp] << '\n';
	*/

	/*
	//三分取中法选取基准a[mid] <= a[low] <= a[high]
	int mid = (low + high) >> 1;
	if (a[mid] > a[low])
	{
		swap(a[mid], a[low]);
	}
	if (a[low] > a[high])
	{
		swap(a[low], a[high]);
	}
	if (a[mid] > a[high])
	{
		swap(a[mid], a[high]);
	}
	cout << '\n' << "基准值: " << a[low];
	*/

	//以第一个数组元素作为pivot基准值
	int pivot = a[low];
	//以最后一个数组元素作为pivot基准值
	//int pivot = a[high];
	//挖坑填数
	while (low < high)
	{
		//从右往左找第一个比基准值小的值
		while (low < high && a[high] >= pivot)high--;
		//挖坑,将土填到前面low的坑
		a[low] = a[high];
		//输出
		cout << '\n';
		for (int i = 0; i < a.size(); i++)
		{
			if (i == high)
			{
				cout << "_" << '\t';
				continue;
			}
			cout << a[i] << '\t';
		}
		cout << '\n';
		//从左往右找第一个比基准值大的值
		while (low < high && a[low] <= pivot)low++;
		//挖坑,将土填到后面high的坑
		a[high] = a[low];
		//输出
		cout << '\n';
		for (int i = 0; i < a.size(); i++)
		{
			if (i == low)
			{
				cout << "_" << '\t';
				continue;
			}
			cout << a[i] << '\t';
		}
		cout << '\n';
	}
	//low==high跳出循环,low的位置放的是基准值pivot
	a[low] = pivot;
	return low;
}
//左右指针法
//左指针从左往右找比基数大的数,右指针从右往左找比基数小的数
int parttitionleftright(vector<int> &a, int left, int right)
{
	//以第一个数组元素作为pivot基准值
	int pointerpivot = left;
	int pivot = a[left];
	while (left < right)
	{
		while (left < right && a[right] >= pivot)right--;
		while (left < right && a[left] <= pivot)left++;
		if (left < right)swap(a[left], a[right]);
		cout << "左指针的值:" << a[left] << '\t' << "右指针的值:" << a[right] << '\n';
		for (int i = 0; i < a.size(); i++)
		{
			cout << a[i] << '\t';
		}
		cout << '\n';
	}
	//与挖坑法不同,这里没有坑要填,所以直接是数的交换
	swap(a[left], a[pointerpivot]);
	return left;
}
//快慢指针法
//定义两个指针,一慢一快,慢指针找比基数小的数,快指针找比基数大的数
int parttitionslowfast(vector<int> &a, int left, int right)
{
	int slow = left - 1;
	int fast = left;
	//因为指针从左边移动,所以要选右边的为基准数
	//当以第一个数组元素为基准数时,搜索必须先从后向前进行,当以最后一个数组元素为基准数时,搜索必须先从前向后进行。
	int pivot = a[right];
	while (fast <= right)
	{
		//若a[fast] >= pivot ,则fast++
		//a[fast] >= pivot时,slow不动,直到fast找到小于pivot的数字,然后交换
		if ((a[fast] <= pivot) && (++slow != fast))
		{
			swap(a[slow], a[fast]);
			cout << "慢指针的值:" << a[slow] << '\t' << "快指针的值:" << a[fast] << '\n';
			for (int i = 0; i < a.size(); i++)
			{
				cout << a[i] << '\t';
			}
			cout << '\n';
		}
		fast++;
	}
	return slow;
}

//求解:通过递归recursive或者非递归(迭代iteration,栈stack实现递归功能)
//递归recursive
void quicksortrecursive(vector<int> &a, int low, int high)
{
	int pointerpivot = parttitionpotholing(a, low, high);
	//左边
	if (low < pointerpivot - 1)quicksortrecursive(a, low, pointerpivot - 1);
	//右边
	if (high > pointerpivot + 1)quicksortrecursive(a, pointerpivot + 1, high);
}
//非递归,迭代iteration,栈实现递归功能
void quicksortiteration(vector<int> &a, int low, int high)
{
	//用栈实现递归的功能
	stack<int>s;
	//确定第一个枢轴
	int pointerpivot = parttitionslowfast(a, low, high);
	//如果枢轴左边有需要排序的子序列,压栈
	if (low < pointerpivot - 1)
	{
		s.push(low);
		s.push(pointerpivot - 1);
	}
	//如果枢轴右边有需要排序的子序列,压栈
	if (high > pointerpivot + 1)
	{
		s.push(pointerpivot + 1);
		s.push(high);
	}
	//直到栈空为止
	while (!s.empty())
	{
		//高位先出栈,计算枢轴值
		high = s.top(); s.pop();
		low = s.top(); s.pop();
		pointerpivot = parttitionslowfast(a, low, high);
		if (low < pointerpivot - 1)
		{
			s.push(low);
			s.push(pointerpivot - 1);
		}
		if (high > pointerpivot + 1)
		{
			s.push(pointerpivot + 1);
			s.push(high);
		}
	}
}

int main()
{
	vector<int>a;
	cout << '\n' << "原顺序: " << '\n';
	a.push_back(7); a.push_back(8); a.push_back(7);
	a.push_back(6); a.push_back(5); a.push_back(6);
	a.push_back(7); a.push_back(8); a.push_back(9);
	for (int i = 0; i < a.size(); i++)
	{
		cout << a[i] << '\t';
	}
	cout << '\n' << '\n';
	cout << '\n' << "排序过程: " << '\n';
	int low = 0;
	int high = a.size() - 1;
	quicksortiteration(a, low, high);
	cout << '\n' << "快排: " << '\n';
	for (int i = 0; i < a.size(); i++)
	{
		cout << a[i] << '\t';
	}
	cout << '\n';
	return 0;
}

5、注意

猜你喜欢

转载自blog.csdn.net/qq_35683407/article/details/105901223