JAVA版排序算法

概述

排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。这里介绍的八个排序算法都属于内排序。


    

    当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。

   快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;


1.插入排序—直接插入排序(Straight Insertion Sort)

基本思想:

将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:先将序列的第1个记录(也可以是多个)看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。

要点:设立哨兵,作为临时存储和判断数组边界之用。

直接插入排序示例:



如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

算法的实现:

[java]  view plain  copy
  1. package sortPractice;  
  2.   
  3.   
  4. public class InsertSort {    
  5. <span style="white-space:pre">  </span>    
  6.     public static void main(String[] args) {    
  7.         int a[] = {3,1,5,7,2,4,9,6,10,8};      
  8.         InsertSort  obj=new InsertSort();    
  9.         System.out.println("初始值:");    
  10.         obj.print(a);    
  11.         obj.insertSort(a);    
  12.         System.out.println("\n排序后:");    
  13.         obj.print(a);    
  14.     
  15.     }    
  16.     
  17.     public void print(int a[]){    
  18.         for(int i=0;i<a.length;i++){    
  19.             System.out.print(a[i]+" ");    
  20.         }    
  21.     }    
  22.     public void insertSort(int[] a) {    
  23.         for(int i=1;i<a.length;i++){//从头部第一个当做已经排好序的,把后面的一个一个的插到已经排好的列表中去。    
  24.                 int j;    
  25.                 int x=a[i];//x为待插入元素    
  26.                 for( j=i;  j>0 && x<a[j-1];j--){//通过循环,逐个后移一位找到要插入的位置,    
  27.                     a[j]=a[j-1];                //即把原队列中所有比X大的元素后移一位。
  28.                 }    
  29.                 a[j]=x;//插入    
  30.         }    
  31.             
  32.     }    
  33. }    

效率:

时间复杂度:O(n^2).最好的情况也就是排序表本身有序,则交换次数为0,比较次数为常数,则为O(n),但是最坏的情况是排序表本身是逆序的情况,比较和移动一次都为最大,此时时间复杂度为O(n2),若排序表平均,复杂度平均也为O(n2)。

