內部排序八大算法原理及JAVA實現

一、概述

  排序有內部排序和外部排序,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納全部的排序記錄,在排序過程中需要訪問外存。具體分類如圖1-1(源自網上)
  
  

圖1-1

  
 某些必要的定義:

  • 時間複雜度:
      時間複雜度是一個函數,它定量描述了該算法的運行時間。這是一個關於代表算法輸入值的字符串的長度的函數。時間複雜度常用大O符號表述,不包括這個函數的低階項和首項係數。
  • 空間複雜度:
      一個程序的空間複雜度是指運行完一個程序所需內存的大小。一個程序執行時除了需要存儲空間和存儲本身所使用的指令、常數、變量和輸入數據外,還需要一些對數據進行操作的工作單元和存儲一些爲現實計算所需信息的輔助空間。程序執行時所需存儲空間包括以下兩部分。  
      (1)固定部分。這部分空間的大小與輸入/輸出的數據的個數多少、數值無關。主要包括指令空間(即代碼空間)、數據空間(常量、簡單變量)等所佔的空間。這部分屬於靜態空間。
      (2)可變空間,這部分空間的主要包括動態分配的空間,以及遞歸棧所需的空間等。這部分的空間大小與算法有關。
  • 穩定性:
      假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,則稱這種排序算法是穩定的;否則稱爲不穩定的。
    對於不穩定的排序算法,只要舉出一個實例,即可說明它的不穩定性;而對於穩定的排序算法,必須對算法進行分析從而得到穩定的特性。
    需要注意的是,排序算法是否爲穩定的是由具體算法決定的,不穩定的算法在某種條件下可以變爲穩定的算法,而穩定的算法在某種條件下也可以變爲不穩定的算法。

二、具體算法

1、插入排序

1.基本思想:
  插入排序由N-1趟排序組成。對於p=1到N-1趟,保證位置從0到p的元素處於已排序狀態。即:第p次排序時,將位置p上的元素向前移動到合適的位置。
2.具體步驟及圖解:

  1. 從第一個元素開始,該元素可以認爲已經被排序
  2. 取出下一個元素,在已經排序的元素序列中從後向前掃描
  3. 如果該元素(已排序)大於新元素,將該元素移到下一位置
  4. 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置
  5. 將新元素插入到該位置後
  6. 重複步驟2~5

下圖詳細描述了插入排序的排序過程(源自網上)。

圖2-1

3.代碼實現

    /**
     * 插入排序(由小到大)
     *
     * @param a 排序數組
     */
    public static <T extends Comparable<? super T>> void insertionSort(T[] a) {
        int i;
        for (int p = 1; p < a.length; p++) {
            T temp = a[p];
            for (i = p; i > 0 && temp.compareTo(a[i - 1]) < 0; i--) {
                a[i] = a[i - 1];
            }
            a[i] = temp;
        }
    }

4.分析

  • 穩定性:相同元素不互換次序,是穩定的。
  • 時間複雜度:
    • 最好情況:數組本就有序,則每趟排序只需進行一次比較,共需N-1趟,故爲O(n)
    • 最差情況:數組倒序,第p趟插入需要比較p-1次,故1+2+……N-1=N*(N-1)/2,故爲O(N^2)
    • 平均情況爲O(N^2)
  • 空間複雜度:所需空間爲常數,O(1)
  • 結論:數據量較小,較有序的情況下性能較好

2、冒泡排序

1.基本思想:
  重複地走訪過要排序的元素,一次比較相鄰兩個元素,如果他們的順序錯誤就把他們調換過來,直到沒有元素再需要交換,排序完成。
2.具體步驟及圖解:

  1. 比較相鄰的元素,如果前一個比後一個大,就把它們兩個調換位置。
  2. 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
  3. 針對所有的元素重複以上的步驟,除了最後一個。
  4. 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。

下圖詳細描述了冒泡排序的排序過程(源自網上)。
這裏寫圖片描述

圖2-2

3.代碼實現

     /**
     * @param a
     * @param <T>
     */
    public static <T extends Comparable<? super T>> void bubbleSort(T[] a) {
        for (int i = 0; i < a.length; i++) {
            for (int j = 0; j < a.length - i - 1; j++) {
                if (a[j].compareTo(a[j + 1]) > 0) {
                    T temp = a[j + 1];
                    a[j + 1] = a[j];
                    a[j] = temp;
                }
            }
        }
    }

