各類排序算法詳解(java版)

摘要

整理了一些有關於排序算法的資料,用java手寫了一些,有些博主懶得寫代碼了就直接copy了網上的代碼。在文章的最後還給出排序算法穩定性的定義以及哪些是穩定的排序算法。

目錄

一、快速排序
二、堆排序
三、插入排序
四、冒泡排序
五、希爾排序
六、歸併排序
七、選擇排序
八、排序算法穩定性


一、快速排序

所謂的快速排序的思想就是,首先把數組的第一個數拿出來做爲一個key,在前後分別設置一個i,j做爲標識,然後拿這個key對這個數組從後面往前遍歷,及j–,直到找到第一個小於這個key的那個數,然後交換這兩個值,交換完成後,我們拿着這個key要從i往後遍歷了,及i++;一直循環到i=j結束,當這裏結束後,我們會發現大於這個key的值都會跑到這個key的後面,不是的話就可能你寫錯了,小於這個key的就會跑到這個值的前面;然後我們對這個分段的數組再時行遞歸調用就可以完成整個數組的排序。
用圖形法表示由下:
這裏寫圖片描述

java代碼實現如下:

package sort;

import java.util.Arrays;

public class quckSort2 {

    public static void sort(int a[],int low,int high){
        if(low>high)
            return;
        int i,j;
        i=low;
        j=high;
        int tmp=a[i];
        while(i<j){
            while(i<j && a[j]>tmp){
                j--;
            }
            if(i<j){
                a[i]=a[j];
                i++;
                a[j]=tmp;
            }
            while(i<j && a[i]<tmp){
                i++;
            }
            if(i<j){
                a[j]=a[i];
                j--;
                a[i]=tmp;
            }

        }
        snp(a);
        sort(a,i+1,high);
        sort(a,low,i-1);

    }

    public static void snp(int[] arrays) {
        for (int i = 0; i < arrays.length; i++) {
            System.out.print(arrays[i] + " ");
        }
        System.out.println();
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        //int a[] = { 49, 38, 65, 97, 76, 13, 27, 49 };
        int a[] = { 3, 3, 2, 1, 6, 5, 4, 2 };
        sort(a,0,a.length-1);
        System.out.println(Arrays.toString(a));
    }

}

二、堆排序

堆是一棵順序存儲的完全二叉樹。

其中每個結點的關鍵字都不大於其孩子結點的關鍵字,這樣的堆稱爲小根堆
其中每個結點的關鍵字都不小於其孩子結點的關鍵字,這樣的堆稱爲大根堆

舉例來說,對於n個元素的序列{R0, R1, … , Rn}當且僅當滿足下列關係之一時,稱之爲堆:
(1) Ri <= R2i+1 且 Ri <= R2i+2 (小根堆)
(2) Ri >= R2i+1 且 Ri >= R2i+2 (大根堆)
其中i=1,2,…,n/2向下取整;

這裏寫圖片描述

如上圖所示,序列R{3, 8, 15, 31, 25}是一個典型的小根堆。堆中有兩個父結點,元素3和元素8。

元素3在數組中以R[0]表示,它的左孩子結點是R[1],右孩子結點是R[2]。

元素8在數組中以R[1]表示,它的左孩子結點是R[3],右孩子結點是R[4],它的父結點是R[0]。可以看出,它們滿足以下規律:

設當前元素在數組中以R[i]表示,那麼,

(1) 它的左孩子結點是:R[2*i+1];
(2) 它的右孩子結點是:R[2*i+2];
(3) 它的父結點是:R[(i-1)/2];
(4) R[i] <= R[2*i+1] 且 R[i] <= R[2i+2]。

調整過程:
首先,按堆的定義將數組R[0..n]調整爲堆(這個過程稱爲創建初始堆),交換R[0]和R[n];
然後,將R[0..n-1]調整爲堆,交換R[0]和R[n-1];
如此反覆,直到交換了R[0]和R[1]爲止。
以上思想可歸納爲兩個操作:

(1)根據初始數組去構造初始堆(構建一個完全二叉樹,保證所有的父結點都比它的孩子結點數值大)。

(2)每次交換第一個和最後一個元素,輸出最後一個元素(最大值),然後把剩下元素重新調整爲大根堆。

當輸出完最後一個元素後,這個數組已經是按照從小到大的順序排列了。
先通過詳細的實例圖來看一下,如何構建初始堆。
設有一個無序序列 { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 }。
這裏寫圖片描述

