十大排序演算法,看這篇就夠了(附完整程式碼/動圖/優質文章)

帥地發表於2019-02-20

說明

十大排序演算法可以說是每個程式設計師都必須得掌握的了,花了一天的時間把程式碼實現且整理了一下,為了方便大家學習,我把它整理成一篇文章,每種演算法會有簡單的演算法思想描述,為了方便大家理解,我還找來了動圖演示;這還不夠,我還附上了對應的優質文章,看完不懂你來砍我,覺得不錯就給我來個好看

術語鋪墊

有些人可能不知道什麼是穩定排序、原地排序、時間複雜度、空間複雜度,我這裡先簡單解釋一下:

1、穩定排序:如果 a 原本在 b 的前面,且 a == b,排序之後 a 仍然在 b 的前面,則為穩定排序。

2、非穩定排序:如果 a 原本在 b 的前面,且 a == b,排序之後 a 可能不在 b 的前面,則為非穩定排序。

3、原地排序:原地排序就是指在排序過程中不申請多餘的儲存空間,只利用原來儲存待排資料的儲存空間進行比較和交換的資料排序。

4、非原地排序:需要利用額外的陣列來輔助排序。

5、時間複雜度:一個演算法執行所消耗的時間。

6、空間複雜度:執行完一個演算法所需的記憶體大小。

十大排序講解順序

為了方便大家查詢,我這裡弄一個偽目錄,沒有跳轉功能。

  • 選擇排序
  • 插入排序
  • 氣泡排序
    • 非優化版本
    • 優化版本
  • 希爾排序
  • 歸併排序
    • 遞迴式歸併排序
    • 非遞迴式歸併排序
  • 快速排序
  • 堆排序
  • 基數排序
    • 非優化版本
    • 優化版本
  • 桶排序
  • 基數排序

另:

程式碼說明:程式碼我自己寫的,並且都是經過好幾組資料測試通過,應該沒啥問題,如有錯,還請反饋下,謝謝。

圖片說明:圖片和動畫都是在百度搜尋的,如有侵權,還望聯絡我刪除,謝謝

選擇排序

過程簡單描述:
首先,找到陣列中最小的那個元素,其次,將它和陣列的第一個元素交換位置(如果第一個元素就是最小元素那麼它就和自己交換)。其次,在剩下的元素中找到最小的元素,將它與陣列的第二個元素交換位置。如此往復,直到將整個陣列排序。這種方法我們稱之為選擇排序

為方便理解我還準備了動圖:十大排序演算法,看這篇就夠了(附完整程式碼/動圖/優質文章)

如果還是不懂的話我還給你準備了優質的文章講解:選擇排序

程式碼如下:

public class SelectSort {
    public static int[] selectSort(int[] a) {
        int n = a.length;
        for (int i = 0; i < n - 1; i++) {
            int min = i;
            for (int j = i + 1; j < n; j++) {
                if(a[min] > a[j]) min = j;
            }
            //交換
            int temp = a[i];
            a[i] = a[min];
            a[min] = temp;
        }
        return a;
    }
}

性質:1、時間複雜度:O(n2) 2、空間複雜度:O(1) 3、非穩定排序 4、原地排序

插入排序

我們在玩打牌的時候,你是怎麼整理那些牌的呢?一種簡單的方法就是一張一張的來,將每一張牌插入到其他已經有序的牌中的適當位置。當我們給無序陣列做排序的時候,為了要插入元素,我們需要騰出空間,將其餘所有元素在插入之前都向右移動一位,這種演算法我們稱之為插入排序

過程簡單描述:

1、從陣列第2個元素開始抽取元素。

2、把它與左邊第一個元素比較,如果左邊第一個元素比它大,則繼續與左邊第二個元素比較下去,直到遇到不比它大的元素,然後插到這個元素的右邊。

3、繼續選取第3,4,....n個元素,重複步驟 2 ,選擇適當的位置插入。

為方便理解我還準備了動圖:

十大排序演算法,看這篇就夠了(附完整程式碼/動圖/優質文章)