4.分析

  • 穩定性:相同元素不互換次序,是穩定的。
  • 時間複雜度:此種排序方式,數據順序對算法影響不大,爲O(N^2)
  • 空間複雜度:所需空間爲常數,O(1)

3、選擇排序

1.基本思想:
  在要排序的一組數中,選出最小的一個數與第1個位置的數交換;然後在剩下的數當中再找最小的與第2個位置的數交換,依次類推,直到第n-1個元素(倒數第二個數)和第n個元素(最後一個數)比較爲止。
2.具體步驟及圖解:

  1. 第一趟,從n 個記錄中找出關鍵碼最小的記錄與第一個記錄交換;
  2. 第二趟,從第二個記錄開始的n-1 個記錄中再選出關鍵碼最小的記錄與第二個記錄交換;
  3. 以此類推…..
  4. 第i 趟,則從第i 個記錄開始的n-i+1 個記錄中選出關鍵碼最小的記錄與第i 個記錄交換,直到整個序列按關鍵碼有序。

下圖詳細描述了選擇排序的排序過程(源自網上)。

圖2-3

3.代碼實現

     /**
     * @param a
     * @param <T>
     */
    public static <T extends Comparable<? super T>> void selectSort(T[] a) {
        int minIndex;
        for (int i = 0; i < a.length - 1; i++) {
            minIndex = i;
            for (int j = i + 1; j < a.length; j++) {
                if (a[i].compareTo(a[j]) > 0) {
                    minIndex = j;
                }
            }
            if (minIndex != i) {
                T temp = a[minIndex];
                a[minIndex] = a[i];
                a[i] = temp;
            }
        }
    }

4.分析

  • 穩定性:選擇排序是不穩定的排序算法,不穩定發生在最小元素與A[i]交換的時刻。比如序列:{ 5, 8, 5, 2, 9 },一次選擇的最小元素是2,然後把2和第一個5進行交換,從而改變了兩個元素5的相對次序。
  • 時間複雜度:無論最好情況還是最差情況,都需要進行1+2+……N-1=N*(N-1)/2次比較找出最值,故爲O(N^2)
  • 空間複雜度:所需空間爲常數,O(1)

4、希爾排序

1.基本思想:
  先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行依次直接插入排序。
2.具體步驟及圖解:

  1. 選擇一個增量序列d[k],其中ti>tj,tk=1;
  2. 按增量序列個數k,對序列進行k 趟排序;
  3. 每趟排序,根據對應的增量ti,將待排序列分割成若干長度爲m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲1 時,整個序列作爲一個表來處理,表長度即爲整個序列的長度。

增量的選擇:

 a. 希爾增量 d[k]的選擇是 d[t]=N/2,d[k]=d[k+1]/2 此處即選擇此種方式 最壞運行時間O(N^2)
 b. Hibbard增量 {1, 3, ..., 2^k-1}  最壞運行時間O(N^3/2)
 c. Sedgewick增量:{1, 5, 19, 41, 109...}該序列中的項或者是9*4^i - 9*2^i + 1或者是4^i - 3*2^i + 1

下圖詳細描述了希爾排序的排序過程(源自網上)。
這裏寫圖片描述

圖2-4

3.代碼實現

     /**
     * @param a
     * @param <T>
     */
      public static <T extends Comparable<? super T>> void shellSort(T[] a) {
        int j;
        for (int gap = a.length / 2; gap > 0; gap /= 2) {//b[k]
            for (int i = gap; i < a.length; i++) {
                T temp = a[i];
                for (j = i; j >= gap && temp.compareTo(a[j - gap]) < 0; j -= gap) {
                    a[j] = a[j - gap];
                }
                a[j] = temp;
            }
        }
    }

4.分析

  • 穩定性:希爾排序是不穩定的排序算法,因爲相同元素可能被分割至不同的組,導致次序改變。
  • 時間複雜度:與選擇的增量有關
    • 希爾增量:此種增量方式,當N=2^k時,會導致前後增量爲倍數關係,有一直到步長爲1之前都未涉及到的元素,極限情況下甚至還有可能之前所有的排序都在浪費時間,如{1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15, 8, 16}數組,步長爲8,4,2時未進行任何處理。故最壞時間複雜度與插入排序相同爲O(N^2)
    • Hibbard增量 最壞運行時間O(N^3/2)
    • Sedgewick增量:爲O(N^4/3)
  • 空間複雜度:所需空間爲常數,O(1)

