[Leedcode][JAVA][第912題][排序算法]

【問題描述】

給你一個整數數組 nums,將該數組升序排列。
示例 1:
輸入:nums = [5,2,3,1]
輸出:[1,2,3,5]

【解答思路】

1.插入排序(熟悉)

每次將一個數字插入一個有序的數組里,成為一個長度更長的有序數組,有限次操作以后,數組整體有序。

  • 優化:「將一個數字插入一個有序的數組」這一步,可以不使用逐步交換,使用先賦值給「臨時變量」,然后「適當的元素」后移,空出一個位置,最后把「臨時變量」賦值給這個空位的策略(就是上面那張圖的意思)。編碼的時候如果不小心,可能會把數組的值修改,建議多調試

時間復雜度:O(N^2) 空間復雜度:O(1)

public class Solution {

    // 插入排序:穩定排序,在接近有序的情況下,表現優異

    public int[] sortArray(int[] nums) {
        int len = nums.length;
        // 循環不變量:將 nums[i] 插入到區間 [0, i) 使之成為有序數組
        for (int i = 1; i < len; i++) {
            // 先暫存這個元素,然后之前元素逐個后移,留出空位
            int temp = nums[i];
            int j = i;
            // 注意邊界 j > 0
            while (j > 0 && nums[j - 1] > temp) {
                nums[j] = nums[j - 1];
                j--;
            }
            nums[j] = temp;
        }
        return nums;
    }
}

作者:liweiwei1419
鏈接:https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/
2.歸并排序(重點)

借助額外空間,合并兩個有序數組,得到更長的有序數組。
時間復雜度:O(NlogN) 空間復雜度:O(N)

  • 優化 1:在「小區間」里轉向使用「插入排序」,Java 源碼里面也有類似這種操作,「小區間」的長度是個超參數,需要測試決定,我這里參考了 JDK 源碼;
  • 優化 2: 在「兩個數組」本身就是有序的情況下,無需合并;
  • 優化 3:全程使用一份臨時數組進行「合并兩個有序數組」的操作,避免創建臨時數組和銷毀的消耗,避免計算下標偏移量。
  • 注意:實現歸并排序的時候,要特別注意,不要把這個算法實現成非穩定排序,區別就在 <= 和 < ,已在代碼中注明。
public class Solution {
    // 歸并排序

    /**
     * 列表大小等于或小于該大小,將優先于 mergeSort 使用插入排序
     */
    private static final int INSERTION_SORT_THRESHOLD = 7;

    public int[] sortArray(int[] nums) {
        int len = nums.length;
        int[] temp = new int[len];
        mergeSort(nums, 0, len - 1, temp);
        return nums;
    }

    /**
     * 對數組 nums 的子區間 [left, right] 進行歸并排序
     *
     * @param nums
     * @param left
     * @param right
     * @param temp  用于合并兩個有序數組的輔助數組,全局使用一份,避免多次創建和銷毀
     */
    private void mergeSort(int[] nums, int left, int right, int[] temp) {
        // 小區間使用插入排序
        if (right - left <= INSERTION_SORT_THRESHOLD) {
            insertionSort(nums, left, right);
            return;
        }

        int mid = left + (right - left) / 2;
        // Java 里有更優的寫法,在 left 和 right 都是大整數時,即使溢出,結論依然正確
        // int mid = (left + right) >>> 1;

        mergeSort(nums, left, mid, temp);
        mergeSort(nums, mid + 1, right, temp);
        // 如果數組的這個子區間本身有序,無需合并
        if (nums[mid] <= nums[mid + 1]) {
            return;
        }
        mergeOfTwoSortedArray(nums, left, mid, right, temp);
    }

    /**
     * 對數組 arr 的子區間 [left, right] 使用插入排序
     *
     * @param arr   給定數組
     * @param left  左邊界,能取到
     * @param right 右邊界,能取到
     */
    private void insertionSort(int[] arr, int left, int right) {
        for (int i = left + 1; i <= right; i++) {
            int temp = arr[i];
            int j = i;
            while (j > left && arr[j - 1] > temp) {
                arr[j] = arr[j - 1];
                j--;
            }
            arr[j] = temp;
        }
    }

