基本思想:
Quicksort是对归并排序算法的优化,继承了归并排序的优点,同样应用了分治思想。
优缺点:
排序算法的应用都需要结合具体环境来考虑,例如若给定序列部分有序,希尔算法最快
快速排序的”快“是建立在综合考虑的基础上,具体情况则不一定(排序效率取决增量序列)
快速排序也不是万能的,例如当给定序列规模很小时,选择排序/插入排序就要比快排好很多。
优:快,空间占用小
缺:不稳定
适用:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,不要求稳定,n较大,快速排序的平均时间最短;
时间avg | 时间min | 时间max | 空间avg | 稳定性 |
O(nlog(n)) | O(nlogn)(每次划分都是对半分) | O(n²)(每次划分都是n-1和1两部分(元素基本有序且采用固定基准(边界))) | 空间复杂度O( n ) 就地快速排序使用的空间是O(1),而真正消耗空间的就是递归调用,每次递归就要保持一些数据; 最优的情况下空间复杂度为:O(logn) ;每一次都平分数组的情况 最差的情况下空间复杂度为:O( n ) ;退化为冒泡排序的情况 | 稳定性:不稳定 |
快速排序改进措施
- 小排序问题:划分为小序列,做直接插入排序,再采用快速排序(由《数据结构与算法分析》(Mark Allen Weiness所著)可知,当待排序列长度为5~20之间,此时使用插入排序能避免一些有害的退化情形)(代码示例中取10)
- 基准的选择:选择适合的切分元素(三数取中,固定基准,随机基准)
- 对于大量重复元素:三向切分
- 尾递归优化:
- 原理:当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。
- 处理切分元素值有重复的情况:左侧扫描最好遇到 >= 切分元素值 的元素时停下,右侧扫描则是遇到 <= 切分元素值的元素时停下。(尽管存在一些等值交换,但可以避免算法的运行时间变为n2级别)
package main.Test;
import java.util.Random;
import java.util.Stack;
public class KuaiSuSort {
/**
* 三向切分-快速排序优化
*
* 插入排序 + 三取样切分 + Tukey's ninther + Bentley-McIlroy 三向切分
*/
private static final int INSERTION_SORT_CUTOFF = 10;
private static final int MEDIAN_OF_3_CUTOFF = 40;
// 非递归
private static void sort(Comparable[] arr) {
int start = 0;
int end = arr.length-1;
Stack<Integer> stack = new Stack<>();
if(start < end)
{
stack.push(end);
stack.push(start);
while(!stack.isEmpty())
{
int l = stack.pop();
int h = stack.pop();
int index = partition(arr,l,h);
if(l < index-1)
{
stack.push(index-1);
stack.push(l);
}
if(h > index+1)
{
stack.push(h);
stack.push(index+1);
}
}
}
}
private static int partition(Comparable[] a, int start, int end)
{
Comparable point = a[start];
while(start < end)
{
while(start < end && !less(a[end], point))
end--;
a[start] = a[end];
while(start < end && !less(point, a[start]))
start++;
a[end] = a[start];
}
a[start] = point;
return start;
}
public static void sort(Comparable[] arr, int type) {
if (type == 0) {
sort(arr);
} else if (type == 1) {
sort(arr, 0, arr.length - 1);
}
}
// 递归
private static void sort(Comparable[] dst, int lo, int hi) {
int arrLen = dst.length;
// (1小排序问题)当子数组大小 <= 10 时,切换到插入排序
if (arrLen <= INSERTION_SORT_CUTOFF) {
insertionSort(dst, lo, hi);
return;
}
// (基准的选择)当子数组大小 <= 40 时,使用三取样切分(median-of-3)选择切分元素median3
else if (arrLen <= MEDIAN_OF_3_CUTOFF) {
int median3 = median3(dst, lo, lo + arrLen / 2, hi);
swap(dst, median3, lo);
}
// (2基准的选择)当子数组大小 > 40 时,使用 Tukey's ninther 方法选择切分元素ninther
else {
int ninther = getNinther(dst, lo, hi, arrLen);
swap(dst, ninther, lo);
}
// (3对于大量重复元素) Bentley-McIlroy 3-way partitioning https://blog.csdn.net/qq1175421841/article/details/50314427
// 使数组 dst[lo...p-1] & dst[q+1...hi] == standard ; dst[p...i-1] < dst[lo] < dst[j+1...q]
int i = lo, j = hi + 1;
int p = lo, q = hi + 1;
Comparable standard = dst[lo];
while (true) {
// 移动指针,使得 dst[p..i-1] < dst[lo] == standard,直到一个 >= standard 的元素a[i]
while (less(dst[++i], standard))
if (i == hi) break;
// 移动指针,使得 dst[lo] == standard > dst[j+1...q],直到一个 <= standard 的元素a[j]
while (less(standard, dst[--j]))
if (j == lo) break;
// 指针交叉时,刚好 dst[i] == standard 的情况下,交换以将 dst[i] 归位
if (i == j && eq(dst[i], standard))
swap(dst, ++p, i);
// 排序完成,退出循环
if (i >= j) break;
// 交换 dst[i] & dst[j] 的值,使其归位
swap(dst, i, j);
// 如果 dst[i] == standard,交换 dst[p] & dst[i],使其归位
if (eq(dst[i], standard)) swap(dst, ++p, i);
// 如果 dst[j] == standard,交换 dst[q] & dst[i],使其归位
if (eq(dst[j], standard)) swap(dst, --q, j);
}
// 在切分循环结束后,将和 standard 相等的元素交换到正确位置
// 即使数组 dst[lo...j-1] < standard == dst[j...i] < dst[i+1...hi]
i = j + 1;
// 把 standard == dst[lo...p-1] 元素归位到 dst[j...i] 中
for (int k = lo; k <= p; k++)
swap(dst, k, j--);
// 把 standard == dst[q+1...hi] 元素归位到 dst[j...i] 中
for (int k = hi; k >= q; k--)
swap(dst, k, i++);
// 递归调用(4尾递归优化)
sort(dst, lo, j);
sort(dst, i, hi);
}
private static int getNinther(Comparable[] dst, int lo, int hi, int n) {
int eps = n / 8;
int mid = lo + n / 2;
int m1 = median3(dst, lo, lo + eps, lo + eps + eps);
int m2 = median3(dst, mid - eps, mid, mid + eps);
int m3 = median3(dst, hi - eps - eps, hi - eps, hi);
return median3(dst, m1, m2, m3);
}
private static void insertionSort(Comparable[] arr, int lo, int hi) {
for (int indexI = lo; indexI <= hi; indexI++) {
for (int indexJ = indexI; indexJ > lo && less(arr[indexJ], arr[indexJ - 1]); indexJ--) {
swap(arr, indexJ, indexJ - 1);
}
}
}
/**
* 比较两个元素的大小
*
* @param comparableA 待比较元素A
* @param comparableB 待比较元素B
* @return 若 A < B,返回 true,否则返回 false
*/
private static boolean less(Comparable comparableA, Comparable comparableB) {
return comparableA.compareTo(comparableB) < 0;
}
/**
* 将两个元素交换位置
*
* @param arr 待交换元素所在的数组
* @param indexI 第一个元素索引
* @param indexJ 第二个元素索引
*/
private static void swap(Comparable[] arr, int indexI, int indexJ) {
Comparable temp = arr[indexI];
arr[indexI] = arr[indexJ];
arr[indexJ] = temp;
}
// 取 arr[i] arr[j] arr[k] 三个元素值的中间元素的下标
private static int median3(Comparable[] arr, int i, int j, int k) {
return (less(arr[i], arr[j]) ?
(less(arr[j], arr[k]) ? j : less(arr[i], arr[k]) ? k : i) :
(less(arr[k], arr[j]) ? j : less(arr[k], arr[i]) ? k : i));
}
// 判断两个元素是否相等
private static boolean eq(Comparable v, Comparable w) {
return v.compareTo(w) == 0;
}
/**
* 打印数组的内容
*
* @param arr 待打印的数组
*/
private static void show(Comparable[] arr) {
for (int index = 0; index < arr.length; index++) {
System.out.print(arr[index] + " ");
}
System.out.println();
}
/**
* 判断数组是否有序
*
* @param arr 待判断数组
* @return 若数组有序,返回 true,否则返回 false
*/
public static boolean isSort(Comparable[] arr) {
for (int index = 1; index < arr.length; index++) {
if (less(arr[index], arr[index - 1])) {
return false;
}
}
return true;
}
// 打乱数组的方法
private static void shuffle(Comparable[] arr) {
int length = arr.length;
Random random = new Random(System.currentTimeMillis());
for (int index = 0; index < length; index++) {
int temp = random.nextInt(length);
swap(arr,index,temp);
}
}
public static void main(String[] args) {
Comparable[] a = {324,4,3,5, 324,2,7,123,3456,2345,234,7,89};
shuffle(a);
sort(a, 0);
System.out.println(isSort(a));
show(a);
}
}