1快速排序
1.1快速和归并的比较
归并排序 | 快速排序 | |
---|---|---|
排序方式 | 将数组分成两个子数组分别排序,并将两个有序的子数组归并以将整个数组排序 | 当两个子数组都有序时整个数组就自然有序了 |
递归调用的时机 | 发生在处理整个数组之前 | 发生在处理整个数组之后 |
数组切分方式 | 一个数组被分为两半 | 切分的位置取决于数组的内容 |
1.2快排基本算法
快速排序递归地将子数组a[lo..hi]
排序,先用partition()
方法将a[j]
放到一个合适的位置,然后再用递归调用将其他位置的元素排序。
该方法的关键在于切分,这个过程使得数组满足下面三个条件:
- 对于某个
j
,a[j]
已经排定 a[lo]
到a[j-1]
中的所有元素都不大于a[j]
a[j+1]
到a[hi]
中的所有元素都不小于a[j]
我们就是通过递归调用切分来排序的。
1.3实现
/**
* 快速排序
* @Author: AZhu
* @Date: 2021/2/22 19:30
*/
public class Quick extends Sorting {
/**
* 对数组a进行快速排序
* @param a
*/
public static void sort(Comparable[] a) {
StdRandom.shuffle(a);//消除对输入的依赖
sort(a, 0, a.length - 1);
}
/**
* 对a[lo..hi]进行排序
* @param a
* @param lo
* @param hi
*/
private static void sort(Comparable[] a, int lo, int hi) {
if (hi >= lo) {
return;
}
int j = partition(a, lo, hi);//切分
sort(a, lo, j - 1);//将左半部分a[lo..j-1]排序
sort(a, j + 1, hi);//将右半部分a[j+1..hi]排序
}
/**
* 将数组a[lo..hi]切分为a[lo..i-1] a[i] a[i+1..hi]
* @param a
* @param lo
* @param hi
* @return
*/
private static int partition(Comparable[] a, int lo, int hi) {
int i = lo, j = hi + 1;//左右扫描指针
Comparable v = a[lo];//切分元素
while (true) {
//扫描左右,检查扫描是否结束并交换元素
while (less(a[++i], v)) {
if (i == hi) break;
}
while (less(v, a[--j])) {
if (j==lo) break;
}
if (i >= j) break;
exch(a, i, j);
}
exch(a, lo, j);//将v=a[j]放入正确的位置
return j;//a[lo..j-1] <= a[j] <=a[j+1..hi]达成
}
}
1.4分析
注意事项:
-
原地切分:
辅助数组可以很容易实现切分,但是切分后的数组复制回去的开销会使我们得不偿失。
-
别越界
如果切分元素是数组中最小或最大的元素,我们就需要小心别让扫描指针跑出数组的边界。
-
保持随机性
打乱数组元素的顺序对于预测算法的运行时间很重要,
保持随机性的另一个方法是在
partition()
中随机选取一个切分元素 -
终止循环
要格外小心保证循环结束。
性能:
内循环十分简洁是快速排序的一个优点。
一个速度上的优势在于它的比较次数很少。
时间复杂度 ~ NlgN
。
- 将长度为
N
的无重复数组排序,平均需要 ~2NlgN
次比较(以及1/6
的比较)。 - 快速排序最多需要约
N^2/2
次比较,但是随机打乱数组能够预防这种情况。
1.5算法改进
优化后的快速排序一般来说能将性能提升20%
~30%
。
-
切换到插入排序
对于小数组,插入排序比快速排序慢
改动:
将
if(hi <= lo) return;
替换成:
if(hi <= lo + M) { Insertion.sort(a,lo,hi); return; }
参数
M
的最佳值是和系统相关,但是5~15
之间的任意值在大多数情况下都能令人满意。 -
三取样切分
使用子数组的一小部分元素的中位数来切分数组。
取样大小为
3
并用大小居中的元素切分的效果最好。 -
熵最优的排序
一个元素全部重复的子数组就不需要继续排序了,但是我们的算法还会继续将它切分为更小的数组。
一个简单的方法是:将数组切分为三部分,分别对应于小于、等于、大于切分元素的数组元素。
2三向切分的快速排序
1.1基本算法
我们使用Comparable接口
(而非less()
接口)对a[i]
进行三向比较来直接处理以下情况:
a[i]
小于v
,将a[lt]
和a[i]
交换,将lt
和i
加一;a[i]
大于v
,将a[gt]
和a[i]
交换,将gt
减一;a[i]
等于v
,将i
减一;
1.2实现
/**
* 三向切分的快速排序
* @Author: AZhu
* @Date: 2021/2/22 19:54
*/
public class Quick3way extends Sorting {
/**
* 对数组a进行三向切分快速排序
* @param a
*/
public static void sort(Comparable[] a) {
StdRandom.shuffle(a);//消除对输入的依赖
sort(a, 0, a.length - 1);
}
/**
* 对a[lo..hi]进行排序
* @param a
* @param lo
* @param hi
*/
private static void sort(Comparable[] a, int lo, int hi) {
if (hi <= lo) {
return;
}
int lt = lo, i = lo + 1, gt = hi;
Comparable v = a[lo];
while (i <= gt) {
//实现:a[lo..lt-1] < v=a[lt..gt] < a[gt+1..hi]
int cmp = a[i].compareTo(v);
if (cmp < 0) {
exch(a, lt++, i++);
} else if (cmp > 0) {
exch(a, i, gt--);
} else {
i++;
}
}
sort(a, lo, lt - 1);
sort(a, gt + 1, hi);
}
}
1.3分析
对于存在大量的重复元素的数组,这种方法比标准的快速排序的效率高得多。
对于只有若干不同主键的随机数组:
- 归并排序的时间复杂度是
线性对数
的 - 三向切分快速排序的时间复杂度是
线性
的
这种对重复元素的适应性使得三向切分的快速排序成为排序库函数的最佳算法选择。