排序算法總結,含 java 高質量算法實現

排序算法總結

概念

  1. 穩定性:
    假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序後的序列中,r[i]仍在r[j]之前,則稱這種排序算法是穩定的;否則稱爲不穩定的。

排序算法分類

在這裏插入圖片描述

時間複雜度

排序算法 (平均)時間複雜度 (最好)時間複雜度 (最壞)時間複雜度 空間複雜度 穩定性
\color{red}比較排序:
插入排序 O(n2) O(n) (基本有序) n2 O(1) 穩定
希爾排序 不穩定
選擇排序 O(n2) O(n2) O(n2) O(1) 不穩定
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不穩定
冒泡排序 O(n2) O(n) O(n2) O(1) 穩定
快速排序 O(nlogn) O(nlogn) O(n2) O(nlogn) 不穩定
歸併排序 O(nlogn) O(nlogn) O(nlogn) O(n) 穩定
\color{red}非比較排序:
計數排序 O(n + k) O(n + k) O(n + k) O(n + k) 穩定
桶排序 O(n + k) O(n) O(n2) O(n + k) 穩定
基數排序 O(n * k) O(n * k) O(n * k) O(n + k) 穩定

Top10 排序算法講解

1. 直接插入排序

  1. 算法思想:
    第 i 趟插入排序爲:在含有 i − 1個元素的\color{red}有序子序列中插入一個元素,使之成爲含有 i 個元素的有序子序列。在查找插入位置的過程中,同時後移元素,所以適合從後向前掃描。
    整個過程爲進行 n − 1 趟插入, 即先將整個序列的第 1個元素看成是有序的,然後從第 2個元素起,逐個進行插入,直到整個序列有序 爲止。

  2. 算法實現:

/**
     * 插入排序實現函數
     *
     * @param arr 待排序序列
     * @return
     */
    public static int[] insertSort(int[] arr) {

        for (int i = 1; i < arr.length; i++) {
            int current = arr[i];
            int preIndex = i - 1;

            while (preIndex >= 0 && arr[preIndex] > current) {
//              後移元素
                arr[preIndex + 1] = arr[preIndex];
                preIndex--;
            }
//          找到插入位置之後,將第 i 個元素插入到有序序列中
            arr[preIndex + 1] = current;
        }
        return arr;
    }
  1. 算法優化:
    注意到插入排序的過程中,每次是將第 i 個元素插入到前面有序序列中,既然是插入到有序序列中,那麼可以採用二分查找的思想來尋找第i個元素插入到前面有序序列中的位置,這樣優化之後的算法時間複雜度爲O(nlogn)。優化之後的算法:
public int[] insertSortV2(int[] nums) {
        for (int i = 1; i < nums.length; i++) {
            int current = nums[i];

//          採用二分查找,查找插入排序第i個元素在前面有序序列中的位置 pivot
            int low = 0, mid = 0, high = i - 1;
            int pivot = 0;
            while (low <= high) {
                mid = (low + high) / 2;
                if (current < nums[mid]) {
                    high = mid - 1;
                }
                if (current >= nums[mid]) {
                    low = mid + 1;
                }
            }
//          找到插入位置之後,將第 i 個元素插入到有序序列中
            pivot = low;
            for (int j = i; j > pivot; j--) {
                nums[j] = nums[j - 1];
            }
            nums[pivot] = current;
        }
        return nums;
    }

測試平臺:LeetCode:912. 排序數組
1是使用最原始的插入排序的算法,2是使用優化之後的插入排序的算法
在這裏插入圖片描述


2.希爾排序

  1. 算法思想:
    希爾排序的思想是採用插入排序的方法,先讓數組中任意間隔爲 h 的元素有序,剛開始 h 的大小可以是 h = n / 2,接着讓 h = n / 4,讓 h 一直縮小,當 h = 1 時,也就是此時數組中任意間隔爲1的元素有序,此時的數組就是有序的了。
  2. 過程演示:
    在這裏插入圖片描述3. 算法實現:
    /**
     * 希爾排序算法實現
     *
     * @param arr 待排序序列
     * @return
     */
    public static int[] shellSort(int[] arr) {
        int len = arr.length;
//      設置默認增量爲:n/2
        int gap = len / 2;

        while (gap > 0) {
            for (int i = gap; i < len - 1; i++) {
                int current = arr[i];
                int preIndex = i - gap;

                while (preIndex >= 0 && arr[preIndex] > current) {
                    arr[preIndex + gap] = arr[preIndex];
                    preIndex -= gap;
                }
                arr[preIndex + gap] = current;
            }
            gap /= 2;
        }
        return arr;
    }