    /**
     * 合并兩個有序數組:先把值復制到臨時數組,再合并回去
     *
     * @param nums
     * @param left
     * @param mid   [left, mid] 有序,[mid + 1, right] 有序
     * @param right
     * @param temp  全局使用的臨時數組
     */
    private void mergeOfTwoSortedArray(int[] nums, int left, int mid, int right, int[] temp) {
       //void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
        System.arraycopy(nums, left, temp, left, right + 1 - left);

        int i = left;
        int j = mid + 1;
//兩條有序序列合并
        for (int k = left; k <= right; k++) {
//left     mid +1
//[1,1,2,3]  [5,6,7,8]  
 //邊界 左邊遍歷完 右邊還沒開始 右邊持續插入
            if (i == mid + 1) {
                nums[k] = temp[j];
//沒能改變值 
                j++;
// [5,6,7,8]   [1,1,2,3]
//邊界  右邊遍歷完 左邊還沒開始 左邊持續插入
            } else if (j == right + 1) {
                nums[k] = temp[i];
                i++;
            } else if (temp[i] <= temp[j]) {
                // 注意寫成 < 就丟失了穩定性(相同元素原來靠前的排序以后依然靠前)
                nums[k] = temp[i];
                i++;
            } else {
                // temp[i] > temp[j]
                nums[k] = temp[j];
                j++;
            }
        }
    }
}

作者:liweiwei1419
鏈接:https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/

3. 快速排序(重點)

快速排序每一次都排定一個元素(這個元素呆在了它最終應該呆的位置),然后遞歸地去排它左邊的部分和右邊的部分,依次進行下去,直到數組有序;


快排版本
import java.util.Random;

public class Solution {

    // 快速排序 1:基本快速排序

    /**
     * 列表大小等于或小于該大小,將優先于 quickSort 使用插入排序
     */
    private static final int INSERTION_SORT_THRESHOLD = 7;

    private static final Random RANDOM = new Random();


    public int[] sortArray(int[] nums) {
        int len = nums.length;
        quickSort(nums, 0, len - 1);
        return nums;
    }

    private void quickSort(int[] nums, int left, int right) {
        // 小區間使用插入排序
        if (right - left <= INSERTION_SORT_THRESHOLD) {
            insertionSort(nums, left, right);
            return;
        }

        int pIndex = partition(nums, left, right);
        quickSort(nums, left, pIndex - 1);
        quickSort(nums, pIndex + 1, right);
    }

    /**
     * 對數組 nums 的子區間 [left, right] 使用插入排序
     *
     * @param nums  給定數組
     * @param left  左邊界,能取到
     * @param right 右邊界,能取到
     */
    private void insertionSort(int[] nums, int left, int right) {
        for (int i = left + 1; i <= right; i++) {
            int temp = nums[i];
            int j = i;
            while (j > left && nums[j - 1] > temp) {
                nums[j] = nums[j - 1];
                j--;
            }
            nums[j] = temp;
        }
    }

//版本 1:基本快排:把等于切分元素的所有元素分到了數組的同一側,可能會造成遞歸樹傾斜;
    private int partition(int[] nums, int left, int right) {
//避免有序數組 效率低下
////隨機生成一個整數,這個整數的范圍就是[0,right - left + 1)
        int randomIndex = RANDOM.nextInt(right - left + 1) + left;
        swap(nums, left, randomIndex);

        // 基準值
        int pivot = nums[left];
        int lt = left;
        // 循環不變量:
        // all in [left + 1, lt] < pivot
        // all in [lt + 1, i) >= pivot
        for (int i = left + 1; i <= right; i++) {
            if (nums[i] < pivot) {
                lt++;
                swap(nums, i, lt);
            }
        }
        swap(nums, left, lt);
        return lt;
    }
//版本 2:雙指針快排:把等于切分元素的所有元素等概率地分到了數組的兩側,避免了遞歸樹傾斜,遞歸樹相對平衡;
 private int partition(int[] nums, int left, int right) {
        int randomIndex = left + RANDOM.nextInt(right - left + 1);
        swap(nums, randomIndex, left);

        int pivot = nums[left];
        int lt = left + 1;
        int gt = right;

        // 循環不變量:
        // all in [left + 1, lt) <= pivot
        // all in (gt, right] >= pivot
        while (true) {
            while (lt <= right && nums[lt] < pivot) {
                lt++;
            }

            while (gt > left && nums[gt] > pivot) {
                gt--;
            }

            if (lt > gt) {
                break;
            }

            // 細節:相等的元素通過交換,等概率分到數組的兩邊
            swap(nums, lt, gt);
            lt++;
            gt--;
        }
        swap(nums, left, gt);
        return gt;
    }


