快速排序
上篇文章介绍了时间复杂度为O(nlgn)的归并排序,本篇文章介绍时间复杂度同样为O(nlgn)但是排序速度比合并排序更快的快速排序(Quick Sort)。
快速排序也是一种采用分治法
解决问题的一个典型应用。它将一个数组分成两个子数组,将两部分独立地排序。
归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序;而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了。
在归并排序中,一个数组被等分
成两半,递归调用发生在处理整个数组之前
;而快速排序中,切分的位置取决于数组的内容
,递归调用发生在处理整个数组之后
。
快速排序的基本思想如下:
- 对数组进行随机化。
- 从数列中取出一个数作为中轴数(pivot)。
- 将比这个数大的数放到它的右边,小于或等于它的数放到它的左边。
- 再对左右区间重复第三步,直到各区间只有一个数。
一 切分
切分操作 (partition):
- 获取中轴元素 pivot
- i 从左至右扫描,如果小于等于基准元素,则 i 自增,否则记下 a[i]
- j 从右至左扫描,如果大于等于基准元素,则 i 自减,否则记下 a[j]
- 交换 a[i] 和 a[j]
- 重复这一步骤直至 i 和 j 交错,然后将基准元素和 a[j] 交换,返回 j 即可。
下面来看某一次划分的步骤(以中轴数进行划分,左边都不大于这个中轴数,右边都不小于该中轴数),如下图:
//切分 选取中轴数
public int partition(int[] a, int low, int high){
int i = low, j = high + 1; //左右扫描指针
int pivot = a[low]; //切分元素
while (true){
while (a[++i] <= pivot){
if (i == high) break; //如果扫描到了最右端,退出循环
}
while (a[--j] >= pivot){
if (j == low) break; //如果扫描到了最左端,退出循环
}
if (i >= j) break; //如果相遇,退出循环
swap(a, i, j); //交换左a[i],a[j]右两个元素,交换完后他们都位于正确的分区
}
swap(a, low, j); //a[j]比a[low]小,a[i]比a[low]大,所以将基准元素与a[j]交换,将基准元素放入正确的位置
return j;
}
public void swap(int[] a, int i, int j){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
切分前后,元素在序列中的分布如下图:
二 完整实现
与归并算法基于归并(merge)
这一过程一样,快速排序基于切分(Partition)
这一过程。
我们就是通过递归地调用切分Partition来排序的。只需要递归调用Partition这一操作,每一次以Partition返回的元素位置来划分为左右两个子序列,然后继续这一过程直到子序列长度为1(low=high)
public class quickSort {
public void sort(int[] a){
sort(a, 0, a.length - 1);
}
public void sort(int[] a, int low, int high){
if (low < high){
int mid = partition(a, low, high); //切分
sort(a, low, mid - 1); //将左半部分排序
sort(a, mid + 1, high); //将右半部分排序
}
}
public int partition(int[] a, int low, int high){
int i = low, j = high + 1;
int pivot = a[low];
while (true){
while (a[++i] <= pivot)
if (i == high) break;
while (a[--j] >= pivot)
if (j == low) break;
if (i >= j) break;
swap(a, i, j);
}
swap(a, low, j);
return j;
}
public void swap(int[] a, int i, int j){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
三 性能分析
在最好的情况下,快速排序只需要大约 NlgN N l g N 次比较操作,在最坏的情况下需要大约 12N2 1 2 N 2 次比较操作。在平均情况下大约 2NlnN 2 N l n N ~ 1.39NlgN 1.39 N l g N 次比较。
在最好的情况
下,每次的划分都会恰好从中间
将序列划分开来,那么只需要
lgN
l
g
N
次划分即可划分完成,是一个标准的分治算法Cn=2Cn/2+N,每一次划分都需要比较
N
N
次,大家可以回想下我们是如何证明合并排序的时间复杂度的。
在最坏的情况
下,即序列已经排好序
的情况下,每次划分都恰好把数组划分成了(0,N)两部分,那么需要 次划分,但是比较的次数则变成了
N
N
, ,
N−2
N
−
2
,….
1
1
, 所以整个比较次数约为 ~
12N2
1
2
N
2
.
为了避免出现最坏的情况,导致序列划分不均,我们可以首先对序列进行随机化排列然后再进行排序就可以避免这种情况
在平均情况
下,快速排序需要大约
1.39NlgN
1.39
N
l
g
N
次比较,这比合并排序多了39%的比较,但是由于涉及了较少的数据移动操作,他要比合并排序更快。
四 算法特点
1、 快速排序是一种就地(in-place)排序算法。只需要常数个额外的空间。在递归中,也只需要对数个额外空间。
2、 将长度为N的数组排序所需的时间和 NlgN 成正比。快速排序切分方法的内循环会用一个递增的索引将数组元素和一个定值比较。这种简洁性也是快排的一个优点,很难想象排序算法中还能有比这更短小的内循环了。比如,归并排序和希尔排序一般都比快速排序慢,其原因就是它们还在内循环中移动数据。
3、 快速排序是非稳定性排序。
推荐博客:浅谈算法和数据结构: 四 快速排序