其他的插入排序有二分插入排序,2-路插入排序。


 2. 插入排序—希尔排序(Shell`s Sort)

希尔排序是1959 年由D.L.Shell 提出来的,相对直接排序有较大的改进。希尔排序又叫缩小增量排序

基本思想:

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

操作方法:

  1. 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  2. 按增量序列个数k,对序列进行k 趟排序;
  3. 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

希尔排序的示例:


算法实现:

 

我们简单处理增量序列:增量序列d = {n/2 ,n/4, n/8 .....1} n为要排序数的个数

即:先将要排序的一组记录按某个增量dn/2,n为要排序数的个数)分成若干组子序列,每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序。继续不断缩小增量直至为1,最后使用直接插入排序完成排序。

[java]  view plain  copy
  1. package com;  
  2. /* 
  3.  * Java实现希尔排序(缩小增量排序) 
  4.  * author:wyr 
  5.  * 2016-7-14 
  6.  *两个步骤:1,建堆  2,对顶与堆的最后一个元素交换位置 
  7.  */  
  8. public class ShellSort {  
  9.   
  10.     public static void main(String[] args) {  
  11.         int a[] = {3,1,5,7,2,4,9,6,10,8};    
  12.         ShellSort  obj=new ShellSort();  
  13.         System.out.println("初始值:");  
  14.         obj.print(a);  
  15.         obj.shellSort(a);  
  16.         System.out.println("\n排序后:");  
  17.         obj.print(a);  
  18.   
  19.     }  
  20.     private void shellSort(int[] a) {  //dk为增量
  21.          int dk = a.length/2;   
  22.          while( dk >= 1  ){    
  23.             ShellInsertSort(a, dk);    
  24.             dk = dk/2;  
  25.          }  
  26.     }  
  27.     private void ShellInsertSort(int[] a, int dk) {//类似插入排序,只是插入排序增量是1,这里增量是dk,把1换成dk就可以了  
  28.         for(int i=dk;i<a.length;i++){  
  29.             if(a[i]<a[i-dk]){  //若后比前小则排序,否则跳过
  30.                 int j;  
  31.                 int x=a[i];//x为待插入元素,保存小的值
  32.                 a[i]=a[i-dk];  
  33.                 for(j=i-dk;  j>=0 && x<a[j];j=j-dk){//通过循环,逐个后移一位找到要插入的位置,j=j-dk保证每个j只循环一次。  
  34.                     a[j+dk]=a[j];  
  35.                 }  
  36.                 a[j+dk]=x;//插入  
  37.             }  
  38.                   
  39.         }  
  40.           
  41.     }  
  42.     public void print(int a[]){  
  43.         for(int i=0;i<a.length;i++){  
  44.             System.out.print(a[i]+" ");  
  45.         }  
  46.     }  
  47. }  

希尔排序为不稳定排序


希尔排序时效分析很难,关键码的比较次数与记录移动次数依赖于增量因子序列d的选取,特定情况下可以准确估算出关键码的比较次数和记录的移动次数。目前还没有人给出选取最好的 增量因子序列的方法。 增量因子序列可以有各种取法,有取奇数的,也有取质数的,但需要注意: 增量因子中除1 外没有公因子,且最后一个 增量因子必须为1。希尔排序方法是一个不稳定的排序方法。
时间复杂度可以为O(n2/3)~O(n2),也算是当时突破排序时间复杂度为O(n2)的伟大成就。空间复杂度O(1);


3. 选择排序—简单选择排序(Simple Selection Sort)

基本思想:

在要排序的一组数中,选出最小(或者最大)的个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后个数)比较为止。

简单选择排序的示例:

 

操作方法:

第一趟,从n 个记录中找出关键码最小的记录与第一个记录交换;

第二趟,从第二个记录开始的n-1 个记录中再选出关键码最小的记录与第二个记录交换;

以此类推.....

第i 趟,则从第i 个记录开始的n-i+1 个记录中选出关键码最小的记录与第i 个记录交换,

直到整个序列按关键码有序。


算法实现:

[java]  view plain  copy
  1. package com;  
  2. /* 
  3.  * author:wyr 
  4.  *
  5.  */  
  6. public class SimpleSelectSort {  
  7.   
  8.     public static void main(String[] args) {  
  9.         int a[] = {3,1,5,7,2,4,9,6,10,8};    
  10.         SimpleSelectSort  obj=new SimpleSelectSort();  
  11.         System.out.println("初始值:");  
  12.         obj.print(a);  
  13.         obj.selectSort(a);  
  14.         System.out.println("\n排序后:");  
  15.         obj.print(a);  
  16.   
  17.     }  
  18.     private void selectSort(int[] a) {  
  19.         for(int i=0;i<a.length;i++){  
  20.             int min=i;//min存放找到的最小(大)值下标。每次循环下标+1
  21.             for(int j=i+1;j<a.length;j++){//循环待排序部分数组找到找最小值
  22.                 if(a[min]>a[j])  
  23.                     min=j;  
  24.             }  
  25.             swap(a,min,i);//把最小值放到它该放的位置上  
  26.         }  
  27.     }  
  28.     public void print(int a[]){  
  29.         for(int i=0;i<a.length;i++){  
  30.             System.out.print(a[i]+" ");  
  31.         }  
  32.     }  
  33.      public  void swap(int[] data, int i, int j) {    
  34.             if (i == j) {    
  35.                 return;    
  36.             }    
  37.             data[i] = data[i] + data[j];    
  38.             data[j] = data[i] - data[j];    
  39.             data[i] = data[i] - data[j];    
  40.         }    
  41. }  

时间复杂度:无论最好最差,比较次数是一样多的,第i趟排序需要进行n-i次关键字比较,而对于交换次数而言,最好的情况是0次,最差是n-1次,最终的排序时间是比较与交换次数的总和,即为O(n2)。


 简单选择排序的改进——二元选择排序

简单选择排序,每趟循环只能确定一个元素排序后的定位。我们可以考虑改进为每趟循环确定两个元素(当前趟最大和最小记录)的位置,从而减少排序所需的循环次数。改进后对n个数据进行排序,最多只需进行[n/2]趟循环即可。具体实现如下:

[cpp]  view plain  copy
 
 
  1. void SelectSort(int r[],int n) {  
  2.     int i ,j , min ,max, tmp;  
  3.     for (i=1 ;i <= n/2;i++) {    
  4.         // 做不超过n/2趟选择排序   
  5.         min = i; max = i ; //分别记录最大和最小关键字记录位置  
  6.         for (j= i+1; j<= n-i; j++) {  
  7.             if (r[j] > r[max]) {   
  8.                 max = j ; continue ;   
  9.             }    
  10.             if (r[j]< r[min]) {   
  11.                 min = j ;   
  12.             }     
  13.       }    
  14.       //该交换操作还可分情况讨论以提高效率  
  15.       tmp = r[i-1]; r[i-1] = r[min]; r[min] = tmp;  //交换r[min]和r[i-1],下同
  16.       tmp = r[n-i]; r[n-i] = r[max]; r[max] = tmp;   
  17.   
  18.     }   

4. 选择排序—堆排序(Heap Sort)

堆排序是一种树形选择排序,是对直接选择排序的有效改进。

基本思想:

堆的定义如下:具有n个元素的序列(k1,k2,...,kn),当且仅当满足


时称之为堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最小项(小顶堆)。
若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的。如:

(a)大顶堆序列:(96, 83,27,38,11,09)

  (b)  小顶堆序列:(12,36,24,85,47,30,53,91)



初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序

因此,实现堆排序需解决两个问题:
1. 如何将n 个待排序的数建成堆;
2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。


首先讨论第二个问题:输出堆顶元素后,对剩余n-1元素重新建成堆的调整过程。
调整大顶堆的方法:

1)设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶((最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。

2)将根结点与左、右子树中较大元素的进行交换。

3)若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复方法 (2).

4)若与右子树交换,如果右子树堆被破坏,即右子树的根结点不满足堆的性质。则重复方法 (2).

5)继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。

称这个自根结点到叶子结点的调整过程为筛选。如图:



再讨论对n 个元素初始建堆的过程。
建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。

1)n 个结点的完全二叉树,则最后一个结点是第个结点的子树。

2)筛选从第个结点为根的子树开始,该子树成为堆。

3)之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。

如图建堆初始过程:无序序列:(49,38,65,97,76,13,27,49)
                              


                              

 

import java.util.Arrays;

public class HeapSort {
    
    private int[] arr;
    
    public HeapSort(int[] arr){
        this.arr = arr;
    }
    
    /**
     * 堆排序的主要入口方法,共两步。这里是从小到大递增排序。
     */
    public void sort(){
        /*
         *  第一步:将数组堆化
         *  beginIndex = 第一个非叶子节点。
         *  从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。
         *  叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。
         */
        int len = arr.length - 1;
        int beginIndex = (len - 1) >> 1; 
        for(int i = beginIndex; i >= 0; i--){
            maxHeapify(i, len);
        }
        
        /*
         * 第二步:对堆化数据排序
         * 每次都是移出最顶层的根节点A[0],与最尾部节点位置调换,同时遍历长度 - 1。
         * 然后从新整理被换到根节点的末尾元素,使其符合堆的特性。
         * 直至未排序的堆长度为 0。
         */
        for(int i = len; i > 0; i--){
            swap(0, i);
            maxHeapify(0, i - 1);
        }
    }
    
    private void swap(int i,int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    
    /**
     * 调整索引为 index 处的数据,使其符合堆的特性。
     * 
     * @param index 需要堆化处理的数据的索引
     * @param len 未排序的堆(数组)的长度
     */
    private void maxHeapify(int index,int len){
        int li = (index << 1) + 1; // 左子节点索引
        int ri = li + 1;           // 右子节点索引
        int cMax = li;             // 子节点值最大索引,默认左子节点。
        
        if(li > len) return;       // 左子节点索引超出计算范围,直接返回。
        if(ri <= len && arr[ri] > arr[li]) // 先判断左右子节点,哪个较大。
            cMax = ri;
        if(arr[cMax] > arr[index]){ // 若较大的子节点比父节点大
            swap(cMax, index);      // 如果父节点被子节点调换,
            maxHeapify(cMax, len);  // 则需要继续判断换下后的父节点是否符合堆的特性。
        }
    }
    
    /**
     * 测试用例
     * 
     * 输出:
     * [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9]
     */
    public static void main(String[] args) {
        int[] arr = new int[]{3,5,3,0,8,6,1,5,8,6,2,4,9,4,7,0,1,8,9,7,3,1,2,5,9,7,4,0,2,6};        
        new HeapSort(arr).sort();        
        System.out.println(Arrays.toString(arr));
    }
    
}


分析:

堆排序主要耗时在建堆和重建堆的反复筛选上。在建初始堆时,对于每个非终节点,最多进行两次比较和互换操作,因此整个初始建堆的时间为O(n)。在进行排序时第i次重建堆需要用O(logi)的时间(完全二叉树的某个结点到根结点的距离为log2i(向下取整)+1),并且需要取n-1次堆顶记录,因此重建堆的时间复杂度为O(NlogN),堆排序为不稳定排序。


设树深度为k,。从根到叶的筛选,元素比较次数至多2(k-1)次,交换记录至多k 次。所以,在建好堆后,排序过程中的筛选次数不超过下式: 

                                

而建堆时的比较次数不超过4n 次,因此堆排序最坏情况下,时间复杂度也为:O(nlogn )。

细致分析:

    初始化建堆过程时间:O(n)

        推算过程:

        首先要理解怎么计算这个堆化过程所消耗的时间,可以直接画图去理解;

        假设高度为k,则从倒数第二层右边的节点开始,这一层的节点都要执行子节点比较然后交换(如果顺序是对的就不用交换);倒数第三层呢,则会选择其子节点进行比较和交换,如果没交换就可以不用再执行下去了。如果交换了,那么又要选择一支子树进行比较和交换;

        那么总的时间计算为:s = 2^( i - 1 )  *  ( k - i );其中 i 表示第几层,2^( i - 1) 表示该层上有多少个元素,( k - i) 表示子树上要比较的次数,如果在最差的条件下,就是比较次数后还要交换;因为这个是常数,所以提出来后可以忽略;

        S = 2^(k-2) * 1 + 2^(k-3)*2.....+2*(k-2)+2^(0)*(k-1)  ===> 因为叶子层不用交换,所以i从 k-1 开始到 1;

        这个等式求解,我想高中已经会了:等式左右乘上2,然后和原来的等式相减,就变成了:

        S = 2^(k - 1) + 2^(k - 2) + 2^(k - 3) ..... + 2 - (k-1)

        除最后一项外,就是一个等比数列了,直接用求和公式:S = {  a1[ 1-  (q^n) ] }  / (1-q);

        S = 2^k -k +1;又因为k为完全二叉树的深度,所以 2^(k-1) <=  n <= (2^k  -1 ),总之可以认为:k ≈ logn 

        综上所述得到:S = n - longn +1,所以时间复杂度为:O(n)


 我们以较大堆为例,用较大堆第一个节点跟最后一个节点交换,交换之后,最后节点即是序列较大点,然后用调整较大堆的办法调整剩下的堆,使之符合堆的性质。如此下去直至所有元素遍历完成为止。 在这里最坏的情况下,每次更新调整较大堆的过程就是logn(即有K层,每层都比较),有n个元素,所以时间复杂度就是O(nlogn)

 

5. 交换排序—冒泡排序(Bubble Sort)

基本思想:

在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。

冒泡排序的示例:

 

算法的实现:

[cpp]  view plain  copy
 
 
  1. void bubbleSort(int a[], int n){  
  2.     for(int i =0 ; i< n-1; ++i) {  
  3.         for(int j = 0; j < n-i-1; ++j) {  
  4.             if(a[j] > a[j+1])  
  5.             {  
  6.                 int tmp = a[j] ; a[j] = a[j+1] ;  a[j+1] = tmp;  
  7.             }  
  8.         }  
  9.     }  
  10. }  


冒泡排序算法的改进

对冒泡排序常见的改进方法是加入一标志性变量exchange,用于标志某一趟排序过程中是否有数据交换,如果进行某一趟排序时并没有进行数据交换,则说明数据已经按要求排列好,可立即结束排序,避免不必要的比较过程。本文再提供以下两种改进算法:

1.设置一标志性变量pos,用于记录每趟排序中最后一次进行交换的位置。由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可。

改进后算法如下:

 public static void bubbleSort(int[] arr) {
    int i, temp, len = arr.length;
    boolean changed;;
    do {
      changed = false;//初始化为没发生数据交换的状态
      for (i = 0; i < len - 1 ; i++) {
        if (arr[i] > arr[i + 1]) {  //前比后大则交换
          temp = arr[i];
          arr[i] = arr[i + 1];
          arr[i + 1] = temp;
          changed = true;//若发生了数据交换,即进入if块
        }
      }
    } while (changed);
  }

2.传统冒泡排序中每一趟排序操作只能找到一个最大值或最小值,我们考虑利用在每趟排序中进行正向和反向两遍冒泡的方法一次可以得到两个最终值(最大者和最小者) , 从而使排序趟数几乎减少了一半。

算法实现

代码如下:

private void core(int[] array) {
        int arrayLength = array.length;

        int preIndex = 0;
        int backIndex = arrayLength - 1;
        while(preIndex < backIndex) {
            preSort(array, arrayLength, preIndex);
            preIndex++;

            if (preIndex >= backIndex) {//若相遇则结束
                break;
            }

            backSort(array, backIndex);
            backIndex--;
        }
    }

    // 从前向后排序
    private void preSort(int[] array, int length, int preIndex) {
        for (int i = preIndex + 1; i < length; i++) {
            if (array[preIndex] > array[i]) {
                ArrayUtils.swap(array, preIndex, i);
            }
        }
    }

    // 从后向前排序
    private void backSort(int[] array, int backIndex) {
        for (int i = backIndex - 1; i >= 0; i--) {
            if (array[i] > array[backIndex]) {
                ArrayUtils.swap(array, i, backIndex);
            }
        }
    }
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

复杂度分析

排序方法 时间复杂度 空间复杂度 稳定性 复杂性
平均情况 最坏情况 最好情况
双向冒泡排序 O(n2) O(n2) O(n) O(1) 稳定 简单

算法评价

  如果单纯从时间复杂度上来讨论,双向冒泡排序与冒泡排序算法复杂度是一致的。不过在双向冒泡排序算法中,我们引入了一些变量,以控制程序流程,在空间复杂度上虽然都O(1),不过双向冒泡排序还是会大一些(至少有多了两个位置指针)。从代码的复杂度上,双向冒泡排序算法会大一些。 
  不过在上面的冒泡排序算法中,我们了解到冒泡排序算法有一个“乌龟问题”。正是因为这个原因,我们引入了双向冒泡排序算法。这里我们可通过一个实例更加象形地了解它。 
  假设我们现在有一个待排序序列{6, 5, 4, 3, 2, 1}。分别使用单向和双向冒泡排序对其进行排序,两种排序算法的过程如下(左图为单向冒泡,右图为双向冒泡):

步骤 单向冒泡排序 双向冒泡排序
原始状态 [6, 5, 4, 3, 2, 1] [6, 5, 4, 3, 2, 1]
第 1 趟 [1, 6, 5, 4, 3, 2] [1, 6, 5, 4, 3, 2]
第 2 趟 [1, 2, 6, 5, 4, 3] [1, 5, 4, 3, 2, 6]
第 3 趟 [1, 2, 3, 6, 5, 4] [1, 2, 5, 4, 3, 6]
第 4 趟 [1, 2, 3, 4, 6, 5] [1, 2, 4, 3, 5, 6]
第 5 趟 [1, 2, 3, 4, 5, 6] [1, 2, 3, 4, 5, 6]

时间复杂度分析:若本身有序,则只执行n-1次比较,为O(N),若本身逆序则需要比较∑(i-1)其中i取值为从2到n,即为(n-1)+(n-2)+...+3+2+1,求和为n*(n-1)/2次,复杂度为O(n²)。

6. 交换排序—快速排序(Quick Sort)

基本思想:

1)选择一个基准元素,通常选择第一个元素或者最后一个元素,

2)通过一趟排序将待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的 元素值比基准值大。

3)此时基准元素在其排好序后的正确位置

4)然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。

快速排序的示例:

(a)一趟排序的过程:

(b)排序的全过程


算法的实现:

 递归实现:

  1.  
    [java]  view plain  copy
    1. package com;  
    2.   
    3. /* 
    4.  * Java实现快速排序算法 
    5.  * author:wyr 
    6.  * 2016-7-14 
    7.  */  
    8. public class QuickSort {  
    9.     public static void main(String[] args) {  
    10.       
    11.         int a[] = {3,1,5,7,2,4,9,6,10,8};    
    12.         QuickSort  obj=new QuickSort();  
    13.         System.out.println("初始值:");  
    14.         obj.print(a);  
    15.         int h=a.length-1;  
    16.         obj.quickSort(a,0,h);  
    17.         System.out.println("\n排序后:");  
    18.         obj.print(a);  
    19.     }  
    20.     private  void quickSort(int[] a,int low, int high) {  
    21.          if(low<high){ //如果不加这个判断递归会无法退出导致堆栈溢出异常  
    22.               int middle=getMiddle(a,low,high);  
    23.               quickSort(a,  0,  middle-1);          //递归对低子表递归排序    
    24.               quickSort(a,   middle + 1, high);        //递归对高子表递归排序    
    25.          }  
    26.     }  
    27.     public int getMiddle(int[] a, int low, int high){  
    28.           
    29.         int key = a[low];//基准元素,排序中会空出来一个位置  
    30.         while(low<high){  
    31.             while(low<high && a[high]>=key){//从high开始找比基准小的,与low换位置  
    32.                 high--;  
    33.             }  
    34.             a[low]=a[high];  
    35.             while(low<high && a[low]<=key){//从low开始找比基准大,放到之前high空出来的位置上  
    36.                 low++;  
    37.             }  
    38.             a[high]=a[low];  
    39.         }  
    40.         a[low]=key;//此时low=high 是基准元素的位置,也是空出来的那个位置  
    41.         return low;  
    42.         
    43.     }  
    44.     public void print(int a[]){  
    45.         for(int i=0;i<a.length;i++){  
    46.             System.out.print(a[i]+" ");  
    47.         }  
    48.     }  
    49. }  


分析:

快速排序是一个不稳定的排序方法。

快排的速度取决于递归树的深度,若根节点是中间值,树是平衡的,快排的效率就高。

在最优情况下,每次划分都很均匀,如果排n个关键字,则此时递归树深度为logn(向下取整)+1,则仅需递归logn次,耗时记为T(n)的话,第一次划分应该是需要扫描整个数组做n次比较,然后第二次划分成两段,每段各自需要耗时T(n/2),(注意这里是最优情况,平分两半),即

T(n)<=2T(n/2)+n,T(1)=0;

T(n)<=2(2T(n/4)+n/2)+n=4T(n/4)+2n;

T(n)<=4(2T(n/8)+n/4)+2n=8T(n/8)+3n;

...

T(n)<=nT(1)+(logn)*n=O(nlogn)

也就是说,在最优情况下,时间复杂度为O(nlogn)


在最坏的情况下,即数组是正序的或逆序的,每次划分都比上一次划分少一个记录的子序列,注意另一段位空。用递归树画出来就是一棵斜树,此时需要执行n-1次递归,且第i次划分需要经过n-i次比较才能找到第i个记录,也就是枢轴的位置,因此比较次数为

(i取值从1到n-1求和)∑(n-i)=n(n-1)/2,此时复杂度为O(n²)。


平均情况下,设枢轴的关键字应该在第K的位置(1<=k<=n),那么,

 T(n)=(2/n)*  (k取值从1到n)∑T(k)+n

由数学归纳法证明其数量级为O(nlogn)


空间复杂度:主要是递归造成的空间,最好情况下深度为logn+1,复杂度就位O(logn),最坏情况进行n-1次递归,空间为O(n),平均情况下空间复杂度也为O(logn)。这里去最坏情况下的空间复杂度。


快排的优化

1)优化枢轴的选择。快速排序是通常被认为在同数量级(O(nlog2n))的排序方法中平均性能最好的。但若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。为改进之,通常以“三者取中法”来选取基准记录,即将排序区间的两个端点与中点三个记录比较得出中间数为支点记录,以此来避免取到最大值或最小值,即int key=low+(high-low),甚至可以取9个数的中位数 。
2)当排序序列长度分割到一定程度时,使用插入排序对于N很小或局部有序的数组,直接插入排序的效率非常高。
3)在一次分割结束后,可以把与基准数相等的元素聚在一起,下次分割时忽略掉这些元素。
对于含有重复元素比较多的序列,这种优化方法效果比较好,可以减少很多跌代次数。
具本过程:
第一步:在划分过程,把与所选取的基准数相等的元素放在数组的两端。
第二步:划分结束后,把两端的与基准数相等的元素移到基准数最终位置的两侧。

4)使用多线程并行处理子划分。

在JDK8中,JAVA的排序
针对不同的情况使用了不同的排序算法,简单罗列下:
一、如果是简单对象数据,例如int,double,且数组长度在一定阀值内,则使用快排,如果在阀值外,则用归并。

1.需要排序的数组为a,判断数组的长度是否大于286,大于使用归并排序(merge sort),否则执行2。 

2.判断数组长度是否小于47,小于则采用直接插入排序,否则执行3。 

3.采用双轴三分快排(两个枢轴:对枢轴的选择是先采用近似算法计算数组长度的1/7,再取中位数做加减来得出包括中位数在内的5个点,最终取第二个和第四个点为枢轴。三个指针:分别是less,k,great。先说一下最终结果,less和great将数组分为3个部分,分别是小于less的,大于less小于great的元素和大于great的元素。 如何达到这个结果呢,初始时,less和great分别指向数组起始的元素和结束的元素。此时,所有的元素在less和great之间,即待处理的元素。随着程序的进行,小于less的元素逐步移动到less左边,大于great的元素移动到great右边。 另外有一个指针k表示处理到哪个元素了,初始值为less,结束值为great(这里的great是会动态改变的,但是大于great的元素一定是处理过的) 

4在排序完成后判断中间的区域是否过大,如果是,则执行5,否则递归执行步骤2。 

5.将等于pivot1或者pivot2的元素移动到两边,然后递归执行步骤2。

int seventh = ( length >> 3 ) + ( length >> 6 ) + 1 ; //近似算法计算数组长度的1/7
int e3 = (left + right) >>> 1 ; // 中位数
int e2 = e3 - seventh;
int e1 = e2 - seventh;
int e4 = e3 + seventh;

int e5 = e4 + seventh;

二、如果是复杂对象数组,则如果数组长度在一定阀值以内,则使用折半插入排序,如果长度在阀值外,则使用归并法,但是如果归并二分后小于阀值了,则在内部还是会使用折半插入排序

那么为什么复杂对象不使用快速排序呢?因为对于一个hashcode计算复杂的对象来说,移动的成本远低于比较的成本,即归并排序是稳定的,快排是不稳定的。

快速排序的改进

  1. private  void quickSort(int[] a,int low, int high) {  
  2.          if((high-low)>MAX_LENGTH_INSERT_SORT){ //若小于这个阈值则进行直接插入排序
  3.               int middle=getMiddle(a,low,high);  
  4.               quickSort(a,  0,  middle-1);          //递归对低子表递归排序    
  5.               quickSort(a,   middle + 1, high);        //递归对高子表递归排序    
  6.          }else{
  7.             InserSort(a);
  8.          }
  9.     }  

MAX_LENGTH_INSERT_SORT阈值的取值有资料认为7合适,有资料认为50合适,实际应用可适当调整,在JAVA底层快速排序实现中用的是27;


7. 归并排序(Merge Sort)

一、归并排序

归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

归并过程为:比较a[i]和a[j]的大小,若a[i]≤a[j],则将第一个有序表中的元素a[i]复制到r[k]中,并令i和k分别加上1;否则将第二个有序表中的元素a[j]复制到r[k]中,并令j和k分别加上1,如此循环下去,直到其中一个有序表取完,然后再将另一个有序表中剩余的元素复制到r中从下标k到下标t的单元。归并排序的算法我们通常用递归实现,先把待排序区间[s,t]以中点二分,接着把左边子区间排序,再把右边子区间排序,最后把左区间和右区间用一次归并操作合并成有序的区间[s,t]。

二、归并操作

三、两路归并算法

1、算法基本思路

     设两个有序的子文件(相当于输入堆)放在同一向量中相邻的位置上:R[low..m],R[m+1..high],先将它们合并到一个局部的暂存向量R1(相当于输出堆)中,待合并完成后将R1复制回R[low..high]中。

(1)合并过程

     合并过程中,设置i,j和p三个指针,其初值分别指向这三个记录区的起始位置。合并时依次比较R[i]和R[j]的关键字,取关键字较小的记录复制到R1[p]中,然后将被复制记录的指针i或j加1,以及指向复制位置的指针p加1。
     重复这一过程直至两个输入的子文件有一个已全部复制完毕(不妨称其为空),此时将另一非空的子文件中剩余记录依次复制到R1中即可。

(2)动态申请R1

     实现时,R1是动态申请的,因为申请的空间可能很大,故须加入申请空间是否成功的处理。



四、归并排序

归并排序有两种实现方法:自底向上和自顶向下。下面说说自顶向下的方法    

(1)分治法的三个步骤

设归并排序的当前区间是R[low..high],分治法的三个步骤是:
①分解:将当前区间一分为二,即求分裂点        
②求解:递归地对两个子区间R[low..mid]和R[mid+1..high]进行归并排序;
③组合:将已排序的两个子区间R[low..mid]和R[mid+1..high]归并为一个有序的区间R[low..high]。
递归的终结条件:子区间长度为1(一个记录自然有序)。

(2)具体算法


void MergeSortDC(SeqList R,int low,int high)
 {//用分治法对R[low..high]进行二路归并排序
 int mid;
 if(low<high){//区间长度大于1
  mid=(low+high)/2; //分解
  MergeSortDC(R,low,mid); //递归地对R[low..mid]排序
  MergeSortDC(R,mid+1,high); //递归地对R[mid+1..high]排序
  Merge(R,low,mid,high); //组合,将两个有序区归并为一个有序区
 }
 }//MergeSortDC



(3)算法MergeSortDC的执行过程

算法MergeSortDC的执行过程如下图所示的递归树。


五、算法分析

1、稳定性

归并排序是一种稳定的排序。

2、存储结构要求

可用顺序存储结构。也易于在链表上实现。

3、时间复杂度

对长度为n的文件,需进行 趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlgn)。

4、空间复杂度

需要一个辅助向量来暂存两有序子文件归并的结果,故其辅助空间复杂度为O(n),显然它不是就地排序。

注意:

若用单链表做存储结构,很容易给出就地的归并排序。

5、比较操作的次数介于(nlogn) / 2和nlogn - n + 1。

6、赋值操作的次数是(2nlogn)。归并算法的空间复杂度为:0 (n)

7、归并排序比较占用内存,但却是一种效率高且稳定的算法。

六、递代码实现


public class MergeSortTest {
  
 public static void main(String[] args) {
 int[] data = new int[] { 2, 4, 7, 5, 8, 1, 3, 6 };
 System.out.print("初始化:\t");
 print(data);
 System.out.println("");
  
 mergeSort(data, 0, data.length - 1);
   
 System.out.print("\n排序后: \t");
 print(data);
 }
  
 public static void mergeSort(int[] data, int left, int right) {
 if (left >= right)
  return;
 //两路归并
 // 找出中间索引
 int center = (left + right) / 2;
 // 对左边数组进行递归
 mergeSort(data, left, center);
 // 对右边数组进行递归
 mergeSort(data, center + 1, right);
 // 合并
 merge(data, left, center, center + 1, right);
 System.out.print("排序中:\t");
 print(data);
 }
  
 /**
 * 将两个数组进行归并,归并前面2个数组已有序,归并后依然有序
 *
 * @param data
 *  数组对象
 * @param leftStart
 *  左数组的第一个元素的索引
 * @param leftEnd
 *  左数组的最后一个元素的索引
 * @param rightStart
 *  右数组第一个元素的索引
 * @param rightEnd
 *  右数组最后一个元素的索引
 */
 public static void merge(int[] data, int leftStart, int leftEnd,
  int rightStart, int rightEnd) {
 int i = leftStart;
 int j = rightStart;
 int k = 0;
 // 临时数组
 int[] temp = new int[rightEnd - leftStart + 1]; //创建一个临时的数组来存放临时排序的数组
 // 确认分割后的两段数组是否都取到了最后一个元素
 while (i <= leftEnd && j <= rightEnd) {
  // 从两个数组中取出最小的放入临时数组
  if (data[i] > data[j]) {
  temp[k++] = data[j++];
  } else {
  temp[k++] = data[i++];
  }
 }
 // 剩余部分依次放入临时数组(实际上两个while只会执行其中一个)
 while (i <= leftEnd) {
  temp[k++] = data[i++];
 }
 while (j <= rightEnd) {
  temp[k++] = data[j++];
 }
 k = leftStart;
 // 将临时数组中的内容拷贝回原数组中 // (原left-right范围的内容被复制回原数组)
 for (int element : temp) {
  data[k++] = element;
 }
 }
  
 public static void print(int[] data) {
 for (int i = 0; i < data.length; i++) {
  System.out.print(data[i] + "\t");
 }
 System.out.println();
 }
}
以上为递归版归并排序。

时间复杂度分析:

    总时间=分解时间+解决问题时间+合并时间。分解时间就是把一个待排序序列分解成两序列,时间为一常数,时间复杂度o(1).,解决问题时间是两个递归式,把一个规模为n的问题分成两个规模分别为n/2的子问题,时间为2T(n/2).合并时间复杂度为o(n)。总时间T(n)=2T(n/2)+o(n).,这个递归式可以用递归树来解,,其解是o(nlogn).。

用递归树的方法解递归式T(n)=2T(n/2)+o(n):

假设解决最后的子问题用时为常数c,则对于n个待排序记录来说整个问题的规模为cn。

 

从这个递归树可以看出,第一层时间代价为cn,第二层时间代价为cn/2+cn/2=cn.....每一层代价都是cn,总共有logn+1层。所以总的时间代价为cn*(logn+1).时间复杂度是o(nlogn).


      一趟归并需要将a[1]~a[n]中相邻的长度为h的有序序列两两归并。并将结果放入临时数组TR1[1]~TR1[n]中。这需要将待排序序列的所有记录扫描一遍,因此耗时O(n)。而由完全二叉树深度可知,整个归并排序需要进行logn(向下取整)次,因此总时间复杂度为O(nlogn)。而且这是归并排序最好、最差、平均的时间性能。

空间复杂度分析:

    由于归并排序在归并过程中需要与原始记录序列同样数量的存储空间存放归并结果以及递归时深度为logn的栈空间,因此空间复杂度为O(n+logn)。在下面非递归版的介绍中,因为少了递归时深度为logn的栈,只用到了临时数组TR,因此空间复杂度O(n),并且在时间上有一点提升,因此使用归并排序时,应该尽量使用非递归方法。

归并排序时稳定的算法     

非递归版归并排序

public class sort{ 
public static void merge_sort(int[] arr) {
    int len = arr.length;
    int[] result = new int[len];
    int block, start;
    
    
    // 原版代码的迭代次数少了一次,没有考虑到奇数列数组的情况
    for(block = 1; block < len; block *= 2) {//每次划分数组都增大为原来的两倍
        for(start = 0; start <len; start += 2 * block) {//每次都从已经排序好的数组后面开始排序
            int low = start;
            int mid = Math.min(len,strat+bolck);//即不能比len大,bolck为划分后每段的长度,为了碰到奇数列数组时能遍历完
            int high = Math.min(len,strat+2*bolck);
            //两个块的起始下标及结束下标
            int start1 = low, end1 = mid;
            int start2 = mid, end2 = high;
            //开始对两个block进行归并排序
            while (start1 < end1 && start2 < end2) {
	        result[low++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
            }
            while(start1 < end1) {
	        result[low++] = arr[start1++];
            }
            while(start2 < end2) {
	        result[low++] = arr[start2++];
            }
        }
     

   //arr用作结果记录,每次都把排好序的数组result给arr,使arr保持最新排序状态
   //result用作临时数组
	int[] temp = arr;
	arr = result;
	result = temp;
        
    }
    
    
    //将排序好的结果arr传给result输出
    result = arr;     
}
    //输出结果
    public static void main(String args[]){
        int a[] = {6,6,2,1,3,7,6,2,6};
        System.out.print(a.toString());
        //输出[6, 6, 2, 1, 3, 7, 6, 2, 6]
        merge_sort(a);
        System.out.print(a.toString());
        //输出[1, 2, 2, 3, 6, 6, 6, 6, 7]
    }
}




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值