3. 選擇排序

  1. 算法思想:
      首先 在\color{red}整個排序序列中找到最小(大)元素,存放到排序序列的\color{red}起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到\color{red}已排序序列的末尾。以此類推,直到所有元素均排序完畢。

  2. 算法實現:

        /**
     * 選擇排序實現函數
     *
     * @param arr 待排序序列
     * @return
     */
    public static int[] selectSort(int arr[]) {
//      min記錄集合元素最小值的下標
        int minIndex = 0;
        for (int i = 0; i < arr.length; i++) {
            minIndex = i;
            for (int j = i + 1; j < arr.length; j++) {
//              未排序的序列中有值比已排序中的最小值還小,更新最小值的位置
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }
//          一趟排序結束,選擇最小的值放到已排序的第 i 的位置上
            if (minIndex != i) {
                int temp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = temp;
            }
        }
        return arr;
    }

4. 堆排序

  1. 概念:
  • 堆排序:
    堆排序是利用堆這種數據結構而設計的一種排序算法,堆排序是一種選擇排序,它的最壞,最好,平均時間複雜度均爲O(nlogn),它也是不穩定排序

  • 堆:
    堆是具有以下性質的\color{red}完全二叉樹:每個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆;或者每個結點的值都小於或等於其左右孩子結點的值,稱爲小頂堆。
    構建的 (大/小) 頂堆,二叉樹的根是整個序列的 最(大/小)值,堆排序正是利用的是這個特性。

  1. 算法思想:
  • 步驟1:將初始待排序關鍵字序列(R1,R2….Rn)構建成大頂堆,此堆爲初始的無序區
  • 步驟2:將R[1]\color{red}堆頂元素R[1]R[n]\color{red}最後一個元素R[n]交換,此時得到新的無序區(R1,R2,……Rn-1)和新的有序區(Rn),且滿足R[1,2…n-1]<=R[n];
  • 步驟3:由於交換後新的堆頂R[1]可能違反堆的性質,因此需要對當前無序區(R1,R2,……Rn-1)調\color{red}調整爲新堆,然後再次將R[1]與無序區最後一個元素交換,得到新的無序區(R1,R2….Rn-2)和新的有序區(Rn-1,Rn)。不斷重複此過程直到有序區的元素個數爲n-1,則整個排序過程完成。
  1. 算法實現:

5. 冒泡排序

  1. 算法思想:
      首先將第 1個元素和第 2個元素進行比較,若前者大於後者,則兩者交換位置,然後比較 第 2個元素和第 3個元素。依此類推,直到第 n − 1個元素和第 n個元素進行過比較或交換爲止。上 述過程稱爲\color{red}一趟冒泡排序,其結果是使得 n個元素中值最大的那個元素被安排在後一個元素的位置 上。然後進行第二趟排序,即對前 n − 1個元素進行同樣的操作,使得前 n − 1個元素中值最大的那 個元素被安排在第 n − 1個位置上。
      一般地,第 i 趟冒泡排序是從前 n − i + 1個元素中的第 1個元素 開始,\color{red}兩兩比較,若前者大於後者,則交換,結果使得前 n − i + 1個元素中最大的元素被安排在第 n − i + 1個位置上。

  2. 優化:
    顯然,判斷冒泡排序結束的條件是“在一趟排序中沒有進行過交換元素的操作”, 爲此,設立一個標誌變量 flag,flag = 1表示有過交換元素的操作,flag = 0表示沒有過交換元素的操 作,在每一趟排序開始前,將 flag置爲 0,在排序過程中,只要有交換元素的操作,就及時將 flag置 爲 1。因爲至少要執行一趟排序操作,故第一趟排序時,flag = 1。

  3. 算法實現:
    以下提供的兩個函數,皆爲冒泡算法的實現。
    bubbleSort 爲 原始的冒泡排序,bubbleSortV2爲加入flag優化過之後的冒泡排序。
    大家可以在第二個循環內加入count來測試以下優化前後兩個算法的差異,可以感受到第二個函數循環執行的次數要少於第一個。

/**
     * 冒泡排序函數實現
     *
     * @param arr 待排序序列
     * @return
     */
    public static int[] bubbleSort(int[] arr) {

        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr.length - 1 - i; j++) {
                if (arr[j + 1] < arr[j]) {
                    int temp = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = temp;
                }
            }
        }
        return arr;
    }

 /**
     * 冒泡排序函數實現,通過判斷一趟冒泡排序中,位置是否發生變化來優化,減少循環的次數
     *
     * @param arr 待排序數組
     * @return
     */
    public static int[] bubbleSortV2(int[] arr) {

//      判斷一趟冒泡排序過程中,是否發生交換,如果沒有發生交換,則代表序列已經有序。
//      flag = 1:發生交換,flag = 0:無交換
        int flag = 1;

        for (int i = 0; i < arr.length && flag == 1; i--) {
            flag = 0;
            for (int j = 0; j < arr.length - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    flag = 1;
                }
            }
        }
        return arr;
    }