5、堆排序

1.基本思想:
  堆排序是指利用堆這種數據結構所設計的一種排序算法。堆是一個近似完全二叉樹的結構(通常堆是通過一維數組來實現的),並同時滿足堆的性質:即子結點的鍵值總是小於(或者大於)它的父節點。利用這種堆性質,可以輕易選出最值。
2.具體步驟及圖解:

  1. 將數組構建爲建一個堆(最大堆或者最小堆根據排序需求選擇)
  2. 把堆頂元素(最大值)和堆尾元素互換
  3. 把堆的尺寸縮小1,並調整使得剩餘元素成爲一個新的堆。
  4. 重複步驟2,直到堆的尺寸爲1

下圖詳細描述了堆排序的排序過程(源自網上)。
這裏寫圖片描述

圖2-5

3.代碼實現

     /**
     * 堆排序(第0位有值)
     *
     * @param a
     * @param <T>
     */
    public static <T extends Comparable<? super T>> void heapSort(T[] a) {
        //將數組轉化爲堆
        for (int i = a.length / 2 - 1; i >= 0; i--) {
            percDown(a, i, a.length);
        }
        //排序
        for (int i = a.length - 1; i >= 0; i--) {
            swap(a, 0, i);
            percDown(a, 0, i);
        }
    }

    /**
     * 最大堆的下濾操作
     *
     * @param a
     * @param hole
     * @param n    二叉堆中現有元素
     * @param <T>
     */
    private static <T extends Comparable<? super T>> void percDown(T[] a, int hole, int n) {
        T temp = a[hole];
        int child;
        for (; (2 * hole + 1) < n; hole = child) {
            child = hole * 2 + 1;//左兒子
            if (child + 1 < n && a[child + 1].compareTo(a[child]) > 0) {
                child += 1;
            }
            if (temp.compareTo(a[child]) < 0) {
                a[hole] = a[child];
            } else {
                break;
            }
        }
        a[hole] = temp;
    }
    /**
     * 交換數組中兩下標的元素
     *
     * @param a      待交換數組
     * @param index1 數組下標1
     * @param index2 數組下標2
     * @param <T>
     */
    private static <T extends Comparable<? super T>> void swap(T[] a, int index1, int index2) {
        T temp = a[index1];
        a[index1] = a[index2];
        a[index2] = temp;
    }

4.分析

  • 穩定性:堆排序是不穩定的排序算法,不穩定發生在堆頂元素與a[i]交換的時刻。
  • 時間複雜度:由於每次重新恢復堆的時間複雜度爲O(logN),共N - 1次重新恢復堆操作,再加上前面建立堆時N / 2次向下調整,每次調整時間複雜度也爲O(logN)。二次操作時間相加還是O(N^2)
  • 空間複雜度:所需空間爲常數,O(1)

6、歸併排序

1.基本思想:
  歸併的含義是將兩個或兩個以上的有序表合併成一個新的有序表。
  假設我們有一個沒有排好序的序列,那麼首先我們使用分割的辦法將這個序列分割成一個個已經排好序的子序列(每個序列一個元素)。然後再利用歸併的方法將一個個的子序列合併成排序好的序列。
2.具體步驟及圖解:

  1. 把 n 個記錄看成 n 個長度爲1的有序子表;
  2. 進行兩兩歸併使記錄關鍵字有序,得到 n/2 個長度爲 2 的有序子表;
  3. 重複第2步直到所有記錄歸併成一個長度爲 n 的有序表爲止。

下圖詳細描述了堆排序的排序過程(源自網上)。
這裏寫圖片描述

圖2-6

