Fork me on GitHub

归并、希尔、快速排序

(一)

  归并排序的运行时间为O(N*logN),一个缺点是他需要在存储器中有另一个大小等于被排序的数据项数目的空间。

  归并算法的中心是归并两个已经有序的数组。

package sort;
//归并排序
public class MergeSort {
    private  static int[] copyArray;
    public static void main(String[] args){
        int[] arrayA = {1,3,7,12};
        int[] arrayB = {5,7,9,10};
        show(merge(arrayA,arrayB));
    }
//归并两个已排好序的数组 public static int[] merge(int[] arrayA,int[] arrayB){ int sizeA = arrayA.length; int sizeB = arrayB.length; int[] array = new int[sizeA+sizeB]; int i=0,j = 0,k=0; while( i<sizeA && j<sizeB){ if(arrayA[i] <= arrayB[j]){ array[k++] = arrayA[i++]; }else{ array[k++] = arrayB[j++]; } } while(i<sizeA){ array[k++] = arrayA[i++]; } while(j<sizeB){ array[k++] = arrayB[j++]; } return array; } //展示 public static void show(int[] array){ for(int temp:array) System.out.print(temp + " "); System.out.println(""); } }

结果:

  方法体有三个while循环,第一个while循环沿着arrayA和arrayB走,比较它们的数据项,并且复制较小的数到array。第二个while循环处理当arrayB的所有数据项都已经移出,而arrayA还有剩余项的情况,这个循环把剩余的数据项直接从arrayA复制到array中。第三个while处理类似的情况。

  归并排序的思想是把一个数组分成两半,排序每一半,然后用merge()方法把数组的两半归并成一个有序的数组。通过递归,再把每一半都分成两个四分之一,对每个四分之一部分排序,然后把它们归并成有序的一半。

package sort;
//归并排序
public class MergeSort {
    //做临时存储的数组
    private  static int[] copyArray;

    public static void main(String[] args){
        int[] array = {1,5,2,7,4,6,11,9};
        copyArray = new int[array.length];
        sort(array,0,array.length-1);
        show(array);
    }
    //递归排序
    public static void sort(int[] array,int start,int end){
        if(start >= end)
            return;
        else{
            int mid = (start+end)/2;
            sort(array,start,mid);
            sort(array,mid+1,end);
            merge(array,start,mid,end);
            }
    }
    //归并两个已排好序的数组
    public static void merge(int[] array,int start,int mid,int end){
        int i=start,j=mid+1,k=start;
        while( i<=mid && j<=end){
            if(array[i] <= array[j]){
                copyArray[k++] = array[i++];
            }else{
                copyArray[k++] = array[j++];
            }
        }
        while(i<=mid){
            copyArray[k++] = array[i++];
        }
        while(j<=end){
            copyArray[k++] = array[j++];
        }
        //将排好序的数据项放回
        for(int m=start;m<k;m++)
            array[m] = copyArray[m];
    }
    //展示
    public static void show(int[] array){
        for(int temp:array)
            System.out.print(temp + " ");
        System.out.println("");
    }
}

结果:

 (二)

  希尔排序基于插入排序,具体排序的过程不再描述,下面我们说说插入排序带来的问题:假设一个很小的数据项在很靠近右端的位置上,这里本来应该是比较大的数据所在的位置,若要把这个小数据项移动到左边的正确位置上,所有的中间数据项都必须向右移动一位,这个步骤对每一个数据项都执行了将近N次的复制。虽不是所有的数据项都必须移动N个位置,但是数据项平均移动了N/2个位置,这就执行了N次N/2的移位,总共N2/2,所以插入排序效率是O(N2)。

  N-增量排序:希尔排序通过加大插入排序中的元素之间的间隔,并在这些有间隔的元素中进行插入排序,从而使数据项能大幅度地移动。当这些数据排过一趟序后,希尔排序算法减小数据项的间隔再进行排序,依次进行下去。这里的以h=3*h+1为例

对于一个长度为1000的数组,1093太大,所以先用364-增量排序,完成一次排序后,利用公式倒推来减小间隔,h=(h-1)/3。

public class ShellSort {
    
    private int[] array;
    private int index;
    //构造方法
    ShellSort(int max){
        array = new int[max];
        index = 0;
    }
    //希尔排序
    public void sort(){
        int h = 1;
        while(h <index/3){
            h = 3*h+1;
        }
        while(h>0){
            for(int out=h;out<index;out++){
                int temp = array[out];
                int in = out;
                while(in>=h&&array[in-h]>temp){
                    array[in] = array[in-h];
                    in-=h;
                }
                array[in] = temp;
            }
            h = (h-1)/3;
        }
    }
    //插入
    public void insert(int data){
        if(index<array.length)
            array[index++] = data;
    }
    //遍历
    public void show(){
        for(int i=0;i<index;i++)
            System.out.print(array[i]+" ");
        System.out.println("");
    }
}

测试类:

public class Test {
    @org.junit.Test
    public void test(){
        ShellSort sh = new ShellSort(15);
        for(int i=0;i<10;i++)
            sh.insert((int)(java.lang.Math.random()*99));
        System.out.print("排序前:");
        sh.show();
        sh.sort();
        System.out.print("排序后:");
        sh.show();
    }
}

结果:

(三)

       划分是快速排序的根本机制。划分数据就是把数据分成两组,使所有关键字大于特定值的数据项在一组,使所有关键字小于特定值的数据项在另一组。根据划分的原因,可以选择任何值作为枢纽。

       划分算法由两个指针开始工作,两个指针分别指向数组的两头。在左边的指针leftIndex向右移动,而在右边的指针rightIndex向左移动。leftIndex标记比枢纽大的数据项,rightIndex标记比枢纽小的数据项,然后交换两个数据项。交换之后继续移动,继续这个循环,直到两个指针相遇。

