算法-排序算法整理
标签:算法

选择排序

  1. 运行时间和输入无关,选择排序不善于利用数组的初始状态,故一个有序的数组或者主键全部相等的数组和一个元素随机排列的数组所用的时间是一样的。

  2. 数据移动是最少的N次交换,每次交换两个元素的值,故选择排序用类N次交换,交换次数和大小和数组的大小是线性关系。

  3. 每趟都可以让一个元素回到最后正确的位置

    //选择排序
    	public class Selection{
    	    // 将a按照升序排列
    	    public void sort(Comparable[] a){
    	        // 数组长度
    	        int N=a.length;
    	        for (int i = 0; i < N; i++) {
    	            // 将a[i]和a[i...N]之间最小的交换
    	            int min=i; //min为最小的索引
    	            for (int j = i+1; j < N; j++) {
    	                if(less(a[j],a[i])){
    	                    min=j;
    	                }
    	            }
    	            exch(a,i,min);
    	        }
    	    }
    	}
    


插入排序

局部有序,相互倒置元素少排序速度很快,插入排序可以很快地发现每个元素已经在正确的位置上了。

下面是几种典型的部分有序的数组

  1. 数组中的每个元素距离它的最终位置都不远
  2. 一个有序的大数组接一个小数组
  3. 数组中只有几个元素的位置不正确

插入排序对于部分有序的数组十分高效,也很适合小规模数组。

// 插入排序
public class Insertion{
    // 将a[]按照升序排列
    public void sort(Comparable[] a){
        int N=a.length;
        for (int i = 0; i < N; i++) {
            // 将a[i]插到a[i-1],a[i-2]...a[i-n]之中
            for (int j = i; j > 0 && less(a[j],a[j-1]); j--) {
                exch(a,j,j-1);
            }
        }   
    }
}

希尔排序

希尔排序为了加快速度简单的改进了插入排序,交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。

思想是是数组任意间隔为h的元素都是有序的,即h有序数组。

希尔排序更高效的原因是它权衡了子数组的规模和有序性排序之初,各个子数组都很短,排序之后子数组都是部分有序的。这两种情况都很适合插入排序。

// 希尔排序
public class Shell {
    // 将a[]按照升序排列
    public static void sort(Comparable[] a) {
        int N = a.length;
        int h = 1;
        while (h < N / 3) {
            h = 3 * h + 1; //h=1,4,13,40,121... 求得h的最大值
        }
        while (h >= 1) {
            // 将数组变为h有序数组
            for (int i = h; i < N; i++) {
                // 将a[i] 插入到a[i-h],a[i-2*h],a[i-3*h]...之中
                for (int j = i; j > h && less(a[j], a[j - h]); j -= h) {
                    exch(a, j, j - h);
                }
            }
            h = h / 3;
        }
    }
}

归并排序

优点:任意长度为N的数组排序时间和NlogN成正比
缺点:它需要的额外空间和N成正比

在使用归并排序的时候,在处理小规模的子数组(比如长度小于15)一般可以将归并排序的运行时间缩短10%~15%

/**
 * 归并排序
 */

// 原地归并抽象方法
public static void merge(Comparable[] a,int lo,int mid,int hi){
    // 将a[lo...mid]和a[mid+1...hi]归并
    int i=lo,j=mid+1;
    Comparable[] aux;
    // 将a[lo...hi]复制到aux[lo...hi]
    for (int k = lo; k <=hi; k++) {
        aux[k]=a[k];
    }   
    // 归并回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++]; //左边小于右边,取左边元素
    }
}

// 自顶向下的归并排序
public class Merge{
    private static Comparable[] aux;
    public static void sort(Comparable[] a){
        aux=new Comparable[a.length];
        sort(a,0,a.length-1);
    }
    private static void sort(Comparable[] a,int lo,int hi){
        if(hi <= lo)return;
        int mid=lo+(hi-lo)/2;
        sort(a,lo,mid);  //将左半边排序
        sort(a,mid+1,hi);  //将右半边排序
        merge(a, lo, mid, hi);  //归并结果
    }
}

// 自底向上的归并排序
public class MergeBU{
    private static Comparable[] aux;
    public static void sort(Comparable[] a){
        int N=a.length;
        aux=new Comparable[a.length];
        for (int sz = q; sz < N; sz+=sz) {  //sz为子数组大小
            for (int lo = 0; lo < N-sz; lo+=sz+sz) {  //lo子数组索引
                merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1,N-1));
            }
        }
    }
}

