基础算法:排序算法

典型的排序算法有:选择排序(selection sort)、插入排序(insertion sort)、希尔排序(shellsort)、合并排序(mergesort)、快速排序(quicksort)、堆排序(heapsort)。

选择哪种排序算法,要考虑三个条件: 是否依赖输入值(input values)、是否需要额外空间(extra memory)、对于相等键如何处理。

基础概念:

An inversion is a pair of entries that are out of order in the array.

1. 逆序(inversion): 在数组中,一个逆序(inversion)是不正常顺序的项目对。

例如:{3, 1, 5 , 2} 的逆序对有:(3,1)、(3,2)、(5,2)

2. 部分排序(partially sorted): 在一个数组中,逆序的个数小于数组大小的固定倍数(逆序个数<= cN, c为固定倍数,N为数组大小),那么对该数组的排序称为 部分排序。

说明: 进行排序的数组,每个元素应该实现了 Comparable ,各种排序通用的方法有:

    /**
     * 比较大小
     * @param v
     * @param w
     * @return
     */
    public static boolean less(Comparable v, Comparable w){
        return v.compareTo(w) < 0;
    }

    /**
     * 交换元素
     * @param a
     * @param i
     * @param j
     */
    public static void exch(Comparable[] a, int i, int j){
        Comparable t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

一、选择排序(Selection sort)

排序后会打乱原有的正确排序,是在原地进行排序,排序运行时间的增长率为N^{2},是需要使用一个额外的空间。

    /**
     * 选择排序,将 最小值移到最前面
     * @param a
     */
    @Override
    public void sort(Comparable[] a) {
        // Sort a[] into increasing order.
        int N = a.length;
        for (int i=0; i<N; i++){
            int min = i;
            for (int j=i+1; j<N; j++){
                if(less(a[j], a[min])){
                    min = j;
                }
                exch(a, i, min);
            }
        }
    }

二、插入排序(insertion sort)

适用: 插入排序对“部分排序”和小数组排序是非常好的方法。

对于特定类型的非随机数组,使用插入排序,工作效率比较高。

插入排序大概比选择排序的效率快两倍。

排序之后不会打乱原来正常的排序(stable),是原地排序,排序运行时间的增长率为N到N^{2}之间, 取决于要排序的项。

    /**
     * 插入排序
     * @param a
     */
    @Override
    public void sort(Comparable[] a) {
        // Sort a[] into inCreasing order
        int N = a.length;
        for (int i = 0; i < N; i++) {
            // Insert a[i] among a[i-1], a[i-2], a[i-3].....
            for (int j = i; j >0 && less(a[j], a[j-1]); j--) {
                exch(a, j, j-1);
            }
        }
    }

缺点: 插入排序对大的无序数组是效率低的。因为插入排序一次只能改变相邻两个元素的位置,如果一个最小的元素位于数组的末尾,就需要N-1次排序。

三、希尔排序(Shellsort)

希尔排序是对插入排序(Insertion sort)的拓展。增长序列可以计算出来,也可以将增长序列存在数据中。下面的算法增长序列是计算出来的 :((3^k) -1 )/2  ==> while (h < N /3)  h= 3 * h + 1;

性能: 希尔排序不一定是二次的。

例如,对于下面的具体实现,在最坏的情况下,排序中的比较次数是和 N^(3/2)  成比例。

希尔排序不是稳定的(改变相同值的位置),是原地排序算法,运行时间的增长率为NlogN , 需要1个额外空间。

    @Override
    public void sort(Comparable[] a) {
        // Sort a[] into increasing order.
        int N = a.length;
        int h = 1;
        // (3^k -1)/2
        while (h < N / 3) h = 3 * h + 1;
        while (h >= 1) {
            // h-sort the array
            for (int i = h; i < N; i++) {
                // Insert a[i] among a[i-h], a[i-2*h]...
                for (int j = i; j >= h && less(a[j], a[j - h]); j -= h) {
                    exch(a, j, j - h);
                }
            }
            h = h / 3;
        }
    }

经验丰富的程序员通常选择希尔排序(shellsort),因为即便对于中等大小的数组,这个排序算法的运行时间也是可以接受的。

shellsort只需要很少数量的代码,并且使用shellsort不需要额外的空间。

使用场景: 如果在一个系统排序不可用的情况下(例如,代码用于硬件或者嵌入式系统),解决一个排序问题,可以安全的选择希尔排序(shellsort)。一段时间之后,再确定是否值得换成更复杂的、高级的方法。

 

四、归并排序(mergesort)

归并排序:为了排序一个数组,将这个数组分成两半,排序这两半(递归操作),然后合并最终的结果。

递归排序最诱人的是,该算法保证对于任何有N个项的数组,所花时间与NlogN成比例。而递归排序重要的缺点是,使用了额外的空间,额外空间的大小与数组大小N成比例。

归并排序有两种实现方法,Top-down和down-up。  归并排序是稳定的(如果两个元素相同,不改变原来的位置),不是原地排序,排序运行时间的增长率为NlogN, 额外的空间为lgN。

4.1 在原地合并的抽象: Abstract in-place merge

前提假设: 一个数据Comparable[] a 被分成两部分第一部分坐标为 lo 到 mid ,第二部分的坐标为 mid+1 到hi;这两部分都已经排完序了。

    /**
     *  原地合并的抽象   Abstract in-place merge
     *  通过复制数组中的每一项到一个副本中,然后再合并到原来的数组中
     * @param a
     * @param lo
     * @param mid
     * @param hi
     */
    public static void merge(Comparable[] a, int lo, int mid, int hi){
        // Merge a[lo..mid] with a[mid+1..hi].
        int i=lo, j= mid+1;
        // Copy a[lo..hi] to aux[lo..hi].
        Comparable[] aux = new Comparable[a.length];
        for (int k=lo; k<=hi;k++){
            aux[k] = a[k];
        }
        
        // Merge back to a[lo..hi].
        for (int k=lo; k<=hi; k++){
            if (i>mid) a[k] = aux[j++];
            else if (j>hi) a[k] = aux[i++];
            else if (less(aux[j], aux[i])) a[k] = aux[j++];
            else a[k] = aux[i++];
        }
    }

4.2 自上而下的归并排序 (Top-down mergesort)

Top-down mergesort 是一个基于Abstract in-place merge 的递归归并算法。

/**
     * Top-down mergesort
     * recursive
     * @param a
     * @param lo
     * @param hi
     */
    private static void sort(Comparable[] a, int lo, int hi){
        if (hi<=lo) return;
        int mid = lo + (hi-lo)/2;
        // Sort left half
        sort(a, lo, mid);
        // Sort right half
        sort(a, mid + 1, hi);
        // Merge results 
        merge(a, lo, mid, hi);
    }

Top-down mergesort 和 abstract in-place mergesort 不在同一个层次,因为我们能排序一个大的数组,而不需要检查每一个条目。我们能够对百万量级(或更多)的元素项使用Top-down mergesort方法来排序,但却不能使用插入排序(insertion sort)或选择排序(selection sort)。

mergesort 的主要缺点是需要 为辅助合并的数组 分配与数组大小N成比例的额外空间。如果空间稀缺,我们需要考虑其他方法。

另外,通过认真考虑后对实现进行修改,还可以大幅缩短mergesort的运行时间。

(1) 对于子数组使用插入排序:对小的数组使用插入排序,将优化典型归并排序运行时间10%或15%;

(2)测试数据是否已经排序:通过增加一个测试,如果a[mid]小于或等于a[mid+1],就跳过调用merge()方法。这样做,仍然需要递归调用,但是对于任何排序的子数组是线性的。

(3)消除向辅助数组中的拷贝:这样是消除向辅助数组拷贝的时间,但不能消除因为辅助数组而使用的空间。为了这样做,需要进行两次排序方法的调用:第一次从给定数组拿到输入经排序后放到辅助数组中,第二次从辅助数组获取输入经过排序后放到给定数组。

4.3 Bottom-up mergesort

@Override
    public void sort(Comparable[] a) {
        int N = a.length;
        for (int sz = 1; sz < N; sz = sz + sz) {
            for (int lo = 0; lo < N - sz; lo += sz + sz) {
                merge(a, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1));
            }
        }
    }