3.代碼實現

     /**
     * 歸併排序
     *
     * @param a
     * @param <T>
     */
    public static <T extends Comparable<? super T>> void mergeSort(T[] a) {
        T[] tempArray = (T[]) new Comparable[a.length];
        mergeSort(a, tempArray, 0, a.length - 1);
    }

    /**
     * 分割
     *
     * @param a
     * @param tempArray
     * @param left
     * @param right
     * @param <T>
     */
    private static <T extends Comparable<? super T>> void mergeSort(T[] a, T[] tempArray, int left, int right) {
        if (left < right) {
            int center = (left + right) / 2;
            mergeSort(a, tempArray, left, center);
            mergeSort(a, tempArray, center + 1, right);
            merge(a, tempArray, left, center + 1, right);
        }
    }

    /**
     * 合併
     *
     * @param a
     * @param tempArray
     * @param leftPos
     * @param rightPos
     * @param rightEnd
     * @param <T>
     */
    private static <T extends Comparable<? super T>> void merge(T[] a, T[] tempArray, int leftPos, int rightPos, int rightEnd) {
        int leftEnd = rightPos - 1;//左數組的界
        int tempPos = leftPos;
        int elementNums = rightEnd - leftPos + 1;//數組的總個數
        while (leftPos <= leftEnd && rightPos <= rightEnd) {
            if (a[leftPos].compareTo(a[rightPos]) > 0) {
                tempArray[tempPos++] = a[rightPos++];
            } else {
                tempArray[tempPos++] = a[leftPos++];
            }
        }
        while (leftPos <= leftEnd) {
            tempArray[tempPos++] = a[leftPos++];
        }
        while (rightPos <= rightEnd) {
            tempArray[tempPos++] = a[rightPos++];
        }
        for (int i = 0; i < elementNums; i++, rightEnd--) {
            a[rightEnd] = tempArray[rightEnd];
        }
    }

4.分析

  • 穩定性:歸併排序是穩定的排序算法。
  • 時間複雜度:
      合併所耗時間是線性的O(N)
      N=1時,花費常熟時間,故:T(1)=1
      T(N)=2T(N/2)+N
      將上式連續分解得:
      T(N)=4T(N/4)+2N==2kT(N/2k)+kN
      將數組分解到每個子表爲1所需分解步驟k滿足:2k=N
      故有:T(N)=NT(1)+NlogN=NlogN+N
    故時間複雜度爲O(N*logN)
  • 空間複雜度:
      申請了一個大小爲N的臨時數組O(N)
      遞歸logN次,每次遞歸使用需要棧空間記錄遞歸O(logN)
      故爲O(N+logN)

7、快速排序

1.基本思想:
  採用分治思想,從數組中挑出一個元素作爲基準(pivot),使用此基準將數組一分爲二,然後對兩個數組分別繼續採取上述做法,直至數組排序完成。
2.具體步驟及圖解:

  1. 從序列中挑出一個元素,作爲”基準”(pivot)。
  2. 把所有比基準值小的元素放在基準前面,所有比基準值大的元素放在基準的後面(相同的數可以到任一邊),這個稱爲分區(partition)操作。
  3. 對每個分區遞歸地進行步驟1~3,遞歸的結束條件是序列的大小是0或1,這時整體已經被排好序

下圖詳細描述了堆排序的排序過程(源自網上,圖中選取pivot做法不可取!!!)。
這裏寫圖片描述

圖2-6

基準的選取:

  • 選取第一個元素或最後一個元素:這種做法是錯誤的,因爲若輸入數組是預排序或反序的,此種做法可能導致某個分區爲空。
  • 選取隨機數:對於隨機數組來說是安全的,但產生隨機數開銷較大,不建議。
  • 三數中值法:採用頭,尾以及中間三個數的中值(中位數)。

3.代碼實現
  採取三分法之後,最小值在最前,最大值在最後。在從後向前掃描(j)的過程中,第一位爲了防止在掃描過程中,

      /**
     * 快速排序
     *
     * @param a   排序數組
     * @param <T>
     */
    public static <T extends Comparable<? super T>> void quickSort(T[] a) {
        quickSort(a, 0, a.length - 1);
    }

    private static <T extends Comparable<? super T>> void quickSort(T[] a, int left, int right) {
        if (right - left >= 3) {
            T pivot = medianFromThree(a, left, right);//中值,標誌
            int i = left, j = right - 1;//開始掃描
            while (true) {
                while (a[++i].compareTo(pivot) < 0) {
                }
                while (a[--j].compareTo(pivot) > 0) {
                }
                if (i < j) {
                    swap(a, i, j);
                } else {
                    break;
                }
            }
            swap(a, i, right - 1);//將pivot放置合適的位置
            quickSort(a, left, i - 1);//較小元素排序
            quickSort(a, i + 1, right);//較大元素排序
        } else {//對於小數組,直接使用插入排序,效率更佳
            insertionSort(a);
        }
    }

    /**
     * 取三個數的中值數  並將三個數放至合適的位置,將中位數移至倒數第二位是爲了防止數組越界
     *
     * @param a     數組
     * @param left  左下標
     * @param right 右下標
     * @param <T>   返回的中值數
     * @return
     */
    private static <T extends Comparable<? super T>> T medianFromThree(T[] a, int left, int right) {
        int center = (left + right) / 2;
        if (a[left].compareTo(a[right]) > 0) {
            swap(a, left, right);
        }
        if (a[left].compareTo(a[center]) > 0) {
            swap(a, left, center);
        }
        if (a[center].compareTo(a[right]) > 0) {
            swap(a, center, right);
        }
        swap(a, center, right - 1);//將中位數移動至倒數第二位
        return a[right - 1];
    }

    /**
     * 交換數組中兩下標的元素
     *
     * @param a      待交換數組
     * @param index1 數組下標1
     * @param index2 數組下標2
     * @param <T>
     */
    private static <T extends Comparable<? super T>> void swap(T[] a, int index1, int index2) {
        T temp = a[index1];
        a[index1] = a[index2];
        a[index2] = temp;
    }