如果還是不懂的話我還給你準備了優質的文章講解:插入排序

程式碼如下:

public class InsertSort {
    public static int[] insertSort(int[] arr) {
        if(arr == null || arr.length < 2)
            return arr;

        int n = arr.length;
        for (int i = 1; i < n; i++) {
            int temp = arr[i];
            int k = i - 1;
            while(k >= 0 && arr[k] > temp)
                k--;
            //騰出位置插進去,要插的位置是 k + 1;
            for(int j = i ; j > k + 1; j--)
                arr[j] = arr[j-1];
            //插進去
            arr[k+1] = temp;
        }
        return arr;
    }
}

性質:1、時間複雜度:O(n2) 2、空間複雜度:O(1) 3、穩定排序 4、原地排序

氣泡排序

1、把第一個元素與第二個元素比較,如果第一個比第二個大,則交換他們的位置。接著繼續比較第二個與第三個元素,如果第二個比第三個大,則交換他們的位置....

我們對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對,這樣一趟比較交換下來之後,排在最右的元素就會是最大的數。

除去最右的元素,我們對剩餘的元素做同樣的工作,如此重複下去,直到排序完成。

為方便理解我還準備了動圖:十大排序演算法,看這篇就夠了(附完整程式碼/動圖/優質文章)

如果還是不懂的話我還給你準備了優質的文章講解:氣泡排序

程式碼如下

public class BubbleSort {
    public static int[] bubbleSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return arr;
        }
        int n = arr.length;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n -i - 1; j++) {
                if (arr[j + 1] < arr[j]) {
                    int t = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = t;
                }
            }
        }
        return arr;
    }
)

性質:1、時間複雜度:O(n2) 2、空間複雜度:O(1) 3、穩定排序 4、原地排序

優化一下氣泡排序的演算法

假如從開始的第一對到結尾的最後一對,相鄰的元素之間都沒有發生交換的操作,這意味著右邊的元素總是大於等於左邊的元素,此時的陣列已經是有序的了,我們無需再對剩餘的元素重複比較下去了。

程式碼如下:

public class BubbleSort {
    public static int[] bubbleSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return arr;
        }
        int n = arr.length;
        for (int i = 0; i < n; i++) {
            boolean flag = true;
            for (int j = 0; j < n -i - 1; j++) {
                if (arr[j + 1] < arr[j]) {
                    flag = false;
                    int t = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = t;
                }
            }
            //一趟下來是否發生位置交換
            if(false)
                break;
        }
        return arr;
    }
}

希爾排序

希爾排序可以說是插入排序的一種變種。無論是插入排序還是氣泡排序,如果陣列的最大值剛好是在第一位,要將它挪到正確的位置就需要 n - 1 次移動。也就是說,原陣列的一個元素如果距離它正確的位置很遠的話,則需要與相鄰元素交換很多次才能到達正確的位置,這樣是相對比較花時間了。

希爾排序就是為了加快速度簡單地改進了插入排序,交換不相鄰的元素以對陣列的區域性進行排序。

希爾排序的思想是採用插入排序的方法,先讓陣列中任意間隔為 h 的元素有序,剛開始 h 的大小可以是 h = n / 2,接著讓 h = n / 4,讓 h 一直縮小,當 h = 1 時,也就是此時陣列中任意間隔為1的元素有序,此時的陣列就是有序的了。

為方便理解我還準備了圖片:
十大排序演算法,看這篇就夠了(附完整程式碼/動圖/優質文章)

如果還是不懂的話我還給你準備了優質的文章講解:希爾排序

程式碼如下

public class ShellSort {
    public static int[] shellSort(int arr[]) {
        if (arr == null || arr.length < 2) return arr;
        int n = arr.length;
        // 對每組間隔為 h的分組進行排序,剛開始 h = n / 2;
        for (int h = n / 2; h > 0; h /= 2) {
            //對各個區域性分組進行插入排序
            for (int i = h; i < n; i++) {
                // 將arr[i] 插入到所在分組的正確位置上
                insertI(arr, h, i);
            }
     }
     return arr;
    }

