快速排序也是一种复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)的排序算法,但它的效率比同等复杂的排序算法还要高,所以在实践中会经常使用。
原理
快速排序的思想是将数组分为两个部分,一部分小于
v
v
v,另一部分大于
v
v
v,这个
v
v
v也称为基准数,一般选取数组第一个数作为基准数。它的实现效果是这样的,当分为两个数组后,每个单独的数组又可以使用该方法排序,直到只有最后一个元素,那这里又可以使用递归方法了。我们将数组分为两部分的过程称为partition。
partition的实现思路如下:假设第一个元素为基准点,并将其索引记为
l
l
l,此时已经有一部分数据进行了整理,得到了
<
v
<v
<v,和
>
v
>v
>v的部分,规定
a
r
r
[
l
+
1...
j
]
<
v
arr[l+1...j]<v
arr[l+1...j]<v,
a
r
r
[
j
+
1...
i
−
1
]
>
v
arr[j+1...i-1]>v
arr[j+1...i−1]>v,
i
i
i表示遍历中当前元素索引。
如果
e
>
v
e>v
e>v就很简单了,直接将索引值向后移动一位即可
如果
e
<
v
e<v
e<v,稍微麻烦一点,将
>
v
>v
>v部分的第一个数据与
e
e
e交换,同时
j
+
+
,
i
+
+
j++,i++
j++,i++即可。
遍历完成后,将
v
v
v与
a
r
r
[
j
]
arr[j]
arr[j]交换,
v
v
v也就放在了正确的位置。
此时
a
r
r
[
l
.
.
.
j
−
1
]
<
v
arr[l...j-1]<v
arr[l...j−1]<v,
a
r
r
[
j
+
1...
i
]
>
v
arr[j+1...i]>v
arr[j+1...i]>v,
a
r
r
[
j
]
=
v
arr[j]=v
arr[j]=v,接下来对两部分数组分别排序即可。
梳理以上过程,可得快排的实现步骤:
1.partition将数组正确分为两部分
2.对两部分递归partition,直至递归结束。
实现
#include <iostream>
#include <algorithm>
template <typename T>
int partition(T arr[], int l, int r){
T v = arr[l];
//arr[l+1,j]<v,arr[j+1,i)>v
int j=l;
for(int i=l+1; i<=r; i++){ //注意是<=r,而不是<r
if(arr[i]<v){
swap(arr[i],arr[j+1]);
j++;
}
}
swap(arr[j],arr[l]);
return j;
}
template <typename T>
void __quickSort(T arr[], int l, int r){
if(l>=r)
return;
int p = partition(arr, l ,r);
__quickSort(arr, l, p-1);
__quickSort(arr, p+1, r);
}
template <typename T)
void quickSort(T arr[], int n){
__quickSort(arr, 0, n-1);
}
优化
首先,对于小规模数组排序可以采用插入排序,提高小规模数组排序运行速度。具体代码只需要将递归结束的条件改为插入排序。
template <typename T>
void __quickSort(T arr[], int l, int r){
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);
}
但这只是一个小小的修改,此时的快速排序有一个致命的弱点会使时间复杂度变为
O
(
n
2
)
O(n^2)
O(n2)。这个问题就出现在解决近乎有序数组上,如果一个有序数组,快速排序每次选择第一个元素作为基准值,在partition操作后基准值的位置几乎没有移动,那就相当于
n
n
n个数据被分为1 和
n
−
1
n-1
n−1个,那么到最后就需要递归n次,同时partition的操作时间复杂度也是
O
(
n
)
O(n)
O(n),此时的复杂度就退化为
O
(
n
2
)
O(n^2)
O(n2)了。所以如果能够尽量选择中间的元素,那么复杂度就能够达到
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)。
为了解决这个问题,我们可以随机选择一个数作为基准数,这样就可以极大的减小每次partition后数组的不均衡性。此时在最差的情况下快排的时间复杂度仍然为
O
(
n
2
)
O(n^2)
O(n2),但是这种概率是非常低的,它的时间复杂度期望为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)。
#include <iostream>
#include <algorithm>
#include <ctime>
template <typename T>
int partition(T arr[], int l, int r){
swap(arr[l],arr[rand()%(r-l+1)+l);
T v = arr[l];
//arr[l+1,j]<v,arr[j+1,i)>v
int j=l;
for(int i=l+1; i<=r; i++){ //注意是<=r,而不是<r
if(arr[i]<v){
swap(arr[i],arr[j+1]);
j++;
}
}
swap(arr[j],arr[l]);
return j;
}
template <typename T>
void __quickSort(T arr[], int l, int r){
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);
}
template <typename T)
void quickSort(T arr[], int n){
srand(time(NULL));
__quickSort(arr, 0, n-1);
}
参考:https://coding.imooc.com/learn/list/71.html