8 大內部排序演算法相關及其java實現

yoylee_web發表於2018-08-07

ps:本博文主要內容來自:www.cricode.com/3212.html,博主新增了演算法的適用場景、分類、java實現相關模組,並對內容進行了一些修改。如有不足之處,請不吝指教。

首先,排序演算法可以分為內部排序和外部排序,內部排序是資料記錄在記憶體中進行排序,而外部排序是因排序的資料很大,一次不能容納全部的排序記錄,在排序過程中需要訪問外存。

    常見的內部排序演算法有:插入排序、希爾排序、選擇排序、氣泡排序、歸併排序、快速排序、堆排序、基數排序等。

    外部排序的演算法有:常用多路歸併排序等,在此不做介紹


本文將依次介紹上述八大內部排序演算法。

演算法一:插入排序

插入排序是一種最簡單直觀的排序演算法,它的工作原理是通過構建有序序列,對於未排序資料,在已排序序列中從後向前掃描,找到相應位置並插入。

演算法步驟:

1)將第一待排序序列第一個元素看做一個有序序列,把第二個元素到最後一個元素當成是未排序序列。

2)從頭到尾依次掃描未排序序列,將掃描到的每個元素插入有序序列的適當位置。(如果待插入的元素與有序序列中的某個元素相等,則將待插入元素插入到相等元素的後面。)

適用場景:

    適用於小資料量並且已經基本有序的資料(此處基本有序代表正序,即想要遞增的結果,則陣列基本遞增s有序),插入排序可以明顯減少資料交換和資料移動次數,進而提升排序效率。

演算法實現:

 

package arithmetic;

/**

* 插入排序java實現

*/

public class InsertSort {
    static void insertSort(int [] datas) throws Exception {
        //異常處理
        if (datas.length == 0){
            throw new Exception("接收到空陣列!!");
        }   
        int len = 1;
        //遍歷舊陣列中的資料
        for (int i = 1; i < datas.length; i++) {
            int num = datas[i];
            //便利新陣列中的資料,進行插入
            for (int j = 0; j < len; j++) {
                //如果滿足條件進行插入
                if (num <= datas[j]){
                    for (int k = len; k > j; k--) {
                    datas[k] = datas[k-1];
                    }
                    datas[j] = num;
                    break;
                }else if (j == len-1){
                    datas[j+1] = num;
                }
            }
            len++;
        }
    }


    public static void main(String[] args) throws Exception {
        int [] datas = {4,2,87,4,2,7,9,6,3,7};
        System.out.println(Arrays.toString(datas));
        //異常處理也可在此處進行處理,接受上層傳過來的異常並進行處理
        //使用希爾排序進行排序
        System.out.println();
        insertSort(datas);
       System.out.println(Arrays.toString(datas));
    }

}

演算法二:希爾排序

希爾排序,也稱遞減增量排序演算法,是插入排序的一種更高效的改進版本。但希爾排序是非穩定排序演算法。

希爾排序是基於插入排序的以下兩點性質而提出改進方法的:

  • 插入排序在對幾乎已經排好序的資料操作時, 效率高, 即可以達到線性排序的效率

  • 但插入排序一般來說是低效的, 因為插入排序每次只能將資料移動一位

希爾排序的基本思想是:先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行依次直接插入排序。點選這裡瞭解常用的加密演算法。

演算法步驟:

1)選擇一個增量序列t1,t2,…,tk,其中ti>tj,tk=1;

2)按增量序列個數k,對序列進行k 趟排序;

3)每趟排序,根據對應的增量ti,將待排序列分割成若干長度為m 的子序列,分別對各子表進行直接插入排序。僅增量因子為1 時,整個序列作為一個表來處理,表長度即為整個序列的長度。