4.分析

  • 穩定性:快速排序是不穩定的排序算法。
  • 時間複雜度:O(N*logN)
  • 空間複雜度:O(N*logN)

8、基數排序

1.基本思想:
  桶排序:是將陣列分到有限數量的桶子裏。每個桶子再個別排序。簡單來說,就是把數據分組,放在一個個的桶中,然後對每個桶裏面的在進行排序。
  基數排序:就是進行多次桶排序。
2.具體步驟及圖解:

  1. 把 n 個記錄看成 n 個長度爲1的有序子表;
  2. 進行兩兩歸併使記錄關鍵字有序,得到 n/2 個長度爲 2 的有序子表;
  3. 重複第2步直到所有記錄歸併成一個長度爲 n 的有序表爲止。

下圖詳細描述了桶排序的排序過程(源自網上)。
這裏寫圖片描述

圖2-6

3.代碼實現

      /**
     * 基數排序(字符串)
     *
     * @param a      待排序的整型數組
     * @param maxLen 最長數字的長度
     */
    public static void radixSort(String[] a, int maxLen) {
        final int BUCKETS = 257;//所有字符的個數,第0位存放該位無字符的字符串
        ArrayList<String>[] buckets = new ArrayList[BUCKETS];
        for (int i = 0; i < BUCKETS; i++) {
            buckets[i] = new ArrayList();
        }
        for (int i = maxLen - 1; i >= 0; i--) {//maxLen次桶排序
            for (String s : a) {
                if (s.length() > i) {
                    buckets[s.charAt(i)+1].add(s);
                }else {
                    buckets[0].add(s);
                }
            }
            int idx=0;
            for(ArrayList<String> bucket:buckets){
                for(String s:bucket){
                    a[idx++]=s;
                }
                bucket.clear();
            }
        }

    }

    /**
     * 基數排序(正整數)
     *
     * @param a      待排序的字符串數組
     * @param maxLen 最大數長度
     */
    public static void radixSort(Integer[] a, int maxLen) {
        ArrayList[] buckets = new ArrayList[10];
        for (int i = 0; i < buckets.length; i++) {
            buckets[i] = new ArrayList();
        }
        for (int i = 0; i < maxLen; i++) {
            for (Integer s : a) {
                buckets[(s / (int) Math.pow(10, i)) % 10].add(s);
            }
            int idx = 0;
            for (ArrayList<Integer> bucket : buckets) {
                for (Integer s : bucket) {
                    a[idx++] = s;
                }
                bucket.clear();
            }
        }
    }

4.分析

  • 穩定性:基數排序是穩定的排序算法。
  • 時間複雜度:O(d(r+n))
  • 空間複雜度:O(rd+n)
      d代表長度,r代表關鍵字的基數,d代表長度,n代表關鍵字。
     

三、所有代碼

import java.util.ArrayList;
import java.util.Map;

/**
 * Created by Administrator on 2017/6/26.
 */
public class Sort {
    /**
     * 插入排序(由小到大)
     * 第p次排序時,將位置p上的元素向前移動到合適的位置
     *
     * @param a
     */
    public static <T extends Comparable<? super T>> void insertionSort(T[] a) {
        int i;
        for (int p = 1; p < a.length; p++) {
            T temp = a[p];
            for (i = p; i > 0 && temp.compareTo(a[i - 1]) < 0; i--) {
                a[i] = a[i - 1];
            }
            a[i] = temp;
        }
    }

