算法笔记-排序02:归并排序,快速排序

思路

归并排序的思想是先将数组分散为小数组分别排序,然后将结果归并起来。

原地归并的抽象方法

将两个已经排序好的数组归并为一个数组这一操作对于归并排序的意义不言而喻,以下是归并方法的实现:

public static void merge(Comparable[] a, int low, int mid, int high){
    int i = low, j = mid+1;
    
    for (int k = low; k <= high; k++)
        indexA[k] = a[k];
    
    for (int k = low; k <= high; k++){
        if (i > mid) a[k] = indexA[j++];
        else if (j > high) a[k] = indexA[i++];
        else if (less(indexA[j], indexA[i])) a[k] = indexA[j++];
        else a[k] = indexA[i++];
    }
}
自顶向下的归并排序

基于原地归并的抽象方法实现了另一种递归归并,这是应用高效算法设计中分治思想的典型例子:

public class Merge {
    
    private static Comparable[] indexA;
    
    public static void merge(Comparable[] a, int low, int mid, int high){
        
        int i = low, j = mid+1;
        
        for (int k = low; k <= high; k++)
            indexA[k] = a[k];
        
        for (int k = low; k <= high; k++){
            if (i > mid) a[k] = indexA[j++];
            else if (j > high) a[k] = indexA[i++];
            else if (less(indexA[j], indexA[i])) a[k] = indexA[j++];
            else a[k] = indexA[i++];
        }
    }
    
    public static void sort(Comparable[] a, int low, int high){
        if (indexA==null) indexA = new Comparable[a.length];
        if (high <= low) return;
        int mid = low + (high-low)/2;
        sort(a, low, mid);
        sort(a, mid+1, high);
        merge(a, low, mid, high);
    }
  
    
    private static boolean less(Comparable v, Comparable w){
        return v.compareTo(w) < 0;
    }
    
