快速排序(Quick Sort)
快速排序(Quick Sort)是对冒泡排序的一种改进。快排的真谛在于极端情况下每次将概率等分 1/2 每次小于这个数的放在前面大于的放在后面,即每次排序都找出了一个正确位置,使得下一次排序个数减少一半。快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分的值均比另一部分的值小。之后分别对这两部分递归或者非递归的方式继续进行快速排序,以达到整个序列有序的目的。
目录
2.3.2、非递归(迭代iteration,栈stack实现递归功能)
3.2、优化,当待排列序列的长度分割到一定大小时,使用插入排序
3.3、优化,聚集相同基准法;处理重复的数组元素,减少递归的次数
1、算法描述
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。快排的真谛在于 极端情况下每次将概率等分 1/2 每次小于这个数的放在前面 大于的放在后面 即每次排序都找出了一个正确位置,使得下一次排序个数减少一半。
分治法的基本思想是:将原问题分解为若干个规模更小但结构与原问题相似的子问题sub。递归地解这些子问题,然后将这些子问题的解组合为原问题的解。
- 选基准pivot:在A[low..high]中任选一个记录作为基准(Pivot)(选取基准的三种方式:固定位置、随机化、三分取中)
- 划分partition: 以此基准将当前无序区划分为左、右两个较小的子区间R[low..pivotpos-1)和A[pivotpos+1..high],并使左边子区间中所有记录的关键字均小于等于基准记录(不妨记为pivot)的关键字pivot.key,右边的子区间中所有记录的关键字均大于等于pivot.key,而基准记录pivot则位于正确的位置(pivotpos)上,它无须参加后续的排序。(划分实现的三种方式:挖坑法、左右指针法、快慢指针法)
- 求解:通过递归recursive或者非递归(迭代iteration,栈stack实现递归功能),调用快速排序对左、右子区间R[low..pivotpos-1]和R[pivotpos+1..high]快速排序。
- 组合:因为当"求解"步骤中的两个递归调用结束时,其左、右两个子区间已有序。对快速排序而言,"组合"步骤无须做什么,可看作是空操作。
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、挖坑法(固定左位置为基准+挖坑法)
基本思想:
- 设置两个变量low、high,排序开始的时候:low=0,high=N-1;
- 以第一个数组元素a[low]作为基准,赋值给pivot,即pivot=a[low];
- 从 high开始向前搜索,从右往左找第一个比基准值小的值,挖坑,将土填到前面low的坑;即由后开始向前搜索( high-- ),找到第一个小于 pivot 的值a[ high ],将a[high]赋值a[low];
- 从 low 开始向后搜索,从左往右找第一个比基准值大的值,挖坑,将土填到后面high的坑;即由前开始向后搜索( low++),找到第一个大于pivot的a[ low ],将a[low]赋值a[high];
- 重复第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、左右指针法(固定左位置为基准+左右指针法)
与挖坑法不同,这里没有坑要填,所以直接是数的交换
左指针从左往右找比基数大的数,右指针从右往左找比基数小的数
基本思想:
- 以第一个数组元素 a[left] 作为基准,赋值给pivot,即pivot=a[left];记住基准的指针 pointerpivot = left,最后交换时使用;
- 从left一直向后走,直到找到一个大于pivot的值,right从后至前,直至找到一个小于 pivot 的值,然后交换这两个数;
- 重复第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、快慢指针法(固定左位置为基准+快慢指针法)
定义两个指针,一慢一快,慢指针找比基数小的数,快指针找比基数大的数
当以第一个数组元素为基准数时,搜索必须先从后向前进行,当以最后一个数组元素为基准数时,搜索必须先从前向后进行。
基本思想:
- 以最后一个数组元素 a[right] 作为基准,赋值给pivot,即 pivot = a[right];
- slow 和 fast 一直向后走;slow 直到找到一个小于 pivot 的值,fast 直至找到一个大于 pivot 的值,然后交换这两个数;
- 重复第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;
}