  快速排序三个步骤:(1)把数组划分成左边较小的一组和右边较大的一组;(2)调用自身对左边的子数组排序;(3)再次调用自身对右边的子数组排序。

  第一种快速排序:以数组最右端数据项作为枢纽。

//快速排序
public class QuickSort {
    private int[] array;
    private int index;
    
    QuickSort(int max){
        array = new int[max];
        index = 0;
    }
    public void quickSort(){
        sort(0,index-1);
    }
    public void sort(int left,int right){
        if(left>=right)
            return;
        else{
            //以数组右边第一个数作为划分值
            int temp = array[right];
            //得到划分值所在的下标
            int partition = partitionArray(left,right,temp);
            sort(left,partition-1);
            sort(partition+1,right);
        }
    }
    public int partitionArray(int left,int right,int value){
        int leftIndex = left;
        int rightIndex = right-1;
        while(true){
            while(array[leftIndex]<value)//一定会停止,最差在最右端
                leftIndex++;
            while(rightIndex >= left && array[rightIndex]>=value)//这个等号决定了把与枢纽相等的值放到了右边
                rightIndex--;
            if(leftIndex>=rightIndex)
                break;
            else
                swap(leftIndex,rightIndex);//交换
        }
        swap(leftIndex,right);//将最右端的枢纽值放到正确位置
        return leftIndex;
    }
    //交换
    public void swap(int i,int j){
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    
    //插入
    public void insert(int data){
        if(index<array.length)
            array[index++] = data;
    }
    //遍历
    public void show(){
        for(int i=0;i<index;i++)
            System.out.print(array[i]+" ");
        System.out.println("");
    }
}

测试类:

public class Test {
    public static void main(String[] args){
        QuickSort sh = new QuickSort(100);
        for(int i=0;i<10;i++)
            sh.insert((int)(java.lang.Math.random()*99));
        System.out.print("排序前:");
        sh.show();
        sh.quickSort();
        System.out.print("排序后:");
        sh.show();
    }
}

结果:

       理想状态下,应该选择被排序的数据项的中值作为枢纽,也就是说,应该有一般的数据项大于枢纽,一半的数据项小于枢纽,这是最优的情况。划分成一大一小的子数组,会降低算法的效率,因为较大的子数组必须要被划分更多次。上面地例子选择最右端的数据项作为枢纽,有可能出现最差的情况。下面的例子是一种改进,改进的方法是找到数组的第一个、最后一个及中间值它们三个中的居中值作为枢纽。

  第二种快速排序:三数据项取中作为枢纽

//快速排序
public class ImproveQuickSort {
    private int[] array;
    private int index;
    
    ImproveQuickSort(int max){
        array = new int[max];
        index = 0;
    }
    public void quickSort(){
        sort(0,index-1);
    }
    public void sort(int left,int right){
        if(right-left+1 <= 3)
            manualSort(left,right);
        else{
            //取三个数的中位数
            int temp = getPivot(left,right);
            //得到划分值所在的下标
            int partition = partitionArray(left,right,temp);
            sort(left,partition-1);
            sort(partition+1,right);
        }
    }
    //数组的size小于等于3排序
    public void manualSort(int left,int right){
        int size = right-left+1;
        if(size<=1)
            return;
        else if(size == 2){
            if(array[left]>array[right])
                swap(left,right);
        }else{
            int mid = (left+right)/2;
            if(array[left]>array[right])
                swap(left,right);
            if(array[mid]>array[right])
                swap(mid,right);
            if(array[left]>array[mid])
                swap(left,mid);
        }
    }
    //得到中位数作为枢纽,并且把它放到right-1的位置
    public int getPivot(int left,int right){
        int mid = (left+right)/2;
        if(array[left]>array[right])
            swap(left,right);
        if(array[mid]>array[right])
            swap(mid,right);
        if(array[left]>array[mid])
            swap(left,mid);
        swap(mid,right-1);
        return array[right-1];
    }
    public int partitionArray(int left,int right,int value){
        //只需要排序left+1到rigt-1,这两边的数已在正确位置
        int leftIndex = left+1;
        int rightIndex = right-2;
        while(true){
            while( array[leftIndex]<value)
                leftIndex++;//最差情况在最右端一定会停止
            while(array[rightIndex]>value)
                rightIndex--;//最差情况在最左端一定会停止
            if(leftIndex>=rightIndex)
                break;
            else{
                if(array[leftIndex] == array[rightIndex])//防止相等的两个数一直在交换,陷入死循环
                    leftIndex++;//或者rightIndex--
                else
                    swap(leftIndex,rightIndex);//交换
            }
        }
        swap(leftIndex,right-1);//将枢纽值放到正确位置
        return leftIndex;
    }
    //交换
    public void swap(int i,int j){
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    
    //插入
    public void insert(int data){
        if(index<array.length)
            array[index++] = data;
    }
    //遍历
    public void show(){
        for(int i=0;i<index;i++)
            System.out.print(array[i]+" ");
        System.out.println("");
    }
}

测试类:

public class Test {
    public static void main(String[] args){
        ImproveQuickSort sh = new ImproveQuickSort(100);
        for(int i=0;i<10;i++)
            sh.insert((int)(java.lang.Math.random()*99));
        System.out.print("排序前:");
        sh.show();
        sh.quickSort();
        System.out.print("排序后:");
        sh.show();
    }
}

结果:

  快速排序的效率是O(N*logN);

 

posted @ 2018-01-23 16:54  爱跑步的星仔  阅读(218)  评论(0编辑  收藏  举报