    /**
     * 將arr[i]插入到所在分組的正確位置上
     * arr[i]] 所在的分組為 ... arr[i-2*h],arr[i-h], arr[i+h] ...
     */
    private static void insertI(int[] arr, int h, int i) {
        int temp = arr[i];
        int k;
        for (k = i - h; k > 0 && temp < arr[k]; k -= h) {
            arr[k + h] = arr[k];
        }
        arr[k + h] = temp;
    }
}

需要注意的是,對各個分組進行插入的時候並不是先對一個組排序完了再來對另一個組排序,而是輪流對每個組進行排序。

性質:1、時間複雜度:O(nlogn) 2、空間複雜度:O(1) 3、非穩定排序 4、原地排序

歸併排序

將一個大的無序陣列有序,我們可以把大的陣列分成兩個,然後對這兩個陣列分別進行排序,之後在把這兩個陣列合併成一個有序的陣列。由於兩個小的陣列都是有序的,所以在合併的時候是很快的。

通過遞迴的方式將大的陣列一直分割,直到陣列的大小為 1,此時只有一個元素,那麼該陣列就是有序的了,之後再把兩個陣列大小為1的合併成一個大小為2的,再把兩個大小為2的合併成4的 ..... 直到全部小的陣列合並起來。

為方便理解我還準備了動圖:

十大排序演算法,看這篇就夠了(附完整程式碼/動圖/優質文章)

如果還是不懂的話我還給你準備了優質的文章講解:歸併排序

程式碼如下:

public class MergeSort {
    // 歸併排序
    public static int[] mergeSort(int[] arr, int left, int right) {
        // 如果 left == right,表示陣列只有一個元素,則不用遞迴排序
        if (left < right) {
            // 把大的陣列分隔成兩個陣列
            int mid = (left + right) / 2;
            // 對左半部分進行排序
            arr = sort(arr, left, mid);
            // 對右半部分進行排序
            arr = sort(arr, mid + 1, right);
            //進行合併
            merge(arr, left, mid, right);
        }
        return arr;
    }

    // 合併函式,把兩個有序的陣列合並起來
    // arr[left..mif]表示一個陣列,arr[mid+1 .. right]表示一個陣列
    private static void merge(int[] arr, int left, int mid, int right) {
        //先用一個臨時陣列把他們合併彙總起來
        int[] a = new int[right - left + 1];
        int i = left;
        int j = mid + 1;
        int k = 0;
        while (i <= mid && j <= right) {
            if (arr[i] < arr[j]) {
                a[k++] = arr[i++];
            } else {
                a[k++] = arr[j++];
            }
        }
        while(i <= mid) a[k++] = arr[i++];
        while(j <= right) a[k++] = arr[j++];
        // 把臨時陣列複製到原陣列
        for (i = 0; i < k; i++) {
            arr[left++] = a[i];
        }
    }
}

性質:1、時間複雜度:O(nlogn) 2、空間複雜度:O(n) 3、穩定排序 4、非原地排序

然而面試官要你寫個非遞迴式的歸併排序怎麼辦?別怕,我這還擼了個非遞迴式的歸併排序,程式碼如下:

public class MergeSort {
    // 非遞迴式的歸併排序
    public static int[] mergeSort(int[] arr) {
        int n = arr.length;
        // 子陣列的大小分別為1,2,4,8...
        // 剛開始合併的陣列大小是1,接著是2,接著4....
        for (int i = 1; i < n; i += i) {
            //進行陣列進行劃分
            int left = 0;
            int mid = left + i - 1;
            int right = mid + i;
            //進行合併,對陣列大小為 i 的陣列進行兩兩合併
            while (right < n) {
                // 合併函式和遞迴式的合併函式一樣
                merge(arr, left, mid, right);
                left = right + 1;
                mid = left + i - 1;
                right = mid + i;
            }
            // 還有一些被遺漏的陣列沒合併,千萬別忘了
            // 因為不可能每個字陣列的大小都剛好為 i
            if (left < n && mid < n) {
                merge(arr, left, mid, n - 1);
            }
        }
        return arr;
    }
}