    private static void exchange(Comparable[] a, int i, int j){
        Comparable t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
    
    private 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 class MergeBU {
    private static Comparable[] indexA;
    
    public static void merge(Comparable[] a, int low, int mid, int high){
        
        int i = low, j = mid+1;
        
        for (int k = low; k <= high; k++)
            indexA[k] = a[k];
        
        for (int k = low; k <= high; k++){
            if (i > mid) a[k] = indexA[j++];
            else if (j > high) a[k] = indexA[i++];
            else if (less(indexA[j], indexA[i])) a[k] = indexA[j++];
            else a[k] = indexA[i++];
        }
    }
    
    public static void sort(Comparable[] a){
        if (indexA == null) indexA = new Comparable[a.length];
        for (int sz = 1; sz<a.length; sz = sz+sz)
            for (int low = 0; low < a.length-sz; low += sz+sz)
                merge(a,low,low+sz-1,Math.min(low+sz+sz-1, a.length-1));
    }
    
    public static void main(String[] args){
        Integer[] a = {9,8,7,6,5,4,3,2,1};
        sort(a);
        for (Integer i: a){
            System.out.println(i);
        }
    }
    
    private static boolean less(Comparable v, Comparable w){
        return v.compareTo(w) < 0;
    }
    
    private static void exchange(Comparable[] a, int i, int j){
        Comparable t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
    
    private 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;
    }
}

两种实现方式的速度比较(给一千个大小为一万的数组排序):

运行结果
快速排序

快速排序可能是应用的最为广泛的一种算法,它流行的原因是实现简单,适用于各种不同的输入数据且在一般的应用中比其他排序算法都要快的多。快速排序的优点:
是原地排序(只需要一个很小的辅助栈)。
所需时间跟NlgN成正比。

快速排序思路:

快速排序和归并排序是互补的,归并排序将整个数组分成小数组,然后将排好序的小数组归并以将整个数组排序;而快速排序是在将大数组分成小数组的时候排序,当小数组小到不可再分的时候,排序也就完成了。
1.首先选择一个中间元素(一般选左端或者右端)。
2.分别获取除中间元素外的左右两端的索引。
3.由左右两端逐渐向中间迭代,每迭代一步比较一下索引中的元素和中间元素,当左边出现比中间元素大的元素的时候,暂停左边的迭代,当右边迭代出比中间元素小的元素的时候,右边迭代也暂停,交换左右两边的元素。
4.重复步骤3,直到左右两边的索引相遇,然后将中间元素移动到中间,这时中间元素左边的元素都比它小,右边的元素都比它大。
5.将上面的中间元素左右两边当成两个数组,分别进行上述过程。
6.重复以上步骤直到数组不可再分。

完整代码
public class Quick {
    
    public static void sort(Comparable[] a){
        sort(a,0,a.length-1);
    }
    
    private static void sort(Comparable[] a,int low, int high){
        if (high <= low) return;
        int j = partition(a,low,high);
        sort(a,low,j-1);
        sort(a,j+1,high);
    }
    
    private static int partition(Comparable[] a, int low, int high){
        //将数组切分为a[lo..i-1], a[i], a[i+1..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]达成
    }
    
    public static void main(String[] args){
        Integer[] a = {9,8,7,6,5,4,3,2,1};
        sort(a, 0, a.length-1);
        for (Integer i: a){
            System.out.println(i);
        }
    }
    
    private static boolean less(Comparable v, Comparable w){
        return v.compareTo(w) < 0;
    }
    
    
    private static void exchange(Comparable[] a, int i, int j){
        Comparable t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
    
    
    private 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;
    }
}

比较快速排序和归并排序的速度
命令:

%   java Main Merge Quick 10000 1000
运行结果
快速排序算法的改进

快速排序自被C.A.R Hoare在1960年发明后,就不断的有人试图改进它,但是由于快速排序已经“so well-balanced”,改进所带来的优化往往都被副作用抵消了,比如《算法》一书中对快速排序的实现中在排序之前会先随机打乱数组来避免一种极端情况——当我们每一次选择的中间元素都恰好是最小元素时,该算法会变的像选择排序,从而导致时间复杂度变成N的平方。但是笔者在测试的时候发现运行上面的指令时随机打乱数组所花掉的时间几乎使得运行时间加倍,而事实上出现这种极端情况的概率比你的电脑在排序时突然被闪电击中的概率都要小的多(这个flag不是我立的,我以后不随随便便排序了)。

但是依然有人找到了一些有用的改进方式:

1.第一种改进方案是说由于插入排序在小数组的时候会比快速排序快,所以在分成小数组的时候使用插入排序,然而笔者在自己的电脑上测试的时候发现无论是大数组还是小数组,快速排序都比插入排序要快得多,按照这种方式修改的快速排序也变慢了,所以存疑。

2.实际应用中我们排序的数组常常含有大量的重复元素,例如将上千万人员的资料按照生日排序,那就必然会有大量的重复的数值(毕竟一百年里面也就四万多天,分配给上千万人作生日,自然有大量重复),于是有人提出与其将数组二分,不如分成三部分,一部分小于中间值,一部分大于中间值,一部分等于中间值,此算法被称为三向切分的快速排序,以下是代码:

public class Quick3Way {

    public static void sort(Comparable[] a){
        sort(a,0,a.length-1);
    }
    
    private static void sort(Comparable[] a,int low, int high){
        if (high <= low) return;
        int lt = low, i = low+1,gt = high;
        Comparable v = a[low];
        while (i <= gt){
            int cmp = a[i].compareTo(v);
            if      (cmp < 0)exchange(a,lt++,i++);
            else if (cmp > 0)exchange(a,i,gt--);
            else    i++;
        }
                     sort(a,low,lt-1);
        if (gt<high) sort(a,gt+1,high);
    }
    
    public static void main(String[] args){
        Integer[] a = {9,8,7,6,5,4,3,2,1};
        sort(a, 0, a.length-1);
        for (Integer i: a){
            System.out.println(i);
        }
    }
    
    private static boolean less(Comparable v, Comparable w){
        return v.compareTo(w) < 0;
    }
      
    private static void exchange(Comparable[] a, int i, int j){
        Comparable t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
     
    private 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;
    }    
}

当使用随机生成的由0~100组成的大小为一万的数组排序一千次时改进方法与原方法所花费的时间如下:

运行结果

对于标准的快速排序,随着数组规模的增大其运行时间会趋于平均运行时间,大幅度偏离的情况非常罕见。而三向切分的快速排序对于大量重复元素的数组来说运行时间由线性对数级别降低到了线性级别,并且和元素的排列没有关系。由于在平常使用中对含有大量重复元素的数组排序的情况很常见,所以拥有对重复元素的适应性的三向分切的快速排序成为了排序库函数的最佳选择。
经过精心优化的快速排序在绝大多数计算机的绝大多数应用中都比其他算法要快,它在当前业界的广泛使用正说明了这一点。

总结一下当前学习过的排序算法的速度:

在给千万级别的数组排序的情况下:
Quick > Merge > Shell > Insertion > Selection

资源以及参考

普林斯顿大学算法课程以其教材《算法》第四版

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 一. 写在前面 要学习算法,“排序”是一个回避不了的重要话题,在分析完并查集算法和常用数据结构之后,今天我们终于可...
    Leesper阅读 2,462评论 0 40
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 将一个记录插入到已排序好...
    依依玖玥阅读 1,177评论 0 2
  • 概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    蚁前阅读 5,101评论 0 52
  • Ba la la la ~ 读者朋友们,你们好啊,又到了冷锋时间,话不多说,发车! 1.冒泡排序(Bub...
    王饱饱阅读 1,763评论 0 7
  • 文|小Cindy 每次在朋友圈看到有人打卡已在某某app背了xxx个单词,或者是已在某某app阅读xxx天都觉得这...
    故園東望路漫漫Cy阅读 345评论 0 1