快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。
对于待排序序列,使用某种划分方法排好一个值key,使得key前面的所有值都比key小,key后面的所有值都比key大(升序),从而以key为界限分成了左右两个待排序的子序列,然后分别对子序列进行上述步骤。
框架分为两种:递归和非递归
某种划分方法分为三种:Hoare、挖坑法、前后指针法
void QuickSort(int* a, int left, int right){
if(left >= right){
return;
}
//对待排序序列使用划分方法,得到基准值的下标keyi
int keyi = QuickSortPart(a, left, right);//划分有三种方法
// 划分成功后以keyi为边界形成了左右两部分 [left, keyi - 1] 和 [keyi + 1, right]
// 递归排[left, keyi - 1]
QuickSort(a, left, keyi - 1);
// 递归排[keyi + 1, right]
QuickSort(a, keyi + 1, right);
}
递归的本质实际就是在控制待排序序列的区间,而非递归版本就是用栈模拟递归去保存和使用待排序序列的区间
//使用栈的前序遍历模拟递归
void QuickSortNoR(int* a, int left, int right){
Stack obj;
StackInit(&obj);
StackPush(&obj, right);//将待排序序列的左右区间入栈
StackPush(&obj, left);
while(!StackEmpty(&obj)){
int left = StackTop(&obj);//取出待排序序列的左右区间
StackPop(&obj);
int right = StackTop(&obj);
StackPop(&obj);
int keyi = QuickSortPart(a, left, right);//排序区间内的数据,得到基准值的下标keyi
StackPush(&obj, right);//将基准值右边的区间入栈
StackPush(&obj, keyi + 1);
StackPush(&obj, keyi - 1);//将基准值左边的区间入栈
StackPush(&obj, left);
}
StackDestroy(&obj);
}
int QuickSortPart(int* a, int left, int right){
int keyi = left;
while(left < right){
while(left < right && a[right] >= a[keyi]){
right--;
}
while(left < right && a[left] <= a[keyi]){
left++;
}
if(left < right){
Swap(&a[left], &a[right]);
}
}
int meeti = left;
Swap(&a[keyi], &a[meeti]);
return meeti;
}
因为只有先从右边开始走,才能够保证相遇位置meeti比keyi的值小,原因如下:
int QuickSortPart(int* a, int left, int right){
int key = a[left];
int hole = left;
while(left < right){
while(left < right && a[right] >= key){
right--;
}
a[hole] = a[right];
hole = right;
while(left < right && a[left] <= key){
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
int QuickSortPart(int* a, int left, int right){
int keyi = left;
int prev = left;
int cur = left + 1;
while(cur <= right){
if(a[cur] < a[keyi] && ++prev != cur){
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
快速排序最理想的状态也就是最优的时间复杂度为 O ( N ∗ l o g N ) O(N*log^{N}) O(N∗logN),而达到这一目的的最好的情况是每次分区时,都几乎是从中间分区的,这样才能够达到把 N N N个数据分为 l o g N log^{N} logN层,而如果当待排序序列是有序时,则快速排序会把 N N N个数据分为 N N N层,就会使得时间复杂度变为 O ( N 2 ) O(N^{2}) O(N2),这是非常糟糕的,所以为了减少这种情况,出现了一个优化函数。
该函数的功能是取到序列的第一个数、中间的数、最后一个数,然后比较大小,返回大小为中间的那个数的下标。
下一步就是将该下标的值与序列的第一个值交换,这样就会造成,假设之前的序列是有序的,而现在的序列的第一个值会是序列中间的那个值,从而以该值为key分区时能够减少只分出一个区的情况,即减少把N个数据分为N层的情况。
int GetMidIndex(int* a, int begin, int end){
int mid = begin + ((end - begin) >> 1);
if(a[begin] <= a[mid]){
if(a[mid] <= a[end]){
return mid;
}
else if(a[begin] <= a[end]){
return end;
}
else{
return begin;
}
}
else{
if(a[begin] <= a[end]){
return begin;
}
else if(a[mid] <= a[end]){
return end;
}
else{
return mid;
}
}
}
该函数这样使用(以Hoare法举例)
int QuickSortPart(int* a, int left, int right){
int mid = GetMidIndex(a, left, right);
Swap(&a[left], &a[mid]);
//上面是优化,其它两种分区方法也是这样使用优化
int keyi = left;
while(left < right){
while(left < right && a[right] >= a[keyi]){
right--;
}
while(left < right && a[left] <= a[keyi]){
left++;
}
if(left < right){
Swap(&a[left], &a[right]);
}
}
int meeti = left;
Swap(&a[keyi], &a[meeti]);
return meeti;
}
快速排序的理想状态就是将 N N N个数据分成 l o g N log^{N} logN层,达到近似完全二叉树的形状,而完全二叉树的最后三层递归将会达到整个递归过程中的87.5%,而最后的三层递归也就最多只有8个数据,与其让这8个数据递归这么多次还不如直接进行插入排序,这样将会减少巨额的递归次数,从而优化排序的速度。
void QuickSort(int* a, int left, int right){
if(left >= right){
return;
}
if(right - left + 1 <= 8){//当数据小于等于8时,不再进行递归了,改为插入排序
Insert(a + left, right - left + 1);
}
else{//当数据量大于8时,才去递归
int keyi = QuickSortPart(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
}
时间复杂度
最优时间复杂度 O ( N ∗ l o g N ) O(N*log^{N}) O(N∗logN)
最差时间复杂度 O ( N 2 ) O(N^{2}) O(N2)
空间复杂度
最优空间复杂度 O ( l o g N ) O(log^{N}) O(logN)
最差时间复杂度 O ( N ) O(N) O(N)
递归主要消耗在栈空间,非递归主要消耗在堆上
稳定性
快速排序是不稳定的