当数组的长度为2的乘方的时候,top-down 和 bottom-up mersort  有相同的比较和数组读取次数,仅仅顺序不同。当数组的长度不是2的乘方的时候,这两种算法对于比较和数组的读取次数是不同的。

Bottom-up mergesort 算法为了排序一个长度为N的数组,需要使用 1/2NlgN 到 NlgN 次比较 和 最多 6NlgN 次数组的读取。

归并排序是基于比较的算法中渐进最优的算法。

五、快速排序(Quick sort)

快速排序比任何其他的排序算法使用都广泛。quicksort 有两个特点:原地排序(仅使用很小的辅助‘堆stack’容器),平均排序一个数组所需时间和NlogN 成正比。

算法的基础

快速排序是一个分治(divide-and-conquer)的排序方法。通过将数组分成两部分,然后分别排序子数组。

快速排序是对归并排序(mergesort)的补充:对于归并排序mergesort,我们将数组分成两个子数组,将这两个子数组排序,然后结合这两个已经排序的子数组,让真个数组排序;对于快速排序quicksort,当两个子数组是已经排序了,那么整个数组都是已经排序了。第一种情况是,在作用于整个数组之前进行递归;第二种情况是,在作用于整个数组之后进行递归。

对于归并排序(mergesort),数组是按照一半进行分割的;对于快速排序(quicksort),被分割的位置取决于数组的内容。