快速排序

我們從陣列中選擇一個元素,我們把這個元素稱之為中軸元素吧,然後把陣列中所有小於中軸元素的元素放在其左邊,所有大於或等於中軸元素的元素放在其右邊,顯然,此時中軸元素所處的位置的是有序的。也就是說,我們無需再移動中軸元素的位置。

從中軸元素那裡開始把大的陣列切割成兩個小的陣列(兩個陣列都不包含中軸元素),接著我們通過遞迴的方式,讓中軸元素左邊的陣列和右邊的陣列也重複同樣的操作,直到陣列的大小為1,此時每個元素都處於有序的位置

為方便理解我還準備了動圖:
十大排序演算法,看這篇就夠了(附完整程式碼/動圖/優質文章)

如果還是不懂的話我還給你準備了優質的文章講解:不要在問我快速排序

程式碼如下:

public class QuickSort {
    public static int[] quickSort(int[] arr, int left, int right) {
        if (left < right) {
            //獲取中軸元素所處的位置
            int mid = partition(arr, left, right);
            //進行分割
            arr = sort(arr, left, mid - 1);
            arr = sort(arr, mid + 1, right);
        }
        return arr;
    }

    private static int partition(int[] arr, int left, int right) {
        //選取中軸元素
        int pivot = arr[left];
        int i = left + 1;
        int j = right;
        while (true) {
            // 向右找到第一個小於等於 pivot 的元素位置
            while (i <= j && arr[i] <= pivot) i++;
            // 向左找到第一個大於等於 pivot 的元素位置
            while(i <= j && arr[j] >= pivot ) j--;
            if(i >= j)
                break;
            //交換兩個元素的位置,使得左邊的元素不大於pivot,右邊的不小於pivot
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
        arr[left] = arr[j];
        // 使中軸元素處於有序的位置
        arr[j] = pivot;
        return j;
    }
}

性質:1、時間複雜度:O(nlogn) 2、空間複雜度:O(logn) 3、非穩定排序 4、原地排序

堆排序

堆的特點就是堆頂的元素是一個最值,大頂堆的堆頂是最大值,小頂堆則是最小值。

堆排序就是把堆頂的元素與最後一個元素交換,交換之後破壞了堆的特性,我們再把堆中剩餘的元素再次構成一個大頂堆,然後再把堆頂元素與最後第二個元素交換....如此往復下去,等到剩餘的元素只有一個的時候,此時的陣列就是有序的了。

為方便理解我還準備了動圖:

十大排序演算法,看這篇就夠了(附完整程式碼/動圖/優質文章)

如果還是不懂的話我還給你準備了優質的文章講解:堆排序是什麼鬼?

程式碼如下:

public class Head {
    // 堆排序
    public static int[] headSort(int[] arr) {
        int n = arr.length;
        //構建大頂堆
        for (int i = (n - 2) / 2; i >= 0; i--) {
            downAdjust(arr, i, n - 1);
        }
        //進行堆排序
        for (int i = n - 1; i >= 1; i--) {
            // 把堆頂元素與最後一個元素交換
            int temp = arr[i];
            arr[i] = arr[0];
            arr[0] = temp;
            // 把打亂的堆進行調整,恢復堆的特性
            downAdjust(arr, 0, i - 1);
        }
        return arr;
    }
    
