1. 程式人生 > >插入排序演算法(java實現詳解版)

插入排序演算法(java實現詳解版)

插入排序分為兩種,直接插入排序二分插入排序,本節我們只介紹直接插入排序。這兩種插入排序實際上都是插入排序,唯一的不同就是插入的方式不一樣。
  • 插入排序就是往數列裡面插入資料元素。一般我們認為插入排序就是往一個已經排好序的待排序的數列中插入一個數,使得插入這個數之後,數列仍然有序。
  • 二分插入排序也是用了分治法的思想去排序的。實際上二分就是使用二分查詢來找到這個插入的位置,剩餘的插入的思想其實和直接插入排序一樣。

所以要完成插入排序,就需要找到這個待插入元素的位置。下面我們一起看看插入排序的具體操作原理。

插入排序的原理

插入排序實際上把待排序的數列分為了兩部分,一部分已排好序,另一部分待排序。我們下面還是以一組實際資料為例進行講解。假設待排序的數列為 63、88、34、99、38、55、9,首先我們將數列儲存為如圖 1 所示。


圖 1 待排序的數列的初始狀態
這時,全部數列都為待排序部分,我們開始一點點地進行插入排序。

首先把 63 拿出來,這是第 1 個元素,不需要排序。這時,已排好序的部分已經有一個元素了,就是 63,而剩餘的元素為待排序的部分。

接著我們把 88 拿出來,與前面的元素相比較,發現比 63 大,符合我們把數列小到大排序的想法,無須交換。這時已排好序的部分又增加了一個新成員,而待排序部分相應地少了一個元素。

之後我們看 34,比前面的元素 88 比較,發現比 88 小,我們把 34 拿出來,讓它在外面等一下,把88向後移動一位,此時陣列的情況如圖 2 所示。


圖 2 待排序的數列的狀態,將 88 向後移動一位
接下來我們繼續向前比較,直到比較到第 1 個元素為止。這時發現 34 仍然比 63 小,繼續把 63 向後移動,這時的狀態如圖 3 所示。


圖 3 待排序的數列的狀態,將 63 向後移動一位
現在發現已經比較到第 1 個元素了,第 1 個位置的 63 元素需要移動,所以第 1 個位置空出來了,那就把拿出來的 34 放到第 1 個位置上,這時數列狀態變為 34、63、88、99、38、55、9。

現在,已排好序的數列部分為 34、63、88,剩餘的後面部分為待排序部分。我們繼續看後面的元素,該處理 99 了,與前 1 個元素比較,發現比 88 大,由於前面的部分已排好序,所以 88 就是前面數列中最大的,99 比 88 大,肯定也比前面的所有元素都大,不用繼續比較了,可以直接把 99 加入前面的已排好序的部分了。

接下來處理 38,與前 1 個元素比較,發現比 99 小,於是把 38 拿出來,將 99 向後移動,這時的待排序的數列的狀態如圖 4 所示。


圖 4 待排序的數列的狀態,將 99 向後移動一位
接著繼續用 38 與 88 比較,發現比 88 小,88 繼續後移一位;繼續與 63 比較,發現比 63 小,63 也後移一位,這時的陣列狀態如圖 5 所示。


圖 5 當前的待排序的數列狀態
此時繼續用 38 與 34 比較,發現比 34 大,不管 34 前面有沒有元素,都不用繼續比較了,可以直接把 38 放在那個空位上。此時的陣列狀態變為 34、38、63、88、99、55、9。

後面就是分別處理 55 和 9 這兩個元素了。通過上面的幾次移動與比較,我們應該可以自己完成對這兩個元素的插入了。最後當待排序的部分已經沒有了時,整個數列就已經完成所有的排序操作了。

