排序

稳定性:任意两个相等的数据,排序前后的相对位置不会发生改变。满足这一条的排序称他为稳定的。

没有一种排序是任何情况下都表现最好的。也就是说一种算法能被写进教科书一定有他的理由。

选择排序

public void run(int[] args){
        int length = args.length;

        //每次检索最小的放到最左边,放好后再检索剩下没排好的
        for (int i = 0 , k = 0 ; i < length ; i ++ , k = i){
            for (int j = k +1 ; j < length ; j++){
                if ( args[j] < args[k])
                    k = j;   //k用来指向当此循环中最小那个
            }
            
            if ( k != i)
                Utils.swap(args, i, k);
        }
    }

 冒泡排序

public void run(int[] args){
        boolean finishSort = false;
        int len = args.length;
        
        for (int i = len ; i > 0 ; i--){ //用i指示内循环中的上限,因为每一次内循环都能将一个最大的数放好位置
            finishSort = true;               //所以每次可以少遍历一个数 i--
            for (int j = 0 ; j < i-1 ; j ++){   //一轮冒泡把最大的放到最右边,然后对剩下没排好的继续冒泡
                if ( args[j] > args[j+1]){
                    Utils.swap(args, j+1, j);
                    finishSort = false;        
                }
            }
            if (finishSort)
                break;  //避免中途已经排序完成还继续做无畏循环
        }
    }

最好情况:一开始就是有序的,那么内循环循环一次即可,时间复杂度T=O(N),最坏情况是逆序,那么就要将两个循环全部走完,T=O(N2)

好处是相当简单,两个for循环就搞定了,但是一个O(N2)的算法是不可接受的。但是他有一个好处,如果数据是按链表来存的,那么冒泡依然可行,因为他只是交换相邻的元素,不管是数组还是链表都没有问题,但是其他算法就不是这样了。还有一个好处,那就是这里是判断>就交换,那么=的话是不会交换的,也就是说这个算法是稳定的!

插入排序

public class InsertSort implements Sort{

    public void run(int[] args){
        int len = args.length;
        
        for (int i = 1 ; i < len ; i++){   //从下标1开始
            for (int j = i ; j > 0 && args[j] < args[j-1] ; j--)  //保证循环后i前面的数都是有序的
                Utils.swap(args, j, j-1);     //交换相邻
        }
    }
}

还有一种写法,不是用交换相邻元素,感觉这个更像插入排序一点!这个应该算正宗的插入排序:)

package sort;

public class InsertSort implements Sort{

    public void run(int[] args){
        int len = args.length;
        
        int j ;
        for (int i = 1 ; i < len ; i++){
            int tmp = args[i];
            for ( j = i ; j > 0 &&  args[j-1] > tmp ; j-- )
                args[j] = args[j-1];            //腾出空位,找到新元素要插入的位置
            args[j] = tmp;    
        }
    }
}

最好情况:跟冒泡一样,也是一开始就是顺序的,遍历一遍元素就可以完成。T=O(N)

最坏情况:逆序,T=O(N2)

有点:非常简单,还有就是比起冒泡,可以省掉交换函数的那三条语句,算起来还是可以省很多步骤的

插入排序也是在前面的元素比新元素大的时候才移位的,如果相等则不会移动,所以插入排序也是稳定

考虑一个问题

34,8,64,51,32,21    这几个数字分别进行插入和冒泡的元素交换次数是多少?

答案是都是9次…………那这是个巧合麽?

对于下标i < j ,如果 A[i] > A[j] ,那么称(i , j)是一对逆序对(INVERSION)

再数数上面的数列有多少个逆序对?会发现有9个,这两个9是一个巧合麽?不是!因为交换一次相邻元素刚好消去一个逆序对!

因为无论如何,都要从头到尾扫描一遍,所以至少是一个O(N)复杂度,另外,他操作的次数是跟他逆序对的个数成正比的,所以可以说T(N,I) = O(N+I) 由此可见,如果这个I非常小,跟N是一个数量级的,甚至比N还要小,换言之,这个序列是基本有序的!则插入排序简单且高效

定理:任意N个不同元素组成的逆序对平均具有N(N-1)/4个逆序对

也就是说不管是插入排序还是冒泡排序,他们的平均时间复杂度都是跟逆序对的个数有关的,由此可以推出下面的定理

定理:任何仅以交换相邻两元素来排序的算法,其平均时间复杂度是Ω(N2)。Ω代表的是时间复杂度的下界,也就是说,你最好也就是N2时间复杂度了,那换句话说,如果想要提高算法的效率,我们必须做到:

  • 每次消去不止一个逆序对。(如果至少交换相邻两元素是做不到这一点的,所以还必须……)
  • 每次交换相隔较远的两个元素。(期望在一次排序中同时消去多个逆序对)

 希尔排序

Donald Shell提出来的。利用了插入排序的简单,同时呢要克服每次只能交换相邻两个元素的缺点。基本步骤如下

  • 定义增量序列DM>DM-1>...>D1=1

  这个增量序列是随你怎么定的,比如5,3,1,但是最后一个一定要是1

  • 对每一个增量Dk进行Dk间隔的排序

注意:Dk间隔有序的序列,在执行Dk-1间隔排序后,仍然是Dk间隔有序的。也就是说,每一次排序都没有把上一次的排序结果弄坏……