        //下沉操作
    public static void downAdjust(int[] arr, int parent, int n) {
        //臨時儲存要下沉的元素
        int temp = arr[parent];
        //定位左孩子節點的位置
        int child = 2 * parent + 1;
        //開始下沉
        while (child <= n) {
            // 如果右孩子節點比左孩子大,則定位到右孩子
            if(child + 1 <= n && arr[child] < arr[child + 1])
                child++;
            // 如果孩子節點小於或等於父節點,則下沉結束
            if (arr[child] <= temp ) break;
            // 父節點進行下沉
            arr[parent] = arr[child];
            parent = child;
            child = 2 * parent + 1;
        }
        arr[parent] = temp;
    }
}

性質:1、時間複雜度:O(nlogn) 2、空間複雜度:O(1) 3、非穩定排序 4、原地排序

計數排序

計數排序是一種適合於最大值和最小值的差值不是不是很大的排序。

基本思想:就是把陣列元素作為陣列的下標,然後用一個臨時陣列統計該元素出現的次數,例如 temp[i] = m, 表示元素 i 一共出現了 m 次。最後再把臨時陣列統計的資料從小到大彙總起來,此時彙總起來是資料是有序的。

為方便理解我還準備了動圖:
十大排序演算法,看這篇就夠了(附完整程式碼/動圖/優質文章)

如果還是不懂的話我還給你準備了優質的文章講解:什麼是計數排序?

程式碼如下:

public class Counting {
    public static int[] countSort(int[] arr) {
        if(arr == null || arr.length < 2) return arr;

        int n = arr.length;
        int max = arr[0];
        // 尋找陣列的最大值
        for (int i = 1; i < n; i++) {
            if(max < arr[i])
                max = arr[i];
        }
        //建立大小為max的臨時陣列
        int[] temp = new int[max + 1];
        //統計元素i出現的次數
        for (int i = 0; i < n; i++) {
            temp[arr[i]]++;
        }
        int k = 0;
        //把臨時陣列統計好的資料彙總到原陣列
        for (int i = 0; i <= max; i++) {
            for (int j = temp[i]; j > 0; j--) {
                arr[k++] = i;
            }
        }
        return arr;
    }
}

性質:1、時間複雜度:O(n+k) 2、空間複雜度:O(k) 3、穩定排序 4、非原地排序

注:K表示臨時陣列的大小,下同

優化一下

上面的程式碼中,我們是根據 max 的大小來建立對應大小的陣列,假如原陣列只有10個元素,並且最小值為 min = 10000,最大值為 max = 10005,那我們建立 10005 + 1 大小的陣列不是很吃虧,最大值與最小值的差值為 5,所以我們建立大小為6的臨時陣列就可以了。

也就是說,我們建立的臨時陣列大小 (max - min + 1)就可以了,然後在把 min作為偏移量。優化之後的程式碼如下所示:

public class Counting {
    public static int[] sort(int[] arr) {
        if(arr == null || arr.length < 2) return arr;

        int n = arr.length;
        int min = arr[0];
        int max = arr[0];
        // 尋找陣列的最大值與最小值
        for (int i = 1; i < n; i++) {
            if(max < arr[i])
                max = arr[i];
            if(min > arr[i])
                min = arr[i];
        }
        int d = max - min + 1;
        //建立大小為max的臨時陣列
        int[] temp = new int[d];
        //統計元素i出現的次數
        for (int i = 0; i < n; i++) {
            temp[arr[i] - min]++;
        }
        int k = 0;
        //把臨時陣列統計好的資料彙總到原陣列
        for (int i = 0; i < d; i++) {
            for (int j = temp[i]; j > 0; j--) {
                arr[k++] = i + min;
            }
        }
        return arr;
    }
}

桶排序

桶排序就是把最大值和最小值之間的數進行瓜分,例如分成 10 個區間,10個區間對應10個桶,我們把各元素放到對應區間的桶中去,再對每個桶中的數進行排序,可以採用歸併排序,也可以採用快速排序之類的。

之後每個桶裡面的資料就是有序的了,我們在進行合併彙總。

為方便理解我還準備了圖片:
十大排序演算法,看這篇就夠了(附完整程式碼/動圖/優質文章)

如果還是不懂的話我還給你準備了優質的文章講解:什麼是桶排序?

程式碼如下:

public class BucketSort {
    public static int[] BucketSort(int[] arr) {
        if(arr == null || arr.length < 2) return arr;

        int n = arr.length;
        int max = arr[0];
        int min = arr[0];
        // 尋找陣列的最大值與最小值
        for (int i = 1; i < n; i++) {
            if(min > arr[i])
                min = arr[i];
            if(max < arr[i])
                max = arr[i];
        }
        //和優化版本的計數排序一樣,弄一個大小為 min 的偏移值
        int d = max - min;
        //建立 d / 5 + 1 個桶,第 i 桶存放  5*i ~ 5*i+5-1範圍的數
        int bucketNum = d / 5 + 1;
        ArrayList<LinkedList<Integer>> bucketList = new ArrayList<>(bucketNum);
        //初始化桶
        for (int i = 0; i < bucketNum; i++) {
            bucketList.add(new LinkedList<Integer>());
        }
        //遍歷原陣列,將每個元素放入桶中
        for (int i = 0; i < n; i++) {
            bucketList.get((arr[i]-min)/d).add(arr[i] - min);
        }
        //對桶內的元素進行排序,我這裡採用系統自帶的排序工具
        for (int i = 0; i < bucketNum; i++) {
            Collections.sort(bucketList.get(i));
        }
        //把每個桶排序好的資料進行合併彙總放回原陣列
        int k = 0;
        for (int i = 0; i < bucketNum; i++) {
            for (Integer t : bucketList.get(i)) {
                arr[k++] = t + min;
            }
        }
        return arr;
    }
}

性質:1、時間複雜度:O(n+k) 2、空間複雜度:O(n+k) 3、穩定排序 4、非原地排序

注:k 表示桶的個數,下同

基數排序

基數排序的排序思路是這樣的:先以個位數的大小來對資料進行排序,接著以十位數的大小來多數進行排序,接著以百位數的大小......

排到最後,就是一組有序的元素了。不過,他在以某位數進行排序的時候,是用“桶”來排序的。

由於某位數(個位/十位....,不是一整個數)的大小範圍為0-9,所以我們需要10個桶,然後把具有相同數值的數放進同一個桶裡,之後再把桶裡的數按照0號桶到9號桶的順序取出來,這樣一趟下來,按照某位數的排序就完成了

為方便理解我還準備了動圖:

十大排序演算法,看這篇就夠了(附完整程式碼/動圖/優質文章)

如果還是不懂的話我還給你準備了優質的文章講解:為什麼說O(n)複雜度的基數排序沒有快速排序快?

程式碼如下:

public class RadioSort {