構造了初始堆後,我們來看一下完整的堆排序處理:
還是針對前面提到的無序序列 { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 } 來加以說明。
這裏寫圖片描述

package sort;

public class Heapsort {

    private int[] a;
    public Heapsort(int[] a){
        this.a=a;
    }

    public void adjust(int index,int lenthOfArray){
        int lenth=index;
        int lenth2=lenthOfArray;
        int parent=(lenth-1)/2;
        int child=parent*2+1;
        int tmp=a[parent];
        //System.out.println("lenth="+lenth);
        while(child<=lenth2){
            if((child+1) <= lenth2){
                if(a[child+1] < a[child])
                child++;
            }
            if(a[child]<tmp){
                a[parent]=a[child];
                a[child]=tmp;

            }
            parent=child;
            child=parent*2+1;
            tmp=a[parent];
        }
    }

    public void display(){
        for(int j=0;j<a.length;j++){
            System.out.print(a[j]+" ");
        }
        System.out.println();
    }
    public void heapSort(){
        for(int i=a.length-1;i>=0;i--){
            for(int j=a.length-1;j>=0;j--){
                adjust(j,i);

            }
            System.out.println(a[0]);
            a[0]=a[i];
//          adjust(i,i);
        }

    }

    public static void main(String[] args) {
        //int[]  a={1,3,4,5,2,6,9,7,8,0};
        int[]  a={5,66,9,88,2,44,7,6,99,110,1000,1256,82,3,5,7,6,9};
        Heapsort heapsort =new Heapsort(a);
        heapsort.heapSort();
    }

}

三、插入排序

以數組{38,65,97,76,13,27,49}爲例,
這裏寫圖片描述

Java實現如下

public class InsertSort {
    public static void insertSort(int[] a) {
        int i, j, insertNote;// 要插入的數據
        for (i = 1; i < a.length; i++) {// 從數組的第二個元素開始循環將數組中的元素插入
            insertNote = a[i];// 設置數組中的第2個元素爲第一次循環要插入的數據
            j = i - 1;
            while (j >= 0 && insertNote < a[j]) {
                a[j + 1] = a[j];// 如果要插入的元素小於第j個元素,就將第j個元素向後移動
                j--;
            }
            a[j + 1] = insertNote;// 直到要插入的元素不小於第j個元素,將insertNote插入到數組中
        }
    }
    public static void main(String[] args) {
        int a[] = { 38,65,97,76,13,27,49 };
        insertSort(a);
        System.out.println(Arrays.toString(a));
    }
}

四、冒泡排序

(1)基本思想:在要排序的一組數中,對當前還未排好序的範圍內的全部數,自上而下對相鄰的兩個數依次進行比較和調整,讓較大的數往下沉,較小的往上冒。即:每當兩相鄰的數比較後發現它們的排序與排序要求相反時,就將它們互換。
(2)實例:
這裏寫圖片描述

public class bubbleSort {
    public  bubbleSort(){
        int a[]={49,38,65,97,76,13,27,49,78,34,12,64,5,4,62,99,98,54,56,17,18,23,34,15,35,25,53,51};
        int temp=0;
        for(int i=0;i<a.length-1;i++){
            for(int j=0;j<a.length-1-i;j++){
            if(a[j]>a[j+1]){
                temp=a[j];
                a[j]=a[j+1];
                a[j+1]=temp;
            }
            }
        }
        for(int i=0;i<a.length;i++)
            System.out.println(a[i]);   
    }
}

五、希爾排序

希爾(Shell)排序又稱爲縮小增量排序,它是一種插入排序。它是直接插入排序算法的一種威力加強版。該方法因DL.Shell於1959年提出而得名。
希爾排序的基本思想是:
把記錄按步長 gap 分組,對每組記錄採用直接插入排序方法進行排序。
隨着步長逐漸減小,所分成的組包含的記錄越來越多,當步長的值減小到 1 時,整個數據合成爲一組,構成一組有序記錄,則完成排序。
我們來通過演示圖,更深入的理解一下這個過程。
這裏寫圖片描述
在上面這幅圖中:

初始時,有一個大小爲 10 的無序序列。

在第一趟排序中,我們不妨設 gap1 = N / 2 = 5,即相隔距離爲 5 的元素組成一組,可以分爲 5 組。

接下來,按照直接插入排序的方法對每個組進行排序。

在第二趟排序中,我們把上次的 gap 縮小一半,即 gap2 = gap1 / 2 = 2 (取整數)。這樣每相隔距離爲 2 的元素組成一組,可以分爲 2 組。

