快速排序是分治策略的一个应用。
算法思想就是基准元素的划分,这点可以参考书上的分析。
主要给出快速排序算法的分析:
快速排序算法的时间主要耗费在划分操作上,并与划分是否平衡密切相关。对于长度为n的待排序序列,一次划分算法Partition需要堆整个待排序序列扫描一遍,其所需要的计算时间显然为O(n)。
下面从三种情况来讨论一下快速排序算法的时间复杂度。
(1)最坏时间复杂度
最坏情况是每次划分选取的基准元素都是在当前待排序序列中的最小(或最大)元素,划分的结果所基准元素左边的子序列为空(或右边的子序列为空),而划分所得的另一个非空的子序列中元素个数仅仅比划分前的排序序列中元素个数少1个。
在这样的情况下,快速排序算法必须做n-1次划分,那么算法的运行时间T(n)的递归形式为:
O(1) n = 1
T(n) =
T(n-1) + O(n) n > 1
因此,快速排序算法的最坏时间复杂度为O(n*(n+1)/2)=O(n*n)
(2)最好时间复杂度
在最好情况下,每次划分所去的基准元素都是当前待排序序列的“中间值”,划分的结果所基准元素的左右两个子序列的长度大致相同,此时,算法的运行时间T(n)的递归形式为:
O(1) n = 1
T(n) =
2 T(n/2) + O(n) n > 1
解出后得到最好时间复杂度为O(nlogn).
(3)平均时间复杂度
在平均情况下,采用归纳法可求得T(n)的数量级也为O(nlogn).
尽管快速排序的最坏时间复杂度为n*n,但是就平均性能来说,它是基于元素比较的内部排序算法中速度最快的,因此得名:Quick_Sort
快速排序的空间复杂度
因为快速排序采用递归执行,所以需要一个栈来存放每一层递归调用的必要信息,其最大容量与递归调用的深度一致。最好情况下,若每次划分都比较均匀,则递归树的高度为O(logn),故递归需要栈空间为O(logn)。最坏情况下,递归树的高度为O(n),所需要的栈空间为O(n)。平均情况下,所需要的栈空间为O(logn)。
给出测试代码:
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
using namespace std;
const int N = 10;
int arr[N];
/******************快速排序稳定版本*************************************/
void QuickSort(int low, int high)
{
if(low >= high)
return ;
int i, j, pivot;
i = low, j = high, pivot = arr[(high + low) >> 1]; //每次取中间元素作为基准
swap(arr[low], arr[(high + low) >> 1]);
while(i < j)
{
while(i < j && arr[j] >= pivot) --j;
arr[i] = arr[j];
while(i < j && arr[i] <= pivot) ++i;
arr[j] = arr[i];
}
arr[i] = pivot;
QuickSort(low, i - 1); //递归左边
QuickSort(j + 1, high); //递归右边
}
/*************************************************************************/
/******************快速排序一般版本(可求第K大元素)*****************/
int Partition(int low, int high)
{
int i, j, pivot;
i = low, j = high, pivot = arr[low];
while(i < j)
{
while(i < j && arr[j] >= pivot) --j;
if(i < j) swap(arr[i++], arr[j]);
while(i < j && arr[i] <= pivot) ++i;
if(i < j) swap(arr[i], arr[j--]);
}
return j; //返回基准元素位置
}
void Quick_Sort(int low, int high)
{
int pivotpos;
if(low < high)
{
pivotpos = Partition(low, high);
Quick_Sort(low, pivotpos - 1); //基准左递归排序
Quick_Sort(pivotpos + 1, high); //基准右递归排序
}
}
int select(int low, int high, int k)
{
int pivotpos, num;
if(low == high)
return arr[high];
pivotpos = Partition(low, high);
num = pivotpos - low + 1; //左边元素个数
if(k == num)
return arr[pivotpos];
else if(k < num)
return select(low, pivotpos - 1, k);
else
return select(pivotpos + 1, high, k - num);
}
/*************************************************************************/
int main()
{
int k;
cout<<"输入10个数字:"<<endl;
for(int i = 0; i < 10; ++i)
scanf("%d", &arr[i]);
QuickSort(0, 9);
cout<<"快速排序后:"<<endl;
for(int i = 0; i < 10; ++i)
cout<<arr[i]<<" ";
cout<<endl;
cout<<"输入第K大元素:"<<endl;
cin>>k;
cout<<select(0, 9, k)<<endl;
return 0;
}
快速排序稳定版本的思想:
每次取待排序数组的中间元素作为基准,满足排序完成后中间元素左边元素都比它小,右边都比它大。(很稳定,每次基准元素都是中间元素)
我们先取出基准元素 pivot = arr[(high + low) >> 1]; 这样相当于中间位置已空, 因为我们开始从右边找第一个比基准元素小的值并放在左边,而左边初始化没有空余位置,为了防止覆盖元素造成错误,我们将第一个元素和基准元素换一下位置,这实际上相当于我们把arr[low]空出来了(因为我们已经用pivot把基准元素取出来了),这时,数组相当于N-1个元素,arr[low]相当于空,这样,我们在右边找到第一个小于基准元素的时候,就可以把它放在arr[low](覆盖也没有问题,应该理解吧?),这样,右边找到的那个元素的位置又空出来了,接下来我们从左边找第一个比基准元素大的元素,找到后放在刚才右边空出来的位置,这样循环下去,最后肯定是low和high相遇,这时退出后,数组中相当于还是N-1个元素,我们只需要把中间元素用pivot赋值就可以了,因为这次调整就是以它为标准的。。。。这样就得到了一个序列(不是最终结果),保证中间那个基准元素的左边都比它小,右边都比它大。之后,我们递归基准元素的左边进行同样的调整,再递归基准元素的右边进行同样的调整。。当全部完成后,就得到了正确的排序结果。。。。。。。OVER~~~~