快速排序(Quick Sort)
原理解析
对于要排序的区间,先定一个基准数p(基准数可随机指定,可指定区间两端,或者区间中心的数为基准数)。然后将区间中小于p的数放到p之前,大于p的数放在p之后,最后使用分治策略,分别对p之前的子区间和p之后的子区间递归并排序。
该算法平均时间复杂度 O ( N × l o g N ) O(N\times log N) O(N×logN),最坏 O ( N 2 ) O(N^2) O(N2)。
有多种方法可以实现上述以p为界的区间划分,下面以升序排序为例介绍一种较为常用的方法:
-
新建两个指针,分别指向区间的左右两端。并取区间左端的值为基准数p(此时视为将p抽离原区间,区间最左端为空)。
-
右指针左移,直到找到比p小的数,然后将该数填充至左指针指向位置(也就是区间最左端空置的位置)。
-
左指针右移,直到找到比p大的数,然后将该数填充至右指针指向位置(原来右指针的指向的值在步骤2中被移动到区间最左端,此时其指向的位置也可以视为空置)。
-
重复步骤2、3,直到两指针相交或越界,此时将p放置于左指针所指位置。
-
遍历完整个区间后,对p这单个元素的排序已完成,p此时被放置到未来数组有序时p应该处于的位置。在p前面的数都比它小,在p后面的数都比它大,此时开始分治递归p之前的区间和p之后的区间,重复1至5步。
代码
void QuickSort(int* a, int lef, int rig){
if (lef >= rig)
return;
int p = a[lef], i = lef, j = rig;
while (i < j) {
while (i<j && a[j]>=p)
j--;
a[i] = a[j];
while (i<j && a[i]<=p)
i++;
a[j] = a[i];
}
a[i] = p;
QuickSort(a, lef, i-1);
QuickSort(a, i+1, rig);
}
进一步的优化
-
小区间优化:若待排序数组较小(N<100),使用快速排序效率可能不如选择排序,因此可加入条件判断,在数组较小时使用选择排序。
-
非递归:使用数组模拟栈,来实现非递归版本的快速排序。
-
三数取中:对于本身有序的数组,每次取最左或最右值为基准数进行快速排序会导致每次递归仅能比上次递归的区间少1,从而进行n层递归,时间复杂度劣化至 O ( N 2 ) O(N^2) O(N2)。因此,可考虑取最左、最右以及中间这三数中,数值处于中间的为基准数进行排序。
快速排序稳定化改造
上述方法可较快地实现以p为界的区间划分,但无法保证排序的稳定性。下面介绍一种稳定化的快速排序方法:
对于给定的区间,确定基准数p后,新建两个数组a1和a2,其中a1用于存储区间中所有比p小的数,a2存储所有比p大的数,对于原区间中与p相等的数,若其位置在p之前,则按位置顺序先后存入a1,若位置在p之后,则按位置顺序先后存入a2。然后按a1, p, a2的顺序将三者覆盖回原来的区间,之后同样用分治策略递归排序a1部分和a2部分。
void QuickSort(int* a, int lef, int rig){
if (lef >= rig)
return;
int n = rig-lef+1;
int a1[n], a2[n], n1 = 0, n2 = 0;
int mid = lef+(rig-lef)/2;
int p = a[mid];
for (int i=lef; i<=rig; i++)
if (i != mid) {
if (a[i]<p)
a1[n1++] = a[i];
else if (a[i]>p)
a2[n2++] = a[i];
else {
if (i<mid)
a1[n1++] = a[i];
else
a2[n2++] = a[i];
}
}
for (int i=lef; i<lef+n1; i++)
a[i] = a1[i-lef];
a[lef+n1] = p;
for (int i=rig-n2+1; i<=rig; i++)
a[i] = a2[i-rig+n2-1];
QuickSort(a, lef, lef+n1-1);
QuickSort(a, rig-n2+1, rig);
}
Quick Select算法
一种用于从无序数组中找第k大(小)元素的算法。由于快速排序中,对区间扫描后,基准数会被放置到正确的位置,其前面的数都比它小,后面的数都比它大,基于该性质,Quick Select的算法步骤如下:(假设要找第k小的元素)
-
和快速排序一样,在给定区间中确定基准数p,并以同样的方式扫描区间并移动元素,令p前面的数都比它小,在p后面的数都比它大。
-
假设此时p前面有n1个元素,p之后有n2个元素,如果n1=k-1,说明基准数即为目标;如果n1>k-1,则说明目标元素在p之前,因此重复1、2两步,在前n1个元素中递归寻找第k小元素;如果n1<k-1,说明目标元素在p之后,因此重复1、2两步,在后面n2个元素中递归寻找第k-n1-1小元素;
该算法平均时间复杂度 O ( N ) O(N) O(N),最坏 O ( N × l o g N ) O(N\times log N) O(N×logN)。
int QuickSelect(int* a, int lef, int rig, int k){
int p = a[lef], i = lef, j = rig;
while (i < j) {
while (i<j && a[j]>=p)
j--;
a[i] = a[j];
while (i<j && a[i]<=p)
i++;
a[j] = a[i];
}
a[i] = p;
int n1 = i - lef;
if (n1 == k-1)
return p;
if (n1 > k-1)
return QuickSelect(a, lef, i-1, k);
return QuickSelect(a, i+1, rig, k-n1-1);
}