按照直接插入排序的方法對每個組進行排序。

在第三趟排序中,再次把 gap 縮小一半,即gap3 = gap2 / 2 = 1。 這樣相隔距離爲 1 的元素組成一組,即只有一組。

按照直接插入排序的方法對每個組進行排序。此時,排序已經結束。

需要注意一下的是,圖中有兩個相等數值的元素 5 和 5 。我們可以清楚的看到,在排序過程中,兩個元素位置交換了。

所以,希爾排序是不穩定的算法。
這裏寫圖片描述

package sort;

public class ShellSort {

    public static void shellSort(int[] a){
        int gap=a.length/2;
        while(gap>=1){
            for(int i=0;(i+gap)<a.length;i++){
                for(int j=i;j>=0;j=j-gap){
                    int tmp=a[j];
                    if(a[j+gap]<tmp){
                        a[j]=a[j+gap];
                        a[j+gap]=tmp;
    //                  tmp=a[i+gap];
                    }
                }
            }
            gap=gap/2;
            printAll(a);
        }
    }
    // 打印完整序列
    public static void printAll(int[] list) {
        for (int value : list) {
            System.out.print(value + " ");
        }
        System.out.println();
    }
    public static void main(String[] args) {
         int[] array = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5, 1,0 };
         shellSort(array);
    }

}

六、歸併排序

歸併排序是建立在歸併操作上的一種有效的排序算法,該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。
將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲二路歸併。

歸併排序的基本思想
將待排序序列R[0…n-1]看成是n個長度爲1的有序序列,將相鄰的有序表成對歸併,得到n/2個長度爲2的有序表;將這些有序序列再次歸併,得到n/4個長度爲4的有序序列;如此反覆進行下去,最後得到一個長度爲n的有序序列。

綜上可知:

歸併排序其實要做兩件事:

(1)“分解”——將序列每次折半劃分。

(2)“合併”——將劃分後的序列段兩兩合併後排序。

我們先來考慮第二步,如何合併?

在每次合併過程中,都是對兩個有序的序列段進行合併,然後排序。

這兩個有序序列段分別爲 R[low, mid] 和 R[mid+1, high]。

先將他們合併到一個局部的暫存數組R2中,帶合併完成後再將R2複製回R中。

爲了方便描述,我們稱 R[low, mid] 第一段,R[mid+1, high] 爲第二段。

每次從兩個段中取出一個記錄進行關鍵字的比較,將較小者放入R2中。最後將各段中餘下的部分直接複製到R2中。

經過這樣的過程,R2已經是一個有序的序列,再將其複製回R中,一次合併排序就完成了。
這裏寫圖片描述
在某趟歸併中,設各子表的長度爲gap,則歸併前R[0…n-1]中共有n/gap個有序的子表:R[0…gap-1], R[gap…2*gap-1], … , R[(n/gap)*gap … n-1]。

調用Merge將相鄰的子表歸併時,必須對錶的特殊情況進行特殊處理。

若子表個數爲奇數,則最後一個子表無須和其他子表歸併(即本趟處理輪空):若子表個數爲偶數,則要注意到最後一對子表中後一個子表區間的上限爲n-1。

package sort;

public class MergeSort {

    public void Merge(int[] array, int low, int mid, int high) {
        int i = low; // i是第一段序列的下標
        int j = mid + 1; // j是第二段序列的下標
        int k = 0; // k是臨時存放合併序列的下標
        int[] array2 = new int[high - low + 1]; // array2是臨時合併序列

        // 掃描第一段和第二段序列,直到有一個掃描結束
        while (i <= mid && j <= high) {
            // 判斷第一段和第二段取出的數哪個更小,將其存入合併序列,並繼續向下掃描
            if (array[i] <= array[j]) {
                array2[k] = array[i];
                i++;
                k++;
            } else {
                array2[k] = array[j];
                j++;
                k++;
            }
        }

        // 若第一段序列還沒掃描完,將其全部複製到合併序列
        while (i <= mid) {
            array2[k] = array[i];
            i++;
            k++;
        }

        // 若第二段序列還沒掃描完,將其全部複製到合併序列
        while (j <= high) {
            array2[k] = array[j];
            j++;
            k++;
        }

        // 將合併序列複製到原始序列中
        for (k = 0, i = low; i <= high; i++, k++) {
            array[i] = array2[k];
        }
    }

