算法系列(五)快速排序及其改进(Java实现)

来源:互联网 发布:居则曰 不吾知也翻译 编辑:程序博客网 时间:2024/05/22 00:49

前言:

快速排序是C.A.R Hoare于1960年发明的。基本思想也是分治。快速排序不需要额外的空间,是原地排序,时间复杂度是O(NlgN),

快速排序快在元素之间是跳跃比较,比较的次数将会减少,内循环短小。在实际应用中,比其他排序算法都要快得多。


快速排序:

对于待排数组a,长度为N,

1,首先,选定一个元素作为固定的比较对象,我们称之为基准元素 v。选定两个变量 i , j作为索引 ,开始时i=0,j=N-1;

2,从 j 开始向前搜索,找到第一个a[ j ]<v ,从 i 开始向后搜索,找到第一个a[ i ] > v, 交换a[ i ]和a[ j ];

3,当i >= j时退出,退出后将 v 归位,即将 v 和 a[ j ]  互换

此时 v  将原数组切分成两个子数组,左边都是比它小的元素,右边都是比它大的元素。

4,对 v左右两边的数组重复以上步骤,直到每组只剩一个元素。

/** * 快速排序  * 最快的排序算法 * 平均比较次数2NlogN,无需额外空间 * 之所以快是因为和定值比较,跳跃交换,交换和比较的次数变少 * @author CANAAN * */ public class QuickSort {public static void sort(Comparable[] a){StdRandom.shuffle(a);  //防止出现倒序等时间复杂度为平方的序列sort(a,0,a.length-1);}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);sort(a,j+1,hi);}private static int partition(Comparable[] a, int lo, int hi) {int i = lo,j = hi+1; //i从第二个元素开始,j从最后一个元素开始while(true){while(less(a[++i], a[lo])){  //比较前先计算++if(i == hi) break;}while(less( a[lo],a[--j])){if(j == lo) break; }if(i>=j) break;exch(a, i, j);  //a[i]>基准,a[j]<基准,交换}exch(a,lo,j); //将基准元素调换到j处,此时左边都是小于a[j]的,右边都是大于a[j]的return j;}private static boolean less(Comparable v, Comparable w){  //比较v是否小于wreturn v.compareTo(w) < 0;}private static void exch(Comparable[] a, int i, int j) {  //交换Comparable t = a[i];a[i] = a[j];a[j] = t;}public static void show(Comparable[] a){  //单行打印数组for (int i = 0; i < a.length; i++) {System.out.print(a[i] + " ");}System.out.println();}public static boolean isSorted(Comparable[] a){  //判断是否有序for (int i = 1; i < a.length; i++) {if(less(a[i], a[i-1]))return false;}return true;}public static void main(String[] args) {String[] a = StdIn.readAllStrings();  //注意这里是字符串Integer[] b = new Integer[a.length];  //转化成比较整数for(int i = 0; i<a.length;i++){b[i] = Integer.parseInt(a[i]);} sort(b);show(b);}}

改进一:当数组较小时,使用插入排序

改进二:基准元素的选取,取样3个值取中间大小的值作为基准元素,保证切分后两个子数组差不多大

public class QuickSort2 {public static int CUTOFF = 5;public static void sort(Comparable[] a){StdRandom.shuffle(a); sort(a,0,a.length-1);}/* * 改进1:当数组较小时,使用插入排序 */private static void sort(Comparable[] a, int lo, int hi) {if(hi <= lo+CUTOFF-1) {InsertionSort.sort(a, lo, hi);return;}//if(hi <= lo)  //return;int m = medianOf3(a, lo, lo + (hi - lo)/2, hi);exch(a, lo, m);int j = partition(a,lo,hi);sort(a,lo,j-1);sort(a,j+1,hi);}/* * 改进2:尽量保证基准元素位于中间(排序后) */private static int medianOf3(Comparable[] a, int i, int j, int k) {  //这里包含一个等号的处理if(!less(a[i],a[k]) && !less(a[j],a[i]) || !less(a[i],a[j]) && !less(a[k],a[i])) return i;if(!less(a[j],a[k]) && !less(a[i],a[j]) || !less(a[j],a[i]) && !less(a[k],a[j])) return j;if(!less(a[k],a[i]) && !less(a[j],a[k]) || !less(a[k],a[j]) && !less(a[i],a[k])) return k;return 0;}private static int partition(Comparable[] a, int lo, int hi) {int i = lo,j = hi+1; //i从第二个元素开始,j从最后一个元素开始while(true){while(less(a[++i],a[lo]))if(i == hi) break;while(less(a[lo],a[--j]))if(j == lo) break; if(i>=j) break;exch(a,i,j);}exch(a,lo,j); //将基准元素调换到j处,此时左边都是小于a[j]的,右边都是大于a[j]的return j;}private static boolean less(Comparable v, Comparable w){  //比较v是否小于wreturn v.compareTo(w) < 0;}private static void exch(Comparable[] a, int i, int j) {  //交换Comparable t = a[i];a[i] = a[j];a[j] = t;}public static void show(Comparable[] a){  //单行打印数组for (int i = 0; i < a.length; i++) {System.out.print(a[i] + " ");}System.out.println();}public static boolean isSorted(Comparable[] a){  //判断是否有序for (int i = 1; i < a.length; i++) {if(less(a[i], a[i-1]))return false;}return true;}public static void main(String[] args) {String[] a = StdIn.readAllStrings();Integer[] b = new Integer[a.length];  //转化成比较整数for(int i = 0; i<a.length;i++){b[i] = Integer.parseInt(a[i]);} sort(b);show(b);}}


插入排序部分:

public class InsertionSort { public static void sort(Comparable[] a, int lo, int hi) {        for (int i = lo; i <= hi; i++) {  //这里到底是比较到hi 还是hi-1,看你给它的是a.length还是a.length-1,要保证比较到最后一个元素            for (int j = i; j > lo && less(a[j], a[j-1]); j--) {                exch(a, j, j-1);            }        }        assert isSorted(a, lo, hi);    }private static boolean less(Comparable v, Comparable w){  //比较v是否小于wreturn v.compareTo(w) < 0;}private static void exch(Comparable[] a, int i, int j) {  //交换Comparable t = a[i];a[i] = a[j];a[j] = t;}}


改进三:取三个变量切分数组,将数组分为大于,等于,小于基准元素三部分,这样在递归时就可以剔除相等的元素,减小比较的次数,当数组中存在大量重复元素时,效果明显。

/** * 三向切分的快速排序--->应对含有大量重复元素的数组 * @author CANAAN * */public class Quick3way {public static void sort(Comparable[] a){StdRandom.shuffle(a);sort(a,0,a.length-1);}private static void sort(Comparable[] a, int lo, int hi) {if(hi <= lo)  return;int lt=lo,i=lo+1,gt=hi;  //三个变量,while(i<=gt){int cmp = a[i].compareTo(a[lo]);if(cmp<0) exch(a,lt++,i++);  //a[lo..lt-1]是小于a[lo]的else if(cmp>0)exch(a,i,gt--);   //a[gt+1..hi]是大于a[lo]的else i++;    //a[lt..i-1]是等于a[lo]的}sort(a,lo,lt-1);  //剔除了相等的元素,减少了递归中不必要的比较和交换!!!sort(a,lt+1,hi);}private static int partition(Comparable[] a, int lo, int hi) {int i = lo,j = hi+1; //i从第二个元素开始,j从最后一个元素开始while(true){while(less(a[++i], a[lo]))  //比较前先计算++if(i == hi) break;while(less( a[lo],a[--j]))if(j == lo) break; if(i>=j) break;exch(a, i, j);  //a[i]>基准,a[j]<基准,交换}exch(a,lo,j); //将基准元素调换到j处,此时左边都是小于a[j]的,右边都是大于a[j]的return j;}private static boolean less(Comparable v, Comparable w){  //比较v是否小于wreturn v.compareTo(w) < 0;}private static void exch(Comparable[] a, int i, int j) {  //交换Comparable t = a[i];a[i] = a[j];a[j] = t;}public static void show(Comparable[] a){  //单行打印数组for (int i = 0; i < a.length; i++) {System.out.print(a[i] + " ");}System.out.println();}public static boolean isSorted(Comparable[] a){  //判断是否有序for (int i = 1; i < a.length; i++) {if(less(a[i], a[i-1]))return false;}return true;}public static void main(String[] args) {String[] a = StdIn.readAllStrings();  //注意这里是字符串,下面比较的是字符串的大小sort(a);show(a);}}


由快速排序得到的查找数组中第K小的元素的算法:

public static Comparable select(Comparable[] a,int k){StdRandom.shuffle(a);int lo = 0,hi = a.length-1;while(hi>lo){int j = partition(a,lo,hi);if(j<k) lo = j+1;else if(j>k) hi = j-1;elsereturn a[k];}return a[k];}



总结:

1. 快速排序平均需要~NlgN次比较和1/3 NlgN次交换。最坏时需要N^2/2次比较,随机打乱可以预防(专业术语叫做去除对输入的依赖);

2. 切分不平衡会影响程序的效率,所以基准元素的选取要注意,可以打乱元素,三取1,六取1,九取1;

3. 与归并排序相比,都是分治,归并在递归后处理数组,而快排在递归前处理数组。快速排序比归并排序稍快。在Java的Arrays类的sort方法源码里使用的是归并排序,原因是归并排序是稳定的,而快速排序是不稳定的。而对于基本数据类型的排序时使用的是快速排序,因为归并要消耗额外的空间。

注:

排序算法的稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。

插入排序和归并排序是稳定的;选择排序,希尔排序,快速排序是不稳定的