快速排序

快速排序适用于各种不同的输入数据且在一般应用中比其他排序算法快得多

优点:它是原地排序(只需要一个很小的辅助栈),且将长度为N的数组排序所需的时间和NlgN成正比
缺点:非常脆弱,在实现时尽量避免低劣的性能

// 快速排序
public class Quick{
    public static void sort(Comparable[] a){
        sort(a,0,a.length-1);
    }
    public 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){
        // 将数组切分为a[lo...i-1],a[i],a[i+1...hi]
        int i=lo,j=h+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. 对于小数组,快速排序比插入排序慢
  2. 因为递归,快速排序的sort()方法在小数组中也会调用自己

因此,在排序小数组时应该切换到插入排序
即:把sort中的语句

if(hi <= lo) return;

替换成:

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

M一般在5~15之间比较满意

三路切分

public class Quick3way{
    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 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,hix);
    }
}

优先队列

当一棵二叉树的每个节点都大于等于他的两个子节点时,他被称为堆有序

堆的算法

用长度为N+1的私有数组pq[]来表示一个大小为N的堆,我们不会用pqp[0],堆元素放在pq[1]至pq[N]中

当某个节点的优先级上升(或是在堆底加入一个新的元素时)我们需要由下到上的恢复堆的顺序,叫做上浮(swim),方法:交换他和他的父节点来修复堆

//上浮
private void swim(int k){
    // 如果父节点(k/2)值比当前节点小,交换
    while(k > 1 && less(k/2,k)){
        exch(k/2,k);
        k=k/2;
    }
}

当某个节点的优先级下降(或是将根节点替换为一个较小元素的时候)我们需要由上至下恢复堆的顺序,叫做下沉(sink),方法:通过将他和它的两个子节点中的较大者交换来修复堆

// 下沉
private void sink(int k){
    while(2*k <= N){
        int j=2*k;
        // 找到两个炸鸡店中较大的那个子节点
        if(j<N && less(j,j+1)) j++;
        // 如果当前节点不比两个子节点小,则停止
        if(!less(k,j))break;
        // 交换父节点与子节点
        exch(k,j);
        // 更新k的值
        k=j;
    }
}

基于堆的优先队列

// 基于堆有优先队列
public class MaxPQ<Key extends Comparable<Key>>{
    private Key[] pq;
    private int N=0;
    public MaxPQ(int maxN){
        pq=(Key[])new Comparable[maxN+1];
    }
    public boolean isEmpty(){
        return N==0;
    }
    public int size(){
        return N;
    }
    public void insert(Key v){
        pq[++N]=v;
        swim(N);
    }
    public Key delMax(){
        // 取得最大值
        Key max=pq[1];
        // 将第一个元素和最后一个元素(最小的交换)
        exch(1,N--);
        // 将对吼一个元素置为null,让系统回收
        pq[N+1]=null;
        // 将交换到第一个位置的元素下沉,重新恢复
        sink(1);
        return max;
    }

}

堆排序

将所有元素插入一个查找最小元素的优先队列,然后再重复调用删除最小元素的操作来将他们按顺序删去,分为两个阶段:

  1. 在堆的构造过程中,我们将原始数组重新组织安排进一个堆中
  2. 然后下层排序,我们从堆中按递减顺序取出所有元素并得到排序结果
  //堆排序
  public static void sort(Comparable[] a){
      int N=a.length;
      for (int k = N/2; k >= 1; k--) {
          sink(a,k,N);
      }
      while(N>1){
          exch(a,1,N--);
          sink(a,1,N);
      }
  }

各种排序算法性能特点

算法 是否稳定 是否原地排序 时间复杂度 空间复杂度 备注
选择排序 N^2 1
插入排序 介于N和N^2之间 1 取决于输入元素排列情况
希尔排序 NlogN 1
快速排序 NlogN lgN 运行效率由概率保证
三向快速排序 介于N和NlogN之间 lgN 运行效率由概率保证,同时也取决于输入元素分布情况
归并排序 NlogN N
堆排序 NlogN 1
  • 9 min read

CONTRIBUTORS


  • 9 min read