6. 快速排序

  1. 算法思想:
      在序列中任意選擇一個元素(通常稱爲分界元素或\color{red}基準元素),把小於或等於基準的所有元素都移到基準的前面,把大於基準的所有元素都移到基準的後面, 這樣,當前序列就被劃分成前後兩個子序列,其中前一個子序列中的所有元素都小於後一個子序列的所有元素,並且基準正好處於排序的最終位置上。然後分別對這兩個子序列遞歸 地進行上述排序過程,直到所有元素都處於排序的最終位置上,排序結束。
    快速排序的本質:每趟排序將選擇的基準放到正確的位置。
  2. 算法實現:
/**
     * 快速排序算法實現
     *
     * @param arr 待排序序列
     * @return
     */
    private static int[] quickSort(int[] arr, int low, int high) {

        if (low < high) {
            int pivotIndex = getPivotIndex(arr, low, high);
            quickSort(arr, low, pivotIndex - 1);
            quickSort(arr, pivotIndex + 1, high);
        }
        return arr;
    }

    /**
     * 快速排序算法 -- 確定基準正確位置
     *
     * @param arr
     * @param low
     * @param high
     * @return
     */
    private static int getPivotIndex(int[] arr, int low, int high) {
//      獲取基準數據
        int pivot = arr[low];

        while (low < high) {
//          尾指針向前遍歷,當後面元素大於等於基準,high--
            if (low < high && arr[high] >= pivot) {
                high--;
            }
            arr[low] = arr[high];
//          頭指針向後遍歷,當前面元素小於等於基準,low++
            if (low < high && arr[low] <= pivot) {
                low++;
            }
            arr[high] = arr[low];
        }
//      當頭指針和尾指針重合時,這個位置便是這一趟排序基準的正確位置
        arr[low] = pivot;
//      返回基準的正確位置
        return low;
    }

7. 歸併排序

和選擇排序一樣,歸併排序的性能不受輸入數據的影響,但表現比選擇排序好的多,因爲始終都是O(n log n)的時間複雜度。代價是需要額外的內存空間。

  1. 算法思想:
      歸併排序是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法的一個非常典型的應用。歸併排序算法有兩個基本的操作,一個是分,也就是把原數組劃分成兩個子數組的過程。另一個是治,它將兩個有序數組合併成一個更大的有序數組。

    將數組平均分成兩部分: center = (left + right)/2,當數組分得足夠小時—數組中只有一個元素時,只有一個元素的數組自然而然地就可以視爲是有序的,此時就可以進行合併操作了。因此,上面講的合併兩個有序的子數組,是從 只有一個元素 的兩個子數組開始合併的。


8. 計數排序

  1. 算法思想:
    計數排序不是基於比較的排序算法,其核心在於將輸入的數據值轉化爲\color{red}鍵存儲在額外開闢的數組空間中。 作爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是\color{red}有確定範圍的整數

    當輸入的元素是 n 個0到k之間的整數時,它的運行時間是 O(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序算法。由於用來計數的數組C的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上1),這使得計數排序對於數據範圍很大的數組,需要大量時間和內存。

  2. 算法演示:

    /**
     * 計數排序算法實現
     *
     * @param arr
     * @return
     */
    public static int[] countingSort(int[] arr) {
        if (arr.length == 0) {
            return arr;
        }

//      min,max:確定用於計數的數組的大小,計數數組大小爲:(max - min + 1)
        int min = arr[0], max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] < min) {
                min = arr[i];
            }
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        int[] bucket = new int[max - min + 1];

//      統計序列中各個元素出現的次數count
        for (int i = 0; i < arr.length; i++) {
            int bias = arr[i] - min;
//          第 i 個元素頻次加1
            bucket[bias]++;
        }

//      把計數數組統計好的數據彙總到原數組
        int index = 0;
        for (int i = 0; i < bucket.length; i++) {
            for (int j = 0; j < bucket[i]; j++) {
                arr[index] = min + i;
                index++;
            }
        }
        return arr;
    }

9. 桶排序

  1. 算法思想:
    桶排序就是把最大值和最小值之間的數進行瓜分,例如分成 10 個區間,10個區間對應10個桶,我們把各元素放到對應區間的桶中去,再對每個桶中的數進行排序,可以採用歸併排序,也可以採用快速排序之類的。
    之後每個桶裏面的數據就是有序的了,我們在進行合併彙總。
  2. 算法演示:
    在這裏插入圖片描述

10. 基數排序

  1. 算法思想:
    基數排序也是非比較的排序算法,對每一位進行排序,從最低位開始排序,時間複雜度爲O(kn), n爲數組長度,k爲數組中的數的最大的位數;

    基數排序是按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序。最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。基數排序基於分別排序,分別收集,所以是穩定的。

  2. 算法實現:

/**
 * 基數排序算法實現
 *
 * @param arr 待排序序列
 * @return
 */
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++) {
        max = Math.max(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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章