    /**
     * 冒泡排序(由小到大)
     * 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
     * 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。
     * 針對所有的元素重複以上的步驟,除了最後一個。
     * 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
     *
     * @param a
     * @param <T>
     */
    public static <T extends Comparable<? super T>> void bubbleSort(T[] a) {
        for (int i = 0; i < a.length; i++) {
            for (int j = 0; j < a.length - i - 1; j++) {
                if (a[j].compareTo(a[j + 1]) > 0) {
                    T temp = a[j + 1];
                    a[j + 1] = a[j];
                    a[j] = temp;
                }
            }
        }
    }

    /**
     * 選擇排序(由小到大)
     * 每一次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,直到全部待排序的數據元素排完
     *
     * @param a
     * @param <T>
     */
    public static <T extends Comparable<? super T>> void selectSort(T[] a) {
        int minIndex;
        for (int i = 0; i < a.length - 1; i++) {
            minIndex = i;
            for (int j = i + 1; j < a.length; j++) {
                if (a[i].compareTo(a[j]) > 0) {
                    minIndex = j;
                }
            }
            if (minIndex != i) {
                T temp = a[minIndex];
                a[minIndex] = a[i];
                a[i] = temp;
            }
        }
    }

    /**
     * 希爾排序(由小到大)
     * 希爾排序屬於插入類排序,是將整個有序序列分割成若干小的子序列分別進行插入排序。
     * <p/>
     * 先取一個正整數d1<n,把所有序號相隔d1的數組元素放一組,組內進行直接插入排序;
     * 然後取d2<d1,重複上述分組和排序操作;
     * 直至di=1,即所有記錄放進一個組中排序爲止。
     * <p/>
     * 希爾增量 d[k]的選擇是 d[t]=N/2,d[k]=d[k+1]/2 此處即選擇此種方式 最壞運行時間O(N^2)
     * Hibbard增量 d[k]的選擇是 d[k]=2^k-1  最壞運行時間O(N^3/2)
     *
     * @param a
     * @param <T>
     */
    public static <T extends Comparable<? super T>> void shellSort(T[] a) {
        int j;
        for (int gap = a.length / 2; gap > 0; gap /= 2) {//b[k]
            for (int i = gap; i < a.length; i++) {
                T temp = a[i];
                for (j = i; j >= gap && temp.compareTo(a[j - gap]) < 0; j -= gap) {
                    a[j] = a[j - gap];
                }
                a[j] = temp;
            }
        }
    }

    /**
     * 堆排序(第0位有值)
     * 初始化操作:將a[1..n]構造爲初始堆
     * 每一趟排序的基本操作:將當前無序區的堆頂記錄R[1]和該區間的最後一個記錄交換,然後將新的無序區調整爲堆(亦稱重建堆)。
     * 重複此操作,直至結束
     *
     * @param a
     * @param <T>
     */
    public static <T extends Comparable<? super T>> void heapSort(T[] a) {
        for (int i = a.length / 2 - 1; i >= 0; i--) {
            percDown(a, i, a.length);
        }
        for (int i = a.length - 1; i >= 0; i--) {
            swap(a, 0, i);
            percDown(a, 0, i);
        }
    }

    /**
     * 最大堆的下濾操作
     *
     * @param a
     * @param hole
     * @param n    二叉堆中現有元素
     * @param <T>
     */
    private static <T extends Comparable<? super T>> void percDown(T[] a, int hole, int n) {
        T temp = a[hole];
        int child;
        for (; (2 * hole + 1) < n; hole = child) {
            child = hole * 2 + 1;//左兒子
            if (child + 1 < n && a[child + 1].compareTo(a[child]) > 0) {
                child += 1;
            }
            if (temp.compareTo(a[child]) < 0) {
                a[hole] = a[child];
            } else {
                break;
            }
        }
        a[hole] = temp;
    }


    /**
     * 歸併排序
     * 歸併的含義是將兩個或兩個以上的有序表合併成一個新的有序表。
     * 假設我們有一個沒有排好序的序列,那麼首先我們使用分割的辦法將這個序列分割成一個個已經排好序的子序列。
     * 然後再利用歸併的方法將一個個的子序列合併成排序好的序列。
     *
     * @param a
     * @param <T>
     */
    public static <T extends Comparable<? super T>> void mergeSort(T[] a) {
        T[] tempArray = (T[]) new Comparable[a.length];
        mergeSort(a, tempArray, 0, a.length - 1);
    }