快速排序算法的核心是分离的过程。被分割的数组由三部分组成: 左边的子数组、分割项、右边的子数组。应该保证:

(1)分割项 a[j] 所处的位置就是在排序后数组的最终位置;

(2)对于分割项左边的数组,没有一个比分割项的大;

(3)对于分割项右边的数组,没有一个比分割项的小;

/**
 * @Date 2019-07-12
 * @Author lifei
 */
public class Quick extends SortCommont {


    @Override
    public void sort(Comparable[] a) {
        /**
         * 随机打乱 数据a元素的顺序
         * 让这个算法变成随机算法,为了更好预测算法的性能
         * 排除对输入的依赖(Eliminate dependence on input))
         */
        StdRandom.shuffle(a);
        sort(a, 0, a.length-1);

    }

    /**
     * quicksort 的递归排序
     * @param a 数组
     * @param lo 较小的索引值
     * @param hi 较大的索引值
     */
    public static void sort(Comparable[] a, int lo, int hi){
        if (hi <= lo) return;
        // 分割
        int j = partition(a, lo, hi);
        // 对左边子数组进行排序: a[lo...j-1]
        sort(a, lo, j-1);
        // 对右边子数组进行排序: a[j+1.. hi]
        sort(a, j+1, hi);

    }

    /**
     * quicksort 的关键方法
     * @param a
     * @param lo
     * @param hi
     * @return
     */
    public static int partition(Comparable[] a, int lo, int hi){
        int i = lo, j=hi+1;
        Comparable v = a[lo];
        while (true){
            // Scan right, Scan left, check for scan complete, and exchange
            while (less(v, a[++i])) if (i==hi) break;
            while (less(a[--j], v)) if (j==lo) break;
            if (i>=j) break;
            exch(a, i, j);
        }
        // Put v = a[j] into position
        exch(a, lo, j);
        return j;
    }
}

快速排序算法的潜在麻烦: 如果分区不平衡,该算法将极其低效。例如,第一次分区的项是最小的项,第二次分区的时用第二小的项,依次类推,最终导致出现大量分区的大的子数组。为了避免这种情况,在使用快速排序之前进行随机排序的原因。

快速排序的优化

(1)切断,使用插入排序。基于两点观察:对极其小的子数组进行排序的时候,快速排序比插入排序要慢;快速排序对于小的子数组也会递归调用;为此,使用下面的语句代替原来 if (hi <= lo) return;

if(hi <= lo + M) { Insertion.sort(a, lo, hi); return;}

M的取值要依赖于系统,但在大多情况下,M取 5 到15 之间的值 ,工作的效果都很好。

(2) Quicksort with 3-way partitioning对有大量重复元素的数组进行排序

思路:将数组分为三段 before、during、after。

before段的的所有元素值小于during段的值,during段的所有元素值都都相等,after段的所有元素值都大于after。

    /**
     * 使用三段式对数据进行排序: 第一段存放的是小于分割值,第二段存放的是等于分割值, 第三段存放的是大于分割值
     * @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){
            int com = a[i].compareTo(v);
            if (com<0) exch(a, lt++, i++);
            else if (com>0) exch(a, i, gt--);
            else i++;
        }
        sort(a, lo, lt-1);
        sort(a, gt+1, hi);
    }

 

六、堆排序

写到另一篇博客了:(二叉树)堆排序:HeapSort

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值