1.算法描述
快速排序(Quicksort),最早由东尼·霍尔基于分治思想提出的排序算法,现在各种语言中自带的排序库很多使用的都是快速排序。
快速排序的基本思想:通过一次排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
-
最基础的算法步骤:使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
-
从数列中挑出一个元素,称为 “基准”(pivot);
-
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
-
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
-
动态图演示
-
算法优化思路
-
对于小规模数组进行插入排序优化
-
在数据近乎有序时,递归树的平衡度差会造成算法复杂度退化成 O ( n 2 ) O_{(n2)} O(n2),利用随机选取元素进行排序的方法优化
-
重新排序数列时相同的数可以到任一边,这种操作会使得算法遇到大量重复元素的排序时需要重复操作,优化:双路快速排序&三路快速排序
2.算法实现及优化
- 三种QuickSort算法实现
//Author:cheng.jiang
#ifndef QUICK_SORT_H
#define QUICK_SORT_H
#include <iostream>
#include "InsertionSort.h"
using namespace std;
// 将arr[l...mid]和arr[mid+1...r]两部分进行partition
// 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
template<typename T>
int __partition(T arr[], int l, int r){
//优化2
swap(arr[l],arr[rand()%(r-l+1)+l]);
T v = arr[l];
int j=l;//初始化j
for( int i= l; i <= r ; i++){
if(arr[i]<v){
swap(arr[j+1],arr[i]);//交换位置
j++;
}
}
swap(arr[l],arr[j]);
return j;
}
// 递归使用快速排序,对arr[l...r]的范围进行排序
template<typename T>
void __quickSort(T arr[], int l, int r){
// 优化1
if( r - l <= 15 ){
insertionSort(arr,l,r);
return;
}
int p = __partition(arr, l, r);
__quickSort(arr, l ,p-1);
__quickSort(arr, p+1, r);
}
//定义quickSort()函数
template<typename T>
void quickSort(T arr[], int n){
//优化2,随机化pivot标定元素
srandom(time(NULL));
__quickSort( arr , 0 , n-1 );
}
// 双路快速排序的partition
// 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
template <typename T>
int _partition2Ways(T arr[], int l, int r){
// 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
swap( arr[l] , arr[rand()%(r-l+1)+l] );
T v = arr[l];
// arr[l+1...i) <= v; arr(j...r] >= v
int i = l+1, j = r;
while( true ){
// 注意这里的边界, arr[i] < v, 不能是arr[i] <= v
while( i <= r && arr[i] < v )
i ++;
// 注意这里的边界, arr[j] > v, 不能是arr[j] >= v
while( j >= l+1 && arr[j] > v )
j --;
if( i > j )
break;
swap( arr[i] , arr[j] );
i ++;
j --;
}
swap( arr[l] , arr[j]);
return j;
}
// 对arr[l...r]部分进行快速排序
template <typename T>
void _quickSort2Ways(T arr[], int l, int r){
// 对于小规模数组, 使用插入排序进行优化
if( r - l <= 15 ){
insertionSort(arr,l,r);
return;
}
// 调用双路快速排序的partition
int p = _partition2Ways(arr, l, r);
_quickSort2Ways(arr, l, p-1 );
_quickSort2Ways(arr, p+1, r);
}
template <typename T>
void quickSort2Ways(T arr[], int n){
srand(time(NULL));
_quickSort2Ways(arr, 0, n-1);
}
// 递归的三路快速排序算法
template <typename T>
void __quickSort3Ways(T arr[], int l, int r){
// 对于小规模数组, 使用插入排序进行优化
if( r - l <= 15 ){
insertionSort(arr,l,r);
return;
}
// 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
swap( arr[l], arr[rand()%(r-l+1)+l ] );
T v = arr[l];
int lt = l; // arr[l+1...lt] < v
int gt = r + 1; // arr[gt...r] > v
int i = l+1; // arr[lt+1...i) == v
while( i < gt ){
if( arr[i] < v ){
swap( arr[i], arr[lt+1]);
i ++;
lt ++;
}
else if( arr[i] > v ){
swap( arr[i], arr[gt-1]);
gt --;
}
else{ // arr[i] == v
i ++;
}
}
swap( arr[l] , arr[lt] );
__quickSort3Ways(arr, l, lt-1);
__quickSort3Ways(arr, gt, r);
}
template <typename T>
void quickSort3Ways(T arr[], int n){
srand(time(NULL));
__quickSort3Ways( arr, 0, n-1);
}
#endif //QUICK_SORT_H
双路快速排序中: while( j >= l+1 && arr[j] > v ) while( i <= r && arr[i] < v )不取等号的原因分析:
a. 对于arr[i]<v和arr[j]>v的方式,第一次partition得到的分点是数组中间;
b. 对于arr[i]<=v和arr[j]>=v的方式,第一次partition得到的分点是数组的倒数第二个。
这是因为对于连续出现相等的情况,a方式会交换i和j的值;而b方式则会将连续出现的这些值归为其中一方,使得两棵子树不平衡
- 测试代码
#include <iostream>
#include "SortTestHelper.h"
#include "InsertionSort.h"
#include "MergeSort.h"
#include "QuickSort.h"
using namespace std;
int main(){
int n = 50000;
// 测试1 测试随机数组
cout<<"Test for random array, size = "<<n<<", random range [0, "<<n<<"]"<<endl;
int* arr1 = SortTestHelper::generateRandomArray(n,0,n);
int* arr2 = SortTestHelper::copyIntArray(arr1, n);
int* arr3 = SortTestHelper::copyIntArray(arr1, n);
int* arr4 = SortTestHelper::copyIntArray(arr1, n);
int* arr5 = SortTestHelper::copyIntArray(arr1, n);
int* arr6 = SortTestHelper::copyIntArray(arr1, n);
SortTestHelper::testSort("Insertion Sort", insertionSort, arr1, n);
SortTestHelper::testSort("Merge Sort", mergeSort, arr2, n);
SortTestHelper::testSort("Merge Sort 2", mergeSort2, arr3, n);
SortTestHelper::testSort("Quick Sort", quickSort, arr4, n);
SortTestHelper::testSort("Quick Sort 2 Ways", quickSort2Ways, arr5, n);
SortTestHelper::testSort("Quick Sort 3 Ways", quickSort3Ways, arr6, n);
delete[] arr1;
delete[] arr2;
delete[] arr3;
delete[] arr4;
delete[] arr5;
delete[] arr6;
cout<<endl;
// 测试2 测试近乎有序的数组
int swapTimes = 10;
assert( swapTimes >= 0 );
cout<<"Test for nearly ordered array, size = "<<n<<", swap time = "<<swapTimes<<endl;
arr1 = SortTestHelper::generateNearlyOrderedArray(n,swapTimes);
arr2 = SortTestHelper::copyIntArray(arr1, n);
arr3 = SortTestHelper::copyIntArray(arr1, n);
arr4 = SortTestHelper::copyIntArray(arr1,n);
arr5 = SortTestHelper::copyIntArray(arr1,n);
arr6 = SortTestHelper::copyIntArray(arr1,n);
SortTestHelper::testSort("Insertion Sort", insertionSort, arr1, n);
SortTestHelper::testSort("Merge Sort", mergeSort, arr2, n);
SortTestHelper::testSort("Merge Sort 2", mergeSort2, arr3, n);
SortTestHelper::testSort("Quick Sort", quickSort, arr4, n);
SortTestHelper::testSort("Quick Sort 2 Ways", quickSort2Ways, arr5, n);
SortTestHelper::testSort("Quick Sort 3 Ways", quickSort3Ways, arr6, n);
delete[] arr1;
delete[] arr2;
delete[] arr3;
delete[] arr4;
delete[] arr5;
delete[] arr6;
cout<<endl;
// 测试3 测试存在包含大量相同元素的数组
cout<<"Test for random array, size = "<<n<<", random range [0,10]"<<endl;
arr1 = SortTestHelper::generateRandomArray(n,0,10);
arr2 = SortTestHelper::copyIntArray(arr1, n);
arr3 = SortTestHelper::copyIntArray(arr1, n);
arr4 = SortTestHelper::copyIntArray(arr1,n);
arr5 = SortTestHelper::copyIntArray(arr1,n);
arr6 = SortTestHelper::copyIntArray(arr1,n);
SortTestHelper::testSort("Insertion Sort", insertionSort, arr1, n);
SortTestHelper::testSort("Merge Sort", mergeSort, arr2, n);
SortTestHelper::testSort("Merge Sort 2", mergeSort2, arr3, n);
// 在包含大量重复元素的情况下, QuickSort会退化成O(n^2)算法, 在这里不做执行
//SortTestHelper::testSort("Quick Sort", quickSort, arr4, n);
SortTestHelper::testSort("Quick Sort 2 Ways", quickSort2Ways, arr5, n);
SortTestHelper::testSort("Quick Sort 3 Ways", quickSort3Ways, arr6, n);
delete[] arr1;
delete[] arr2;
delete[] arr3;
delete[] arr4;
delete[] arr5;
delete[] arr6;
return 0;
}
- 实验结果
3.算法分析
快速排序是一种原地排序,只需要一个很小的栈作为辅助空间,空间复杂度为
O
(
l
o
g
2
n
)
O_(log2n)
O(log2n),所以适合在数据集比较大的时候使用。
时间复杂度比较复杂,最好的情况是
O
(
n
)
O_{(n)}
O(n),最差的情况是
O
(
n
2
)
O_{(n2)}
O(n2),所以平时说的
O
(
n
l
o
g
n
)
O_(nlogn)
O(nlogn),为其平均时间复杂度。对于随机数组、近乎有序的数组和大量重复元素的数组,三种QuickSort的性能各有不同。