    /**
     * 分割
     *
     * @param a
     * @param tempArray
     * @param left
     * @param right
     * @param <T>
     */
    private static <T extends Comparable<? super T>> void mergeSort(T[] a, T[] tempArray, int left, int right) {
        if (left < right) {
            int center = (left + right) / 2;
            mergeSort(a, tempArray, left, center);
            mergeSort(a, tempArray, center + 1, right);
            merge(a, tempArray, left, center + 1, right);
        }
    }

    /**
     * 合併
     *
     * @param a
     * @param tempArray
     * @param leftPos
     * @param rightPos
     * @param rightEnd
     * @param <T>
     */
    private static <T extends Comparable<? super T>> void merge(T[] a, T[] tempArray, int leftPos, int rightPos, int rightEnd) {
        int leftEnd = rightPos - 1;//左數組的界
        int tempPos = leftPos;
        int elementNums = rightEnd - leftPos + 1;//數組的總個數
        while (leftPos <= leftEnd && rightPos <= rightEnd) {
            if (a[leftPos].compareTo(a[rightPos]) > 0) {
                tempArray[tempPos++] = a[rightPos++];
            } else {
                tempArray[tempPos++] = a[leftPos++];
            }
        }
        while (leftPos <= leftEnd) {
            tempArray[tempPos++] = a[leftPos++];
        }
        while (rightPos <= rightEnd) {
            tempArray[tempPos++] = a[rightPos++];
        }
        for (int i = 0; i < elementNums; i++, rightEnd--) {
            a[rightEnd] = tempArray[rightEnd];
        }
    }

    /**
     * 快速排序
     * 通過一趟排序將待排序的記錄分隔成獨立的兩個部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小,另一部分記錄的關鍵字均比另一部分記錄的關鍵字大。
     * 接着分別對兩部分分別進行同樣的操作,最終得到有序的結果
     * <p/>
     * 關鍵字選取:三數中值法(取最左,最右和中間三個數的中值)
     *
     * @param a   排序數組
     * @param <T>
     */
    public static <T extends Comparable<? super T>> void quickSort(T[] a) {
        quickSort(a, 0, a.length - 1);
    }

    private static <T extends Comparable<? super T>> void quickSort(T[] a, int left, int right) {
        if (right - left >= 3) {
            T pivot = medianFromThree(a, left, right);//中值,標誌
            int i = left, j = right - 1;//開始掃描
            while (true) {
                while (a[++i].compareTo(pivot) < 0) {
                }
                while (a[--j].compareTo(pivot) > 0) {
                }
                if (i < j) {
                    swap(a, i, j);
                } else {
                    break;
                }
            }
            swap(a, i, right - 1);//將pivot放置合適的位置
            quickSort(a, left, i - 1);//較小元素排序
            quickSort(a, i + 1, right);//較大元素排序
        } else {//對於小數組,直接使用插入排序,效率更佳
            insertionSort(a);
        }
    }

    /**
     * 取三個數的中值數  並將三個數放至合適的位置
     *
     * @param a     數組
     * @param left  左下標
     * @param right 右下標
     * @param <T>   返回的中值數
     * @return
     */
    private static <T extends Comparable<? super T>> T medianFromThree(T[] a, int left, int right) {
        int center = (left + right) / 2;
        if (a[left].compareTo(a[right]) > 0) {
            swap(a, left, right);
        }
        if (a[left].compareTo(a[center]) > 0) {
            swap(a, left, center);
        }
        if (a[center].compareTo(a[right]) > 0) {
            swap(a, center, right);
        }
        swap(a, center, right - 1);//將中位數移動至倒數第二位
        return a[right - 1];
    }

    /**
     * 交換數組中兩下標的元素
     *
     * @param a      待交換數組
     * @param index1 數組下標1
     * @param index2 數組下標2
     * @param <T>
     */
    private static <T extends Comparable<? super T>> void swap(T[] a, int index1, int index2) {
        T temp = a[index1];
        a[index1] = a[index2];
        a[index2] = temp;
    }

    /**
     * 基數排序(字符串)
     *
     * @param a      待排序的整型數組
     * @param maxLen 最長數字的長度
     */
    public static void radixSort(String[] a, int maxLen) {
        final int BUCKETS = 257;//所有字符的個數,第0位存放該位無字符的字符串
        ArrayList<String>[] buckets = new ArrayList[BUCKETS];
        for (int i = 0; i < BUCKETS; i++) {
            buckets[i] = new ArrayList();
        }
        for (int i = maxLen - 1; i >= 0; i--) {//maxLen次桶排序
            for (String s : a) {
                if (s.length() > i) {
                    buckets[s.charAt(i)+1].add(s);
                }else {
                    buckets[0].add(s);
                }
            }
            int idx=0;
            for(ArrayList<String> bucket:buckets){
                for(String s:bucket){
                    a[idx++]=s;
                }
                bucket.clear();
            }
        }

    }