適用場景:

    比較在希爾排序中是最主要的操作,而不是交換。”用已知最好的(Sedgewick提出的)步長序列的希爾排序比直接插入排序要快,甚至在小陣列中比快速排序和堆排序還快,但是在涉及大量資料時希爾排序還是比快速排序慢。

    因為增量初始值不容易選擇,所以該演算法不常用。

演算法實現:

package arithmetic;

/**
* 希爾排序java實現
*/
public class ShellSort {
    static void shellSort(int[] datas) throws Exception{
        //異常處理
        if (datas.length==0){
            throw new Exception("接收到空陣列,請傳入非空陣列!");
        }
        int len = datas.length;
        int d = len;
        //希爾排序的間隔    
        while(d > 1){
            //取上一次的二分之一
            d = (d+1)>>1;
            //迴圈到len-d即可,因為後面沒有可匹配的
            for (int i = 0; i < len-d; i++) {
                if (datas[i]>datas[i+d]){
                int temp = datas[i+d];
                datas[i+d] = datas[i];
                datas[i] = temp;
                }
           }
        }
        System.out.println();
        System.out.println(Arrays.toString(datas));
    }

    public static void main(String[] args) throws Exception {
        int [] datas = {4,2,87,4,2,7,9,6,3,7};
        System.out.println(Arrays.toString(datas));
        //異常處理也可在此處進行處理,接受上層傳過來的異常並進行處理        
        //使用希爾排序進行排序
        shellSort(datas);
    }
}

演算法三:選擇排序

選擇排序(Selection sort)也是一種簡單直觀的排序演算法。

演算法步驟:

1)首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置

2)再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。

3)重複第二步,直到所有元素均排序完畢。

適用場景:

    不管資料是否基本有序,在每次取最小值的時候都會進行全部比較。所以更加適合資料量小,無法確定資料是否有序的時候。

演算法實現:

package arithmetic;

/**
* 選擇排序java實現
*/
public class ChooseSort {
    static void chooseSort(int[] datas) throws Exception {
        //異常處理
        if (datas.length <= 0){
            throw new Exception("接收到空陣列!!");
        }
        int min_index;
        //迴圈陣列中所有資料
        for (int i = 0; i < datas.length; i++) {
            min_index = i;
            //獲得最小值下標
            for (int j = i+1; j < datas.length; j++) {
                if (datas[j] < datas[min_index])
                    min_index = j;
            }
            //將最小值與已排序好部分的下一個元素交換位置 (實現在一個陣列中完成排序,不需要重建陣列)
            int temp = datas[i];
            datas[i] = datas[min_index];
            datas[min_index] = temp;
        }
    }

    public static void main(String[] args) throws Exception {
        int[] datas = {4,2,87,4,2,7,9,6,3,7};
        System.out.println(Arrays.toString(datas));
        System.out.println();
        //異常處理也可在此處進行處理,接受上層傳過來的異常並進行處理
        //使用選擇排序進行排序
        chooseSort(datas);
        System.out.println(Arrays.toString(datas));
    }
}

演算法四:氣泡排序

氣泡排序(Bubble Sort)也是一種簡單直觀的排序演算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個演算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端。點選這裡瞭解常用的加密演算法。

演算法步驟

1)比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。

2)對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。

3)針對所有的元素重複以上的步驟,除了最後一個。

4)持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。

適用場景:

    適用於資料初始狀態基本有序,此處基本有序代表正序,即想要遞增的結果,則陣列基本遞增s有序。

演算法實現:

package arithmetic;

/**
* 氣泡排序java實現
*/
public class BubbleSort {

    static void bubbleSort(int[] datas) throws Exception {
        //異常處理
        if (datas.length <= 0){
            throw new Exception("接收到空陣列!!");
        }
        int k = datas.length; //用來提高氣泡排序效率,指向每輪排序的最後一個交換的位置
        int m;
        //迴圈比較直到滿足break;條件
        while(k > 0) {
            m = k;
            for (int i = 0; i < m - 1; i++) {
                if (datas[i] > datas[i + 1]) {
                    int temp = datas[i];
                    datas[i] = datas[i + 1];
                    datas[i + 1] = temp;
                    k = i+1;
                }
            }
            //如果此輪沒有交換的資料,說明順序一致,直接退出。如果有,則m和一定不一致,則繼續迴圈
            if (m == k)
               break;
        }
    }