    private void swap(int[] nums, int index1, int index2) {
        int temp = nums[index1];
        nums[index1] = nums[index2];
        nums[index2] = temp;
    }
}

作者:liweiwei1419
鏈接:https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/

版本 3:三指針快排:把等于切分元素的所有元素擠到了數組的中間,在有很多元素和切分元素相等的情況下,遞歸區間大大減少
-partition() 合并到quickSort()

private void quickSort(int[] nums, int left, int right) {
      // 小區間使用插入排序
      if (right - left <= INSERTION_SORT_THRESHOLD) {
          insertionSort(nums, left, right);
          return;
      }

      int randomIndex = left + RANDOM.nextInt(right - left + 1);
      swap(nums, randomIndex, left);

      // 循環不變量:
      // all in [left + 1, lt] < pivot
      // all in [lt + 1, i) = pivot
      // all in [gt, right] > pivot
      int pivot = nums[left];
      int lt = left;
      int gt = right + 1;

      int i = left + 1;
      while (i < gt) {
          if (nums[i] < pivot) {
              lt++;
              swap(nums, i, lt);
              i++;
          } else if (nums[i] == pivot) {
              i++;
          } else {
              gt--;
              swap(nums, i, gt);
          }
      }
      swap(nums, left, lt);
      // 注意這里,大大減少了兩側分治的區間
      quickSort(nums, left, lt - 1);
      quickSort(nums, gt, right);
  }

作者:liweiwei1419
鏈接:https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/

4. 堆排序(堆很重要,堆排序根據個人情況掌握)

堆排序是選擇排序的優化,選擇排序需要在未排定的部分里通過「打擂臺」的方式選出最大的元素(復雜度 O(N)O(N)),而「堆排序」就把未排定的部分構建成一個「堆」,這樣就能以 O(\log N)O(logN) 的方式選出最大元素;

  • 參考《算法 4》堆部分
public class Solution {

   public int[] sortArray(int[] nums) {
       int len = nums.length;
       // 將數組整理成堆
       heapify(nums);

       // 循環不變量:區間 [0, i] 堆有序
       for (int i = len - 1; i >= 1; ) {
           // 把堆頂元素(當前最大)交換到數組末尾
           swap(nums, 0, i);
           // 逐步減少堆有序的部分
           i--;
           // 下標 0 位置下沉操作,使得區間 [0, i] 堆有序
           siftDown(nums, 0, i);
       }
       return nums;
   }

   /**
    * 將數組整理成堆(堆有序)
    *
    * @param nums
    */
   private void heapify(int[] nums) {
       int len = nums.length;
       // 只需要從 i = (len - 1) / 2 這個位置開始逐層下移
       for (int i = (len - 1) / 2; i >= 0; i--) {
           siftDown(nums, i, len - 1);
       }
   }

   /**
    * @param nums
    * @param k    當前下沉元素的下標
    * @param end  [0, end] 是 nums 的有效部分
    */
   private void siftDown(int[] nums, int k, int end) {
       while (2 * k + 1 <= end) {
           int j = 2 * k + 1;
           if (j + 1 <= end && nums[j + 1] > nums[j]) {
               j++;
           }
           if (nums[j] > nums[k]) {
               swap(nums, j, k);
           } else {
               break;
           }
           k = j;
       }
   }