    /**
     * 基數排序(正整數)
     *
     * @param a      待排序的字符串數組
     * @param maxLen 最大數長度
     */
    public static void radixSort(Integer[] a, int maxLen) {
        ArrayList[] buckets = new ArrayList[10];
        for (int i = 0; i < buckets.length; i++) {
            buckets[i] = new ArrayList();
        }
        for (int i = 0; i < maxLen; i++) {
            for (Integer s : a) {
                buckets[(s / (int) Math.pow(10, i)) % 10].add(s);
            }
            int idx = 0;
            for (ArrayList<Integer> bucket : buckets) {
                for (Integer s : bucket) {
                    a[idx++] = s;
                }
                bucket.clear();
            }
        }
    }

}

四、總體分析

1 快速排序(QuickSort)
  快速排序是一個就地排序,分而治之,大規模遞歸的算法。從本質上來說,它是歸併排序的就地版本。快速排序比大部分排序算法都要快。儘管我們可以在某些特殊的情況下寫出比快速排序快的算法,但是就通常情況而言,沒有比它更快的了。快速排序是遞歸的,對於內存非常有限的機器來說,它不是一個好的選擇。

2 歸併排序(MergeSort)
  歸併排序先分解要排序的序列,從1分成2,2分成4,依次分解,當分解到只有1個一組的時候,就可以排序這些分組,然後依次合併回原來的序列中,這樣就可以排序所有數據。合併排序比堆排序稍微快一點,但是需要比堆排序多一倍的內存空間,因爲它需要一個額外的數組。

3 堆排序(HeapSort)
  堆排序適合於數據量非常大的場合(百萬數據)。
  堆排序不需要大量的遞歸或者多維的暫存數組。這對於數據量非常巨大的序列是合適的。比如超過數百萬條記錄,因爲快速排序,歸併排序都使用遞歸來設計算法,在數據量非常大的時候,可能會發生堆棧溢出錯誤。堆排序會將所有的數據建成一個堆,最大的數據在堆頂,然後將堆頂數據和序列的最後一個數據交換。接下來再次重建堆,交換數據,依次下去,就可以排序所有的數據。

4 Shell排序(ShellSort)
  Shell排序通過將數據分成不同的組,先對每一組進行排序,然後再對所有的元素進行一次插入排序,以減少數據交換和移動的次數。平均效率是O(nlogn)。其中分組的合理性會對算法產生重要的影響。現在多用D.E.Knuth的分組方法。
  Shell排序比冒泡排序快5倍,比插入排序大致快2倍。Shell排序比起QuickSort,MergeSort,HeapSort慢很多。但是它相對比較簡單,它適合於數據量在5000以下並且速度並不是特別重要的場合。它對於數據量較小的數列重複排序是非常好的。

5 插入排序(InsertSort)
  插入排序通過把序列中的值插入一個已經排序好的序列中,直到該序列的結束。插入排序是對冒泡排序的改進。它比冒泡排序快2倍。一般不用在數據大於1000的場合下使用插入排序,或者重複排序超過200數據項的序列。

6 冒泡排序(BubbleSort)
  冒泡排序是最慢的排序算法。在實際運用中它是效率最低的算法。它通過一趟又一趟地比較數組中的每一個元素,使較大的數據下沉,較小的數據上升。它是O(n^2)的算法。

7 選擇排序(SelectSort)
  這兩種排序方法都是交換方法的排序算法,效率都是 O(n2)。在實際應用中處於和冒泡排序基本相同的地位。它們只是排序算法發展的初級階段,在實際中使用較少。

8 基數排序(RadixSort)
  基數排序和通常的排序算法並不走同樣的路線。它是一種比較新穎的算法,但是它只能用於整數的排序,如果我們要把同樣的辦法運用到浮點數上,我們必須瞭解浮點數的存儲格式,並通過特殊的方式將浮點數映射到整數上,然後再映射回去,這是非常麻煩的事情,因此,它的使用同樣也不多。而且,最重要的是,這樣算法也需要較多的存儲空間。

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