    public void MergePass(int[] array, int gap, int length) {
        int i = 0;

        // 歸併gap長度的兩個相鄰子表
        for (i = 0; i + 2 * gap - 1 < length; i = i + 2 * gap) {
            Merge(array, i, i + gap - 1, i + 2 * gap - 1);
        }

        // 餘下兩個子表,後者長度小於gap
        if (i + gap - 1 < length) {
            Merge(array, i, i + gap - 1, length - 1);
        }
    }

    public int[] sort(int[] list) {
        for (int gap = 1; gap < list.length; gap = 2 * gap) {
            MergePass(list, gap, list.length);
            System.out.print("gap = " + gap + ":\t");
            this.printAll(list);
        }
        return list;
    }

    // 打印完整序列
    public void printAll(int[] list) {
        for (int value : list) {
            System.out.print(value + "\t");
        }
        System.out.println();
    }
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        int[] array = {9, 1, 5, 3, 4, 2, 6, 8, 7};

        MergeSort merge = new MergeSort();
        System.out.print("排序前:\t\t");
        merge.printAll(array);
        merge.sort(array);
        System.out.print("排序後:\t\t");
        merge.printAll(array);
    }

}

七、選擇排序

a) 原理:每一趟從待排序的記錄中選出最小的元素,順序放在已排好序的序列最後,直到全部記錄排序完畢。也就是:每一趟在n-i+1(i=1,2,…n-1)個記錄中選取關鍵字最小的記錄作爲有序序列中第i個記錄。基於此思想的算法主要有簡單選擇排序、樹型選擇排序和堆排序。(這裏只介紹常用的簡單選擇排序)

b) 簡單選擇排序的基本思想:給定數組:int[] arr={裏面n個數據};第1趟排序,在待排序數據arr[1]~arr[n]中選出最小的數據,將它與arrr[1]交換;第2趟,在待排序數據arr[2]~arr[n]中選出最小的數據,將它與r[2]交換;以此類推,第i趟在待排序數據arr[i]~arr[n]中選出最小的數據,將它與r[i]交換,直到全部排序完成。

c) 舉例:數組 int[] arr={5,2,8,4,9,1};


第一趟排序: 原始數據:5 2 8 4 9 1

最小數據1,把1放在首位,也就是1和5互換位置,

排序結果:1 2 8 4 9 5


第二趟排序:

第1以外的數據{2 8 4 9 5}進行比較,2最小,

排序結果:1 2 8 4 9 5


第三趟排序:

除1、2以外的數據{8 4 9 5}進行比較,4最小,8和4交換

排序結果:1 2 4 8 9 5


第四趟排序:

除第1、2、4以外的其他數據{8 9 5}進行比較,5最小,8和5交換

排序結果:1 2 4 5 9 8


第五趟排序:

除第1、2、4、5以外的其他數據{9 8}進行比較,8最小,8和9交換

排序結果:1 2 4 5 8 9


注:每一趟排序獲得最小數的方法:for循環進行比較,定義一個第三個變量temp,首先前兩個數比較,把較小的數放在temp中,然後用temp再去跟剩下的數據比較,如果出現比temp小的數據,就用它代替temp中原有的數據。具體參照後面的代碼示例,相信你在學排序之前已經學過for循環語句了,這樣的話,這裏理解起來就特別容易了。

package sort;

public class SelectionSort {

    public static void swap(int[] a,int i,int j){
        int tmp=a[i];
        a[i]=a[j];
        a[j]=tmp;
    }

    public static int findMinNumber(int[] a,int i){
        int minNumber = a[i];
        int subscript=i;
        for(int j=i;j<a.length;j++){
            if(a[j]<minNumber){
                minNumber=a[j];
                subscript=j;
            }
        }
        if(i!=subscript){
            swap(a,i,subscript);
        }       
        return minNumber;
    }

    public static void selectionSort(int[] a){
        int lenth=a.length;
        for(int i=0;i<lenth;i++){
            findMinNumber(a,i);
        }
        printAll(a);
    }

    // 打印完整序列
    public static void printAll(int[] a) {
        for (int value : a) {
            System.out.print(value + " ");
        }
        System.out.println();
    }
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        int[] array = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5};
        //initSelectionSort(array);
        SelectionSort.selectionSort(array);
    }

}

八、排序算法的穩定性

假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,ri=rj,且ri在rj之前,而在排序後的序列中,ri仍在rj之前,則稱這種排序算法是穩定的;否則稱爲不穩定的。

堆排序、快速排序、希爾排序、直接選擇排序不是穩定的排序算法,而基數排序、冒泡排序、直接插入排序、折半插入排序、歸併排序是穩定的排序算法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章