   private void swap(int[] nums, int index1, int index2) {
       int temp = nums[index1];
       nums[index1] = nums[index2];
       nums[index2] = temp;
   }
}

作者:liweiwei1419
鏈接:https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/

5. 選擇排序

每一輪選取未排定的部分中最小的部分交換到未排定部分的最開頭,經過若干個步驟,就能排定整個數組。即:先選出最小的,再選出第 2 小的,以此類推。
時間復雜度:O(N) 空間復雜度:O(1)

  // 選擇排序:每一輪選擇最小元素交換到未排定部分的開頭
import java.util.Arrays;

public class Solution {

    // 選擇排序:每一輪選擇最小元素交換到未排定部分的開頭

    public int[] sortArray(int[] nums) {
        int len = nums.length;
        // 循環不變量:[0, i) 有序,且該區間里所有元素就是最終排定的樣子
        for (int i = 0; i < len - 1; i++) {
            // 選擇區間 [i, len - 1] 里最小的元素的索引,交換到下標 i
            int minIndex = i;
            for (int j = i + 1; j < len; j++) {
                if (nums[j] < nums[minIndex]) {
                    minIndex = j;
                }
            }
            swap(nums, i, minIndex);
        }
        return nums;
    }

    private void swap(int[] nums, int index1, int index2) {
        int temp = nums[index1];
        nums[index1] = nums[index2];
        nums[index2] = temp;
    }

    public static void main(String[] args) {
        int[] nums = {5, 2, 3, 1};
        Solution solution = new Solution();
        int[] res = solution.sortArray(nums);
        System.out.println(Arrays.toString(res));
    }
}

作者:liweiwei1419
鏈接:https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/
6. 希爾排序(不建議多花時間了解)

插入排序的優化。在插入排序里,如果靠后的數字較小,它來到前面就得交換多次。「希爾排序」改進了這種做法。帶間隔地使用插入排序,直到最后「間隔」為 1 的時候,就是標準的「插入排序」,此時數組里的元素已經「幾乎有序」
希爾排序的時間復雜度至今還沒有明確的結論,只有一個范圍

public class Solution {

    // 希爾排序

    public int[] sortArray(int[] nums) {
        int len = nums.length;
        int h = 1;

        // 使用 Knuth 增量序列
        // 找增量的最大值
        while (3 * h + 1 < len) {
            h = 3 * h + 1;
        }

        while (h >= 1) {
            // insertion sort
            for (int i = h; i < len; i++) {
                insertionForDelta(nums, h, i);
            }
            h = h / 3;
        }
        return nums;
    }

    /**
     * 將 nums[i] 插入到對應分組的正確位置上,其實就是將原來 1 的部分改成 gap
     *
     * @param nums
     * @param gap
     * @param i
     */
    private void insertionForDelta(int[] nums, int gap, int i) {
        int temp = nums[i];
        int j = i;
        // 注意:這里 j >= deta 的原因
        while (j >= gap && nums[j - gap] > temp) {
            nums[j] = nums[j - gap];
            j -= gap;
        }
        nums[j] = temp;
    }
}

作者:liweiwei1419
鏈接:https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/

7. 冒泡排序(了解)

外層循環每一次經過兩兩比較,把每一輪未排定部分最大的元素放到了數組的末尾

public class Solution {

    // 冒泡排序:超時

    public int[] sortArray(int[] nums) {
        int len = nums.length;
        for (int i = len - 1; i >= 0; i--) {
            // 先默認數組是有序的,只要發生一次交換,就必須進行下一輪比較,
            // 如果在內層循環中,都沒有執行一次交換操作,說明此時數組已經是升序數組
            boolean sorted = true;
            for (int j = 0; j < i; j++) {
                if (nums[j] > nums[j + 1]) {
                    swap(nums, j, j + 1);
                    sorted = false;
                }
            }
            if (sorted) {
                break;
            }
        }
        return nums;
    }

    private void swap(int[] nums, int index1, int index2) {
        int temp = nums[index1];
        nums[index1] = nums[index2];
        nums[index2] = temp;
    }
}