    public static void main(String[] args) throws Exception {
        int[] datas = {4,2,87,4,2,7,9,6,3,7};
        //列印未排序前陣列
        System.out.println(Arrays.toString(datas));
        System.out.println();
        //異常處理也可在此處進行處理,接受上層傳過來的異常並進行處理
        //使用選擇排序進行排序
       int[] new_datas = bubbleSort(datas);
       System.out.println(Arrays.toString(datas));
    }
}

演算法五:歸併排序

歸併排序(Merge sort)是建立在歸併操作上的一種有效的排序演算法。該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。

演算法步驟:

1. 申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合併後的序列

2. 設定兩個指標,最初位置分別為兩個已經排序序列的起始位置

3. 比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置

4. 重複步驟3直到某一指標達到序列尾

5. 將另一序列剩下的所有元素直接複製到合併序列尾

如:(此處來自百度百科)

設有數列{6,202,100,301,38,8,1}

初始狀態:6,202,100,301,38,8,1

第一次歸併後:{6,202},{100,301},{8,38},{1},比較次數:3;

第二次歸併後:{6,100,202,301},{1,8,38},比較次數:4;

第三次歸併後:{1,6,8,38,100,202,301},比較次數:4;

總的比較次數為:3+4+4=11;

適用場景:

 其時間複雜度為: O(nlog2(n)) ,所以適合資料量較大,有需要排序穩定的需求的時候。

演算法實現:

 

package arithmetic;

import java.util.Arrays;

/**
* 二路歸併排序演算法java實現
*/
public class MegerSort {
    private static void sort(int[] datas, int start, int end) throws Exception {
        //異常處理
        if (datas.length == 0){
            throw new Exception("接收到空陣列!!");
        }
        if (start >= end)
            return;
        //取中間位置為分割點
        int mid = (start + end) >> 1;
        /**
        * 遞迴實現
        * 1.遞迴過程直到start >= end
        * 2.遞迴到底時,開始進行二路歸併mergerSort
        */
        sort(datas, start, mid);
        sort(datas, mid + 1, end);
        mergerSort(datas, start, mid, end);
    }

    // 將兩個有序序列歸併為一個有序序列(二路歸併)
    private static void mergerSort(int[] datas, int start, int mid, int end) {
        int[] arr = new int[end + 1]; // 定義一個臨時陣列,用來儲存排序後的結果
        int low = start; // 臨時陣列的索引
        int left = start;
        int center = mid + 1;
        // 取出最小值放入臨時陣列中
        while (start <= mid && center <= end) {
            arr[low++] = datas[start] > datas[center] ? datas[center++] : datas[start++];
        }

        // 若還有段序列不為空,則將其加入臨時陣列末尾
        while (start <= mid) {
            arr[low++] = datas[start++];
        }
        while (center <= end) {
            arr[low++] = datas[center++];
        }

        // 將臨時陣列中的值copy到原陣列中
        for (int i = left; i <= end; i++) {
            datas[i] = arr[i];
        }
    }

    public static void main(String[] args) throws Exception {
        int[] datas = {4,2,87,4,2,7,9,6,3,7};
        System.out.println(Arrays.toString(datas));
        System.out.println();
        //異常處理也可在此處進行處理,接受上層傳過來的異常並進行處理
        //使用歸併排序進行排序
        sort(datas,0,datas.length-1);
        System.out.println(Arrays.toString(datas));
    }
}

演算法六:快速排序