    public static int[] radioSort(int[] arr) {
        if(arr == null || arr.length < 2) return arr;

        int n = arr.length;
        int max = arr[0];
        // 找出最大值
        for (int i = 1; i < n; i++) {
            if(max < arr[i]) max = arr[i];
        }
        // 計算最大值是幾位數
        int num = 1;
        while (max / 10 > 0) {
            num++;
            max = max / 10;
        }
        // 建立10個桶
        ArrayList<LinkedList<Integer>> bucketList = new ArrayList<>(10);
        //初始化桶
        for (int i = 0; i < 10; i++) {
            bucketList.add(new LinkedList<Integer>());
        }
        // 進行每一趟的排序,從個位數開始排
        for (int i = 1; i <= num; i++) {
            for (int j = 0; j < n; j++) {
                // 獲取每個數最後第 i 位是陣列
                int radio = (arr[j] / (int)Math.pow(10,i-1)) % 10;
                //放進對應的桶裡
                bucketList.get(radio).add(arr[j]);
            }
            //合併放回原陣列
            int k = 0;
            for (int j = 0; j < 10; j++) {
                for (Integer t : bucketList.get(j)) {
                    arr[k++] = t;
                }
                //取出來合併了之後把桶清光資料
                bucketList.get(j).clear();
            }
        }
        return arr;
    }
}

性質:1、時間複雜度:O(kn) 2、空間複雜度:O(n+k) 3、穩定排序 4、非原地排序

總結

用一張圖彙總了10大排序演算法的性質

十大排序演算法,看這篇就夠了(附完整程式碼/動圖/優質文章)

如果你是複習/學習十大排序演算法,一定要自己不看示例程式碼手動實現一遍,一定要自己不看示例程式碼手動實現一遍,一定要自己不看示例程式碼手動實現一遍。

這波整理,留下點贊 + 鼓勵我一下?

最後推廣下我的公眾號:苦逼的碼農,文章都會首發於我的公眾號,期待各路英雄的關注交流。

2018原創文章彙總

相關文章