作者:liweiwei1419
鏈接:https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/
8. 計數排序(了解)

每個出現的數值都做一個計數,然后根據計數從小到大輸出得到有序數組。
-保持穩定性的做法是:先對計數數組做前綴和,在第 2 步往回賦值的時候,根據原始輸入數組的數據從后向前賦值,前綴和數組保存了每個元素存放的下標信息(這里沒有說得太細,本來這一點就不重要,也不難理解)。

作者:liweiwei1419
鏈接:https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/

public class Solution {

    // 計數排序

    private static final int OFFSET = 50000;

    public int[] sortArray(int[] nums) {
        int len = nums.length;
        // 由于 -50000 <= A[i] <= 50000
        // 因此"桶" 的大小為 50000 - (-50000) = 10_0000
        // 并且設置偏移 OFFSET = 50000,目的是讓每一個數都能夠大于等于 0
        // 這樣就可以作為 count 數組的下標,查詢這個數的計數
        int size = 100000;

        // 計數數組
        int[] count = new int[size];
        // 計算計數數組
        for (int num : nums) {
            count[num + OFFSET]++;
        }

        // 把 count 數組變成前綴和數組
        for (int i = 1; i < size; i++) {
            count[i] += count[i - 1];
        }

        // 先把原始數組賦值到一個臨時數組里,然后回寫數據
        int[] temp = new int[len];
        System.arraycopy(nums, 0, temp, 0, len);

        // 為了保證穩定性,從后向前賦值
        for (int i = len - 1; i >= 0; i--) {
            int index = count[temp[i] + OFFSET] - 1;
            nums[index] = temp[i];
            count[temp[i] + OFFSET]--;
        }
        return nums;
    }
}

作者:liweiwei1419
鏈接:https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/
9. 選擇排序(了解)
  • 基于關鍵字的排序,例如針對數值排序,個位、十位、百位就是關鍵字。針對日期數據的排序:年、月、日、時、分、秒就是關鍵字
  • 「基數排序」用到了「計數排序」
public class Solution {

    // 基數排序:低位優先

    private static final int OFFSET = 50000;

    public int[] sortArray(int[] nums) {
        int len = nums.length;

        // 預處理,讓所有的數都大于等于 0,這樣才可以使用基數排序
        for (int i = 0; i < len; i++) {
            nums[i] += OFFSET;
        }

        // 第 1 步:找出最大的數字
        int max = nums[0];
        for (int num : nums) {
            if (num > max) {
                max = num;
            }
        }

        // 第 2 步:計算出最大的數字有幾位,這個數值決定了我們要將整個數組看幾遍
        int maxLen = getMaxLen(max);

        // 計數排序需要使用的計數數組和臨時數組
        int[] count = new int[10];
        int[] temp = new int[len];

        // 表征關鍵字的量:除數
        // 1 表示按照個位關鍵字排序
        // 10 表示按照十位關鍵字排序
        // 100 表示按照百位關鍵字排序
        // 1000 表示按照千位關鍵字排序
        int divisor = 1;
        // 有幾位數,外層循環就得執行幾次
        for (int i = 0; i < maxLen; i++) {

            // 每一步都使用計數排序,保證排序結果是穩定的
            // 這一步需要額外空間保存結果集,因此把結果保存在 temp 中
            countingSort(nums, temp, divisor, len, count);

            // 交換 nums 和 temp 的引用,下一輪還是按照 nums 做計數排序
            int[] t = nums;
            nums = temp;
            temp = t;

            // divisor 自增,表示采用低位優先的基數排序
            divisor *= 10;
        }

        int[] res = new int[len];
        for (int i = 0; i < len; i++) {
            res[i] = nums[i] - OFFSET;
        }
        return res;
    }