快速排序是由東尼·霍爾所發展的一種排序演算法。在平均狀況下,排序 n 個專案要Ο(n log n)次比較。在最壞狀況下則需要Ο(n2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他Ο(n log n) 演算法更快,因為它的內部迴圈(inner loop)可以在大部分的架構上很有效率地被實現出來。

快速排序使用分治法(Divide and conquer)策略來把一個序列(list)分為兩個子序列(sub-lists)。

演算法步驟

1 從數列中挑出一個元素,稱為 “基準”(pivot),

2 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分割槽退出之後,該基準就處於數列的中間位置。這個稱為分割槽(partition)操作。

3 遞迴地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。

遞迴的最底部情形,是數列的大小是零或一,也就是永遠都已經被排序好了。雖然一直遞迴下去,但是這個演算法總會退出,因為在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。

簡介相關: https://baike.baidu.com/item/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/369842?fr=kg_qa#4

時間複雜度分析https://www.cnblogs.com/pugang/archive/2012/07/02/2573075.html

適用場景

    適合大資料量,不要求排序穩定,待排序的關鍵字是隨機分佈時。是目前基於比較的內部排序中被認為是最好的方法。

演算法實現:

package arithmetic;

public class QuickPaixvTest {

    static int nums[] = {4,2,78,342,123,543,6,311,2};

    public static void main(String[] args) {
        System.out.print("原陣列: ");
        for (int i = 0; i < nums.length; i++) {
            System.out.print(nums[i]+" ");
        }
        fastSort(0,nums.length-1,nums);
        System.out.println();
        System.out.print("排序後陣列: ");
        for (int i = 0; i < nums.length; i++) {
            System.out.print(nums[i]+" ");
        }
    }

    static void fastSort(int low ,int high ,int[] nums){
        //異常
        if (nums.length == 0)
            throw new NegativeArraySizeException("陣列為空!");
        if (low < 0 || high < 0 || high >= nums.length || low >= nums.length)
            throw new ArrayIndexOutOfBoundsException("請輸入正確的陣列下標!");
        int i = low;
        int j = high;
        int key = nums[low];
        //配合其中的兩個while可達到左邊右邊迴圈比較的效果
        while(i<j){
            //從右邊比較,直到右面小於左面或者j == i
            while (j > i && nums[j] >= key){
                j--;
            }
            //用if判斷一下為了避免上述迴圈時因為不滿足第一個條件而退出的
            if (key >= nums[j]){
                 /**交換*/
                int tem = nums[j];
                nums[j] = nums[i];
                nums[i] = tem;
            }
            //從左邊比較
            while (j > i && nums[i] <= key){
                i++;
            }
            if (nums[i] >= key){
                /**交換*/
                int tem = nums[j];
                nums[j] = nums[i];
                nums[i] = tem;
            }
        }
        if (i>low) fastSort(low,i-1,nums);
        if (j<high) fastSort(j+1,high,nums);
    }
}

演算法七:堆排序

堆排序(Heapsort)是指利用堆這種資料結構所設計的一種排序演算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。

堆排序的平均時間複雜度為Ο(nlogn) 。

分為:大根堆排序,小根堆排序

演算法步驟:

1)建立一個堆H[0..n-1]

2)把堆首(最大值)和堆尾互換

3)把堆的尺寸縮小1,並呼叫shift_down(0),目的是把新的陣列頂端資料調整到相應位置

4) 重複步驟2,直到堆的尺寸為1

詳解https://blog.csdn.net/CSDN___LYY/article/details/81454613

適用場景:

    適用於記憶體要求嚴格,資料量大,不要求排序穩定的時候。堆排序所需的輔助空間少於快速排序,並且不會出現快速排序可能出現的最壞情況。

演算法實現:(大根堆,遞增順序)

package arithmetic;

import java.util.Arrays;