最重要的一点是,希尔排序的增量序列的选取很关键,如果选的不好,可能比前面的几趟排序都不能产生效果,最后还是得靠增量1来排序。这里选取序列1/2(3k-1)

package sort;

public class ShellSort implements Sort {

    @Override
    public void run(int[] args) {
        int len = args.length;
        int h = 1;
        while ( h < len)
            h = 3*h + 1;
        int j;
        while ( h >= 1){
            //将数组变成h有序,增量为h的插入排序
            for ( int i = h ; i < len  ; i++){
                int tmp = args[i];
                for ( j = i ; j-h >= 0 && args[j-h] > tmp ; j-=h)
                    args[j] = args[j-h];
                args[j] = tmp;
            }
            h /= 3;
        }
    }
}

注意:虽然插入排序是稳定的,但是希尔排序不是稳定的,这个还是容易理解的

归并排序

自顶向下的归并

public class MergeSort implements Sort {
    //递归实现
    int[] tmp;
    
    public void run(int[] a){
        tmp = new int[a.length];  //一次性分配数组
        sort( a , 0 , a.length -1 );
    }
    
     void sort(int[] 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);  //归并结果
    }
    
     void merge(int[] a , int lo , int mid , int hi){
        
        for ( int k = 0 ; k <= hi ; k++)
            tmp[k] = a[k];           //复制数组
        
        int i = lo , j = mid+1;   // i扫描左半边的元素 j扫描右半边的元素
        
        for ( int k = lo ; k <= hi ; k++){
            if ( i > mid )            a[k] = tmp[j++];
            else if ( j > hi )        a[k] = tmp[i++];
            else if ( tmp[i] < tmp[j] )  a[k] = tmp[i++];
            else                        a[k] = tmp[j++];
        }
    }
}

递归便于了解,但是实际上操作得时候递归并不是那么美妙的,下面提供一种非递归的方法实现,也叫自底向上的归并排序

void sortBU( int[] a){
         //非递归
         int length = a.length;
         
         for ( int size = 1 ; size < length ; size = 2*size) //size为每次要归并的子数组的大小,2个、4个、8个……
             for ( int lo = 0 ; lo < length-size ; lo = lo + 2*size)  //lo为子数组的索引
                 merge(a , lo , lo+size-1 , Math.min(lo+2*size-1, length-1));
     }

归并排序有一个好处是它是稳定的,平均时间复杂度是nlogn,最坏时间复杂度也是nlogn,千好万好有一点不好的是,它需要一个额外的空间,它需要在数组和数组之间来回的捣腾复制元素,所以实际应用中基本不用归并排序做内排序,就是说如果我们所有的元素都可以在内存里面完成,没用会用归并排序做这件事,但是它在外排序的时候是非常有用的

非递归实现比较适合用链表组织数据,想象一下将链表先按大小为1的子链表进行排序,然后是2,然后是4,这种方法只需要重新组织链表链接就能将链表原地排序,不需要创建任何新的链表结点。

快速排序

传说中现实应用中最快的一种排序算法,但是它也不是在任何情况下都是最好的,我们总能构造出一种它的最坏情况,但是对于大规模的随机数据,快速排序的表现还是相当出色的。跟归并排序有相似之处,都是属于分治思想

有两个关键的地方:1.选主元。2.根据主元分成两个独立的子集

快速排序的最好情况:每次选的主元都恰好在数组的中间 T(N) = O(NlogN)

  • 选主元

1.令pivot = a[0]?

有一种情况,如果一开始就是有序的,根据这种选法的话,子集划分的时候就会发现左边的是空集,然后要遍历右边所有的元素,然后开始递归

T(N) = O(N) + T(N-1)=O(N) + O(N-1) + T(N-2)=...=O(N2)

2.随机选主元?随机函数不便宜啊

3.取头中尾的中位数

  例如8 , 12 , 3 的中位数就是8

当然还有很多选主元的方法

public class QuickSort implements Sort{
    
    public void run(int[] a){
        sort(a , 0 , a.length-1);
    }
    
    public void sort(int[] a , int lo , int hi ){
        if ( hi <= lo) return;  
        int j = partition(lo , hi , a);
        sort(a , lo , j-1);
        sort(a , j+1 , hi);
    }
    
    public int partition(int lo , int hi , int[] a){
        
        int v = a[lo];   //选第一个数作为主元
        int i = lo , j = hi+1;
        
        while (true){
            while (a[++i] < v) if ( i  == hi) break; //遇到一个大于v的数就停下来
            while (a[--j] > v ) if ( j == lo) break; //遇到一个小于v的数就停下来
            
            if ( i >= j) break;
            Utils.swap(a , i , j); //交换这两个数
        }
        Utils.swap(a, lo, j);
        return j;
    }
}

快速排序的问题?

1.用递归,会占用大量的堆栈空间,要push很多次,然后还要pop很多次。对于小规模数据,比如N<100可能还不如插入排序来的快,所以大规模数据我们才采用递归,小规模的就不用这个了

由此可以对快速排序做一个优化,在递归的时候判断一下数组的大小,当比较小的时候就停止递归,直接用简单排序,比如插入排序,要修改也很简单,比如这样

//        if ( hi <= lo) return;  
        if ( hi <= lo +5) {
            new InsertSort().run(a);
            return;
        }
原文地址:https://www.cnblogs.com/i-love-kobe/p/5925581.html