    private void countingSort(int[] nums, int[] res, int divisor, int len, int[] count) {
        // 1、計算計數數組
        for (int i = 0; i < len; i++) {
            // 計算數位上的數是幾,先取個位,再十位、百位
            int remainder = (nums[i] / divisor) % 10;
            count[remainder]++;
        }

        // 2、變成前綴和數組
        for (int i = 1; i < 10; i++) {
            count[i] += count[i - 1];
        }

        // 3、從后向前賦值
        for (int i = len - 1; i >= 0; i--) {
            int remainder = (nums[i] / divisor) % 10;
            int index = count[remainder] - 1;
            res[index] = nums[i];
            count[remainder]--;
        }

        // 4、count 數組需要設置為 0 ,以免干擾下一次排序使用
        for (int i = 0; i < 10; i++) {
            count[i] = 0;
        }
    }

    /**
     * 獲取一個整數的最大位數
     *
     * @param num
     * @return
     */
    private int getMaxLen(int num) {
        int maxLen = 0;
        while (num > 0) {
            num /= 10;
            maxLen++;
        }
        return maxLen;
    }
}

作者:liweiwei1419
鏈接:https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/

10. 桶排序(了解)
ublic class Solution {

    // 桶排序
    // 1 <= A.length <= 10000
    // -50000 <= A[i] <= 50000

    // 10_0000

    private static final int OFFSET = 50000;

    public int[] sortArray(int[] nums) {
        int len = nums.length;
        // 第 1 步:將數據轉換為 [0, 10_0000] 區間里的數
        for (int i = 0; i < len; i++) {
            nums[i] += OFFSET;
        }

        // 第 2 步:觀察數據,設置桶的個數
        // 步長:步長如果設置成 10 會超出內存限制
        int step = 1000;
        // 桶的個數
        int bucketLen = 100000 / step;

        int[][] temp = new int[bucketLen + 1][len];
        int[] next = new int[bucketLen + 1];

        // 第 3 步:分桶
        for (int num : nums) {
            int bucketIndex = num / step;
            temp[bucketIndex][next[bucketIndex]] = num;
            next[bucketIndex]++;
        }

        // 第 4 步:對于每個桶執行插入排序
        for (int i = 0; i < bucketLen + 1; i++) {
            insertionSort(temp[i], next[i] - 1);
        }

        // 第 5 步:從桶里依次取出來
        int[] res = new int[len];
        int index = 0;
        for (int i = 0; i < bucketLen + 1; i++) {
            int curLen = next[i];
            for (int j = 0; j < curLen; j++) {
                res[index] = temp[i][j] - OFFSET;
                index++;
            }
        }
        return res;
    }

    private void insertionSort(int[] arr, int endIndex) {
        for (int i = 1; i <= endIndex; i++) {
            int temp = arr[i];
            int j = i;
            while (j > 0 && arr[j - 1] > temp) {
                arr[j] = arr[j - 1];
                j--;
            }
            arr[j] = temp;
        }
    }
}

作者:liweiwei1419
鏈接:https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/

【總結】

1.排序算法總結
維基百科

1.1 插入排序

  • 時間復雜度:O(N^2) 空間復雜度:O(1)
  • 優點 :在「幾乎有序」的數組上表現良好,特別地,在「短數組」上的表現也很好。因為「短數組」的特點是每個元素離它最終排定的位置都不會太遠
  • 應用:在小區間內執行排序任務的時候,可以轉向使用「插入排序」

1.2 歸并排序

  • 時間復雜度:O(NlogN) 空間復雜度:O(N)
    -算法思想:遞歸、分治處理問題的思想在算法領域是非常常見的,通過編寫「歸并排序」學習遞歸思想,了解遞歸的細節,熟悉分治思想,是相當好的學習材料。
    -優點:「歸并排序」比「快速排序」好的一點是,它借助了額外空間,可以實現「穩定排序」,Java 里對于「對象數組」的排序任務,就是使用歸并排序(的升級版 TimSort,在這里就不多做介紹)。

