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

說明

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

術語鋪墊

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

一、穩定排序:若是 a 本來在 b 的前面,且 a == b,排序以後 a 仍然在 b 的前面,則爲穩定排序。算法

二、非穩定排序:若是 a 本來在 b 的前面,且 a == b,排序以後 a 可能不在 b 的前面,則爲非穩定排序。shell

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

四、非原地排序:須要利用額外的數組來輔助排序。函數

五、時間複雜度:一個算法執行所消耗的時間。工具

六、空間複雜度:運行完一個算法所需的內存大小。學習

十大排序講解順序

爲了方便你們查找,我這裏弄一個僞目錄,沒有跳轉功能。測試

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

另:優化

代碼說明:代碼我本身寫的,而且都是通過好幾組數據測試經過,應該沒啥問題,若有錯,還請反饋下,謝謝。

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

選擇排序

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

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

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

代碼以下:

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;
    }
}

性質:一、時間複雜度:O(n2) 二、空間複雜度:O(1) 三、非穩定排序 四、原地排序

插入排序

咱們在玩打牌的時候,你是怎麼整理那些牌的呢?一種簡單的方法就是一張一張的來,將每一張牌插入到其餘已經有序的牌中的適當位置。當咱們給無序數組作排序的時候,爲了要插入元素,咱們須要騰出空間,將其他全部元素在插入以前都向右移動一位,這種算法咱們稱之爲插入排序

過程簡單描述:

一、從數組第2個元素開始抽取元素。

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

三、繼續選取第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;
    }
}

性質:一、時間複雜度:O(n2) 二、空間複雜度:O(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;
    }
)

性質:一、時間複雜度:O(n2) 二、空間複雜度:O(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++) {
            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;
    }
}

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

性質:一、時間複雜度:O(nlogn) 二、空間複雜度:O(1) 三、非穩定排序 四、原地排序

歸併排序

將一個大的無序數組有序,咱們能夠把大的數組分紅兩個,而後對這兩個數組分別進行排序,以後在把這兩個數組合併成一個有序的數組。因爲兩個小的數組都是有序的,因此在合併的時候是很快的。

經過遞歸的方式將大的數組一直分割,直到數組的大小爲 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];
        }
    }
}

性質:一、時間複雜度:O(nlogn) 二、空間複雜度:O(n) 三、穩定排序 四、非原地排序

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

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;
    }
}

性質:一、時間複雜度:O(nlogn) 二、空間複雜度:O(logn) 三、非穩定排序 四、原地排序

堆排序

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

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

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

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

代碼以下:

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;
    }
}

性質:一、時間複雜度:O(nlogn) 二、空間複雜度:O(1) 三、非穩定排序 四、原地排序

計數排序

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

基本思想:就是把數組元素做爲數組的下標,而後用一個臨時數組統計該元素出現的次數,例如 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;
    }
}

性質:一、時間複雜度:O(n+k) 二、空間複雜度:O(k) 三、穩定排序 四、非原地排序

注: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;
    }
}

性質:一、時間複雜度:O(n+k) 二、空間複雜度:O(n+k) 三、穩定排序 四、非原地排序

注: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;
    }
}

性質:一、時間複雜度:O(kn) 二、空間複雜度:O(n+k) 三、穩定排序 四、非原地排序

總結

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

若是你是複習/學習十大排序算法,必定要本身不看示例代碼手動實現一遍,必定要本身不看示例代碼手動實現一遍,必定要本身不看示例代碼手動實現一遍。

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

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

2018原創文章彙總

相關文章
相關標籤/搜索