/**
* 堆排序java實現
*/
public class HeapSort {
    static int k = 0; //全域性k,用於排序一次後獲得一個最大的,需要陣列的size-1
    public static void main(String[] args) {
        int[] datas = {4,2,87,4,2,7,9,6,3,7};
        System.out.println(Arrays.toString(datas));
        //初始化堆
        buildHeap(datas);
        //進行排序
        for (int i = datas.length-1; i > 0 ; i--) {
            //堆頂的最大值與陣列的最後一個元素交換(此處為k存在的原因)
            int temp = datas[i];
            datas[i] = datas[0];
            datas[0] = temp;
            k++; //使陣列的size-1
            //調整堆
            adjustHeap(datas,0);
        }
        System.out.println(Arrays.toString(datas));
    }

    /**
    * 初始化堆
    * @param datas 需被初始化的陣列
    */
    static void buildHeap(int[] datas){
        // i = (datas.length-1)>>1 :找到最後一個父結點,之後往前迴圈調整堆
        for (int i = (datas.length-1)>>1; i >= 0; i--) {
           adjustHeap(datas,i);
        }
    }
    /**
    * 調整堆
    * @param datas 需被調整的堆
    * @param i 調整元素所在的位置
    */
    static void adjustHeap(int[] datas , int i){
        //迴圈,使可以調整到底部
        while(true){
            int left = (i<<1)+1; //左孩子
            int right = (i<<1)+2; //右孩子

            int largest = i; //標識自身和孩子中,最大值的下標
            if (left < datas.length-k && datas[i] < datas[left]){
                largest = left;
            }

            if (right < datas.length-k && datas[largest] < datas[right]){
                largest = right;
            }

            //如果滿足條件,表示最大的不是節點自身,則交換
            if (largest != i){
                int temp = datas[i];
                datas[i] = datas[largest];
                datas[largest] = temp;
            }else{ //否則,退出迴圈
                // 原因:因為是從最下面開始進行調整的,所以我們可以只要最大節點是節點自身,我們就可以直接退出
                return;
            }
            i = largest; //修改自身節點
        }
    }
}

演算法八:基數排序

    基數排序是一種非比較型整數排序演算法,其原理是將整數按位數切割成不同的數字,然後按每個位數分別比較。由於整數也可以表達字串(比如名字或日期)和特定格式的浮點數,所以基數排序也不是隻能使用於整數。

    陣列{53, 3, 542, 748, 14, 214, 154, 63, 616}排序的過程圖:(來自網路)

    前面說的幾大排序演算法 ,大部分時間複雜度都是O(n2),也有部分排序演算法時間複雜度是O(nlogn)。而桶式排序卻能實現O(n)的時間複雜度。但桶排序的缺點是:

    1)首先是空間複雜度比較高,需要的額外開銷大。排序有兩個陣列的空間開銷,一個存放待排序陣列,一個就是所謂的桶,比如待排序值是從0到m-1,那就需要m個桶,這個桶陣列就要至少m個空間。

    2)其次待排序的元素都要在一定的範圍內等等。


總結

各種排序的穩定性,時間複雜度、空間複雜度、穩定性總結如下圖:

分類:

(1)插入排序法

    直接插入排序,希爾排序

(2)交換排序

    氣泡排序,快速排序

(3)選擇排序

    直接選擇排序,堆排序

(4)歸併排序

     歸併排序

(5)基數排序

關於時間複雜度:

(1)平方階(O(n2))排序各類簡單排序:直接插入、直接選擇和氣泡排序;

(2)線性對數階(O(nlog2n))排序快速排序、堆排序和歸併排序;

(3)O(n1+§))排序,§是介於0和1之間的常數。希爾排序

(4)線性階(O(n))排序基數排序,此外還有桶、箱排序。

關於穩定性:( 穩定性是指如果存在多個具有相同值的記錄,經過排序後,這些記錄的相對次序仍然保持不變,則這種排序演算法稱為穩定的)

穩定的排序演算法:氣泡排序、插入排序、歸併排序和基數排序

不是穩定的排序演算法:選擇排序、快速排序、希爾排序、堆排序

相關文章