1.3 快排

  • 時間復雜度:O(NlogN) 空間復雜度:O(logN)
  • 算法思想:分而治之(分治思想),與「歸并排序」不同,「快速排序」在「分」這件事情上不想「歸并排序」無腦地一分為二,而是采用了 partition 的方法,因此就沒有「合」的過程。


    partition

    -缺點: 快速排序丟失了穩定性,如果需要穩定的快速排序,需要具體定義比較函數,這個過程叫「穩定化」,在這里就不展開了。

  • 缺點:(針對特殊測試用例:順序數組或者逆序數組)一定要隨機化選擇切分元素(pivot),否則在輸入數組是有序數組或者是逆序數組的時候,快速排序會變得非常慢(等同于冒泡排序或者「選擇排序」);

1.4 堆排序

  • 時間復雜度:O(NlogN) 空間復雜度:O(1)
  • 堆排序是選擇排序的優化,選擇排序需要在未排定的部分里通過「打擂臺」的方式選出最大的元素(復雜度 O(N)),而「堆排序」就把未排定的部分構建成一個「堆」,這樣就能以 O(logN) 的方式選出最大元素;
  • 堆是一種相當有意思的數據結構,它在很多語言里也被命名為「優先隊列」。它是建立在數組上的「樹」結構,類似的數據結構還有「并查集」「線段樹」等。「優先隊列」是一種特殊的隊列,按照優先級順序出隊,從這一點上說,與「普通隊列」無差別。「優先隊列」可以用數組實現,也可以用有序數組實現,但只要是線性結構,復雜度就會高,因此,「樹」結構就有優勢,「優先隊列」的最好實現就是「堆」。

1.5 選擇排序

  • 時間復雜度:O(N^2) 空間復雜度:O(1)
  • 優點:交換次數最少。
  • 算法思想 1:貪心算法:每一次決策只看當前,當前最優,則全局最優。注意:這種思想不是任何時候都適用。
  • 算法思想 2:減治思想:外層循環每一次都能排定一個元素,問題的規模逐漸減少,直到全部解決,即「大而化小,小而化了」。運用「減治思想」很典型的算法就是大名鼎鼎的「二分查找」。

1.6 希爾排序

  • 時間復雜度:尚不明確

1.7 冒泡排序(了解)

  • 時間復雜度:O(N^2) 空間復雜度:O(1)
  • 優點:「冒泡排序」有個特點:在遍歷的過程中,提前檢測到數組是有序的,從而結束排序,而不像「選擇排序」那樣,即使輸入數據是有序的,「選擇排序」依然需要「傻乎乎」地走完所有的流程。

1.8 計數排序 1.9 選擇排序 1.10 桶排序

  • 時間復雜度:O(N) 優化
  • 特點: 一個數該放在哪里,是由這個數本身的大小決定的,它不需要經過比較。也可以認為是哈希的思想:由數值映射地址。
  • 特點: 一定需要額外的空間才能完成排序任務
  • 缺點: 適用場景不多,主要是因為使用這三種排序一定要保證輸入數組的每個元素都在一個合理的范圍內
  • 優點:可以實現成穩定排序,無需穩定化
2.代碼規范

2.1 循環不變量 (快排)
-保持「循環不變量」,即定義的變量在循環開始前、循環過程中、循環結束以后,都保持不變的性質,這個性質是人為根據問題特點定義的。
循環不變量」是證明算法有效性的基礎,更是寫對代碼的保證,遵守循環不變量,是不是該寫等于號,先交換還是先 ++ ,就會特別清楚,絕對不會寫錯,將遵守的「循環不變量」作為注釋寫在代碼中。
2.2 《算法 4》代碼風格不推薦

  • 代碼是寫給人看的,應該盡量避免代碼個人風格化,采用統一規范的寫法,保證易讀性,可擴展性。


    算法4

    2.3 變量名稱優化

  • lt 是 less than 的縮寫,表示(嚴格)小于;
  • gt 是 greater than 的縮寫,表示(嚴格)大于;
  • le 是 less than or equal 的縮寫,表示小于等于(本代碼沒有用到);
  • ge 是 greater than or equal 的縮寫,表示大于等于(本代碼沒有用到)。
3.排序入門成功 筆試面試不慌
  public int[] sortArray(int[] nums) {
        Arrays.sort(nums);
        return nums;
    }
安利網站:

算法動態演示

參考鏈接:https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容