接下來我們總結一下直接插入排序的整個執行過程。
  1. 首先需要明確待排序的數列由兩部分組成,一部分是已排好序的部分,另一部分是待排序的部分。
  2. 接著我們每次選擇待排序的部分的第 1 個元素,分別與前面的元素進行比較。當大於前面的元素時,可以直接進入已排好序的部分;當小於前面的元素時,需要把這個元素拿出來,將前面的元素後移一位,繼續與前面的元素相比,直到比較完陣列的第 1 個元素或者出現一個元素小於我們拿出來的這個元素,這時停止比較、移動,直接把這個元素放到當時的空位上。
  3. 一直重複步驟 2,當待排序的部分已經沒有元素可進行插入時,停止操作,當前的數列為已排好序的數列。

插入排序的實現

插入排序的實現程式碼已經可以寫出來了。首先外層肯定有個大迴圈,迴圈這個待排序的部分的數列,內層是分別與前 1 個元素進行比較、移動,直到找到位置進行插入為止。

下面我們看看插入排序的程式碼實現。
public class InsertSort {
    private int[] array;
    public InsertSort(int[] array) {
        this.array = array;
    }
    public void sort() {
        if (array == null) {
            throw new RuntimeException("array is null");
        }
        int length = array.length;
        if (length > 0) {
            for (int i = 1; i < length; i++) {
                int temp = array[i];
                int j = i;
                for (; j > 0 && array[j - 1] > temp; j--) {
                    array[j] = array[j - 1];
                }
                array[j] = temp;
            }
        }
    }
    public void print() {
        for (int i = 0; i < array.length; i++) {
            System.out.println(array[i]);
        }
    }
}
下面是測試程式碼。
public class SortTest {
    public static void main(String[] args) {
        testInsertSort();
    }
    /**
     * 插入排序
     */
    private static void testInsertSort() {
        int[] array = {5, 9, 1, 9, 5, 3, 7, 6, 1};
        InsertSort insertSort = new InsertSort(array);
        insertSort.sort();
        insertSort.print();
    }
}
和自己寫的程式碼對照一下,你是否完美地完成這個演算法了呢?

插入排序的特點及效能

插入排序的操作很簡單,而且我們通過上面的例項及原理可以知道,插入排序在數列近似有序時,效率會比較高,因為這樣會減少比較和移動的次數。

插入排序的時間複雜度是 O(n2),我們會發現這個實現是個雙重巢狀迴圈,外層執行n遍,內層在最壞的情況下執行 n 遍,而且除了比較操作還有移動操作。最好的情況是數列近似有序,這時一部分內層迴圈只需要比較及移動較少的次數即可完成排序。如果數列本身已經排好序,那麼插入排序也可以達到線性時間複雜度及 O(n),所以我們應該明確地認識到,使用插入排序演算法進行排序時,數列越近似有序,效能就越高。

插入排序的空間複雜度是 O(1),是常量級的,由於在採用插入排序時,我們只需要使用一個額外的空間來儲存這個“拿出來”的元素,所以插入排序只需要額外的一個空間去做排序,這是常量級的空間消耗。

插入排序是穩定的,由於是陣列內部自己排序,把後面的部分按前後順序一點點地比較、移動,可以保持相對順序不變,所以插入排序是穩定的排序演算法。

插入排序的適用場景

插入排序的效能並不是很好,和氣泡排序也算是“難兄難弟”了。但插入排序也有一個好處就是所佔用的空間很少,只有一個儲存臨時變數的額外空間就夠了。

插入排序由於其時間複雜度並不是很好,所以很少會被單獨使用。在所有的基本排序演算法中,在一般情況下我們可以直接選擇快速排序,因為這個排序演算法已經夠用了。

由於在數列近似有序時,效能會比較好,而且對於元素較少的情況,時間複雜度就算是 O(n2) 也不會消耗太多的效能,所以插入排序並非一無是處。

前面提到,在快速排序的分割槽規模達到一定的值比如 10 時,我們會改用插入排序演算法去排序那個分割槽的資料。而快速排序的最後的資料往往是近似有序的,所以使用快速排序的效能並不一定會有多好,這時使用插入排序的實際效能往往會更好些。所以很多程式語言在內部對快速排序的實現也是在分割槽的元素數量達到了一定小的規模時,改用插入排序對分割槽的資料元素進行排序操作。