LeetCode刷題-04-排序

第四講 排序

4.1 巨經典的排序算法

1. 冒泡排序(很簡單)

  • 平均時間複雜度 O(n^2) ,空間複雜度 O(1),穩定java

  • 基本思想算法

    • 兩個數比較大小,較大的數下沉,較小的數冒起來。
  • 演示(圖片來自菜鳥教程)數組

    bubbleSort

  • 代碼函數

    /**
     * 冒泡排序
     * @param array 待排序的數組
     */
    public static void bubbleSort(int[] array) {
        for(int i=0; i<array.length-1; i++){
            for(int j=array.length-1; j>i; j--){
                if(array[j] < array[j-1]){
                    int temp = array[j];
                    array[j] = array[j-1];
                    array[j-1] = temp;
                }
            }
        }
    }

2. 選擇排序

  • 平均時間複雜度 O(n^2) ,空間複雜度 O(1),不穩定優化

  • 基本思想ui

    • 遍歷數組,依次找到第 i 小的元素,而後把它和第i 個元素交換位置
  • 演示設計

    selectionSort

  • 代碼3d

    /**
     * 選擇排序
     * @param array 待排序的數組
     */
    public static void selectSort(int[] array) {
        for(int i=0;i<array.length-1;i++){
            int minIndex = i;
            for(int j=i+1;j<array.length;j++){
                if(array[j]<array[minIndex]){
                    minIndex = j;
                }
            }
            if(minIndex != i){
                int temp = array[i];
                array[i] = array[minIndex];
                array[minIndex] = temp;
            }
        }
    }

3. 插入排序

  • 平均時間複雜度 O(n^2) ,空間複雜度 O(1),穩定code

  • 基本思想blog

    • 在要排序的一組數中,假定前 i 個數已經排好序了,那麼第 i+1 個數只須要插入到前面已經排好序的數組中便可
  • 演示(圖片來自菜鳥教程)

    insertionSort

  • 代碼

    /**
     * 插入排序
     * @param array 待排序的數組
     */
    public void insertSort(int[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            for (int j = i + 1; j > 0; j--) {
                if (array[j] < array[j-1]) {
                    int temp = array[j-1];
                    array[j-1] = array[j];
                    array[j] = temp;
                } else {
                    break;
                }
            }
        }
    }

4. 歸併排序

  • 平均時間複雜度 O(n log(n)) ,空間複雜度 O(n),穩定

  • 基本思想(分治)

    • 首先考慮如何合併兩個有序數組
      • 咱們只須要依次比較兩個數組中的第一個數便可,誰小放到新數組中
    • 歸併排序的主要思想就是歸併兩個有序數組。
      • 將原數組分紅二組 A,B。若是A,B都是有序的,歸併便可。
      • 可是A,B都不是有序的,那麼就繼續分。直到這個數組中只有1個數據的時候。
      • 這個時候能夠認爲他是有序的,而後依次向上合併
    • 總結:遞歸分解數組,而後再從小到大合併
  • 演示(圖片來自菜鳥教程)

    mergeSort

  • 代碼

    /**
     * 歸併排序
     * @param array 須要排序的數組
     * @return 排好序的數組
     */
    public int[] mergeSort(int[] array) {
        // 建立額外的空間
        int[] res = Arrays.copyOf(array, array.length);
        if (res.length < 2) {
            return res;
        }
        int mid = array.length / 2;
        int[] left = Arrays.copyOfRange(array, 0, mid);
        int[] right = Arrays.copyOfRange(array, mid, array.length);
        return merge(mergeSort(left), mergeSort(right));
    }
    
    /**
     * 歸併兩個有序數組
     * @param array1 有序數組1
     * @param array2 有序數組2
     * @return 歸併後的新數組
     */
    private int[] merge(int[] array1, int[] array2) {
        int[] res = new int[array1.length + array2.length];
        int p = 0;
        int i = 0;
        int j = 0;
        while (i < array1.length && j < array2.length) {
            if (array1[i] <= array2[j]) {
                res[p++] = array1[i++];
            } else {
                res[p++] = array2[j++];
            }
        }
        // 剩下了left
        while (i < array1.length) {
            res[p++] = array1[i++];
        }
        // 剩下的是right
        while (j < array2.length) {
            res[p++] = array2[j++];
        }
        return res;
    }

5. 快速排序(很經常使用)

  • 平均時間複雜度 O(n log(n)) ,空間複雜度 O(log n),不穩定

  • 算法思想(分治)

    • 選取一個key,對於比key大的,所有放到右邊,對於比key小的,所有放到key的左邊。這樣key的位置就能夠惟一肯定了。而後對key兩邊的區間重複操做就能夠肯定全部元素的位置了
  • 演示(圖片來自菜鳥教程)

    img

  • 具體操做

    1. i=0,j=n-1,key=array[i]
    2. j 向前移動,找到第一個比 key 小的元素,把這個元素放到 i
    3. i 向後移動,找到第一個比 key 大的元素,將它放到 j 的位置
    4. i 的地方放上 key
    5. 通過這麼一步操做,就可使得 [0,i-1] 中全部的元素都是比 i 小的,[j,n-1] 中的全部元素都是比 i 大 的,而後重複操做,直到 i==j 這樣就知足了 array[0,i-1] < array[i] < array[i+1,n-1] 。而後 i左右兩邊的區間重複操做。
  • 代碼

    /**
     * @param array 須要排序區間所在的數組
     * @param left 區間的起始下標
     * @param right 區間的結束下標
     */
    public void quickSort(int[] array, int left, int right) {
        if (left < right) {
            int i = left;
            int j = right;
            int key = array[left];
            while (i < j) {
                // 從j開始向左尋找到第一個比 key 小的數
                while (i < j && array[j] >= key) {
                    j--;
                }
                if (i < j ) {
                    array[i] = array[j];
                    i++;
                }
                // 從i開始向右尋找第一個大於等於 key 的數
                while (i < j && array[i] < key) {
                    i++;
                }
                if (i < j) {
                    array[j] = array[i];
                    j--;
                }
            }
            array[i] = key;
            quickSort(array, left, i-1);
            quickSort(array, i+1, right);
        }
    }

6. 堆排序

  • 平均時間複雜度 O(n+k) ,空間複雜度 O(k),穩定

  • 什麼是堆?

    • 堆是具備如下性質的徹底二叉樹:每一個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆;或者每一個結點的值都小於或等於其左右孩子結點的值,稱爲小頂堆。(注意是左右孩子而不是左右子樹)
  • 基本思想

    • 將待排序的數組構形成一個大頂堆。此時,堆頂的元素就是最大的,將堆頂元素和最後一個元素交換位置,而後從新構造大頂堆。
    • 咱們不須要想構造一顆二叉樹同樣去構造堆,咱們只須要按照如下規則進行對應便可:
      • 即數組下標爲 i 的元素,他的左右孩子的下標是 2i+12i+2
    • 如此一來,咱們就不須要額外的空間
  • 演示

    img

  • 代碼

    /**
     * 堆排序
     * @param array 待排序的數組
     */
    public void heapSort(int[] array) {
        // len表示的是未進行排序的長度
        int len = array.length;
        for (int i = 0; i < array.length; i++) {
            // 從最後一個非葉子節點開始調整,使其知足大頂堆的性質
            int last = len / 2 - 1;
            for (int j = last; j >= 0; j--) {
                int left = 2 * j + 1;
                int right = left + 1;
                if (array[left] > array[j]) {
                    swap(array, left, j);
                }
                if (right < len && array[right] > array[j]) {
                    swap(array, right, j);
                }
            }
            len--;
            // 將堆頂元素和放到正確的地方
            swap(array, 0, array.length - 1 - i);
        }
    }
    
    /**
     * 交換數組中的兩個元素
     * @param array 數組
     * @param i1 元素1
     * @param i2 元素2
     */
    private void swap(int[] array, int i1, int i2) {
        int temp =  array[i1];
        array[i1] = array[i2];
        array[i2] = temp;
    }

7. 計數排序

  • 平均時間複雜度 O(n+k) ,空間複雜度 O(k),穩定。其中,k是整數的範圍

  • 基本思想

    • 遍歷數組,得出最大值、最小值、差距三者中的兩個。
    • 申請一個新數組,數組長度爲差距。而後對原數組中的數進行遍歷,記錄出現的次數
    • 最後遍歷新數組,就能夠獲得排序後的結果
  • 演示

    countingSort

  • 代碼

    /**
     * 計數排序
     * @param array 待排序的數組
     */
    public void countingSort(int[] array) {
        int min = array[0];
        int max = array[0];
        // 找最大值和最小值
        for (int i : array) {
            min = Math.min(i, min);
            max = Math.max(i, max);
        }
    
        // 申請額外的空間,大小爲最值之間的範圍
        int[] temp = new int[max - min + 1];
    
        // 填充新數組
        for (int i : array) {
            temp[i - min]++;
        }
    
        // 遍歷新數組,而後填充原數組
        int index = 0;
        for (int i = 0; i < temp.length; i++) {
            if (temp[i] != 0) {
                Arrays.fill(array, index, index + temp[i], i + min);
                index += temp[i];
            }
        }
    }
  • 注意:計數排序對於必定範圍內的整數進行排序具備最高的效率,前提是整數的範圍不要太大

8. 桶排序

  • 平均時間複雜度 O(n) ,空間複雜度 O(n+k),穩定。其中k是桶的數量

  • 基本思想

    • 首先劃分若干個範圍相同的 「桶」 ,
    • 而後將待排序的數據扔到屬於他本身的桶中
    • 對桶中的數據進行排序
    • 依次輸出全部桶中的數據
  • 關鍵

    • 計數排序的優化,適合於對於大量有範圍的數據
    • 桶的劃分要儘可能保證元素應當能夠均勻分散到全部桶中,若是說全部元素都集中在一個桶中,那麼桶排序就會失效
  • 演示

    img

  • 代碼

    • 桶排序的關鍵在於桶的劃分和選取。須要根據實際使用場景按需進行設計,因此這裏不作展現。

4.2 快速選擇

215. 數組中的第K個最大元素(Medium)

  • 思路

    • 快速選擇算法是快速排序的一個變形
    • 從上文對於快速排序的講解中不難發現。對於一個 key 來講,在進行一次快速排序的過程當中,是能夠肯定出這個 key 所在有序數組中的位置。
    • 那麼咱們只須要關注這個位置是否是咱們須要的 K 便可。若是比 k 大,則在 key 的右邊再作一次快速排序便可;反之,則在左邊作一次快速排序
  • 時間複雜度 O(n),空間複雜度 O(1)

  • 代碼

    public int findKthLargest(int[] nums, int k) {
        int left = 0;
        int right = nums.length - 1;
        int target = nums.length - k;
        while (left < right) {
            int p = quickSort(nums, left, right);
            if (p < target) {
                left = p + 1;
            } else if (p > target) {
                right = p - 1;
            } else {
                return nums[p];
            }
        }
        return nums[left];
    }
    
    // 快速排序函數,返回key的下標
    public int quickSort(int[] array, int left, int right) {
        int i = left;
        int j = right;
        int key = array[left];
        while (i < j) {
            while (i < j && array[j] >= key) {
                j--;
            }
            if (i < j) {
                array[i] = array[j];
                i++;
            }
            while (i < j && array[i] < key) {
                i++;
            }
            if (i < j) {
                array[j] = array[i];
                j--;
            }
        }
        array[i] = key;
        return i;
    }
  • 注意點

    • k=1 時,函數可能不會在循環體中返回結果。此時,退出循環後left=5 ,因此要返回 nums[left]

4.3 桶排序

347. 前 K 個高頻元素(Medium)

  • 思路

    • 典型的桶排序的使用場景,來回顧一下桶排序
      • 桶排序就是把須要排序的元素放到屬於他的桶中,而後再對桶中的元素進行排序,最後把所用桶中的元素輸出就是排序以後的結果。重點是讓全部元素均勻的分佈到全部桶中
    • 若是咱們的桶中只放這個元素本身,那麼桶的大小就是這個元素的出現次數,而後根據桶的大小進行排序,就能夠很方便的獲得出現頻率前K個高頻元素
    • 重點:將元素和出現頻率進行對應
    • 時間 O(n log(n)), 空間 O(n)
  • 代碼

    public int[] topKFrequent(int[] nums, int k) {
        int[] res = new int[k];
        // 用哈希表作桶排序,每一個元素做爲key,出現次數做爲value
        Map<Integer, Integer> map = new HashMap<>();
        for (int num : nums) {
            map.put(num, map.getOrDefault(num, 0)+1);
        }
        int[][] bucket = new int[map.size()][2];
        int p = 0;
        // 利用數組將元素和出現次數進行對應
        for (Integer i : map.keySet()) {
            bucket[p][0] = i;
            bucket[p++][1] = map.get(i);
        }
        // 降序排序
        Arrays.sort(bucket, ((o1, o2) -> o2[1]-o1[1]));
        for (int i = 0; i < k; i++) {
            res[i] = bucket[i][0];
        }
        return res;
    }
  • 優化

    • 在獲得每一個元素的頻率以後,能夠再對頻率進行一次桶排序,這樣的話能夠把時間複雜度下降到 O(n)
  • 代碼

    // 再對頻率進行一次桶排序,這樣就能夠獲得前K個高頻的元素
    // 對於頻率最高的元素,放在最前面
    List<Integer>[] bucket = new List[maxFrequency + 1];
    for (int i : map.keySet()) {
        int f = maxFrequency - map.get(i);
        if (bucket[f] == null) {
            bucket[f] = new ArrayList<>();
        }
        bucket[f].add(i);
    }
    List<Integer> res = new ArrayList<>();
    int i = 0;
    while (k > 0) {
        List<Integer> list = bucket[i++];
        if (list != null) {
            res.addAll(list);
            k -= list.size();
        }
    }

4.4 基礎練習

451. 根據字符出現頻率排序(Medium)

  • 思路

    • 桶排序,每一個字母一個桶,最後降序輸出
  • 代碼

    public String frequencySort(String s) {
        Map<Character, Integer> map = new HashMap<>();
        // 對字母進行桶排序,獲得每一個字母出現的頻率
        int max = 0;
        for (int i = 0; i < s.length(); i++) {
            map.put(s.charAt(i), map.getOrDefault(s.charAt(i), 0) + 1);
            max = Math.max(max, map.get(s.charAt(i)));
        }
        // 再對頻率進行一次桶排序
        ArrayList<Character>[] bucket = new ArrayList[max + 1];
        for (char c : map.keySet()) {
            int f = map.get(c);
            if (bucket[f] == null) {
                bucket[f] = new ArrayList<>();
            }
            bucket[f].add(c);
        }
        int p = 0;
        char[] chars = s.toCharArray();
        for (int i = max; i >= 0; i--) {
            if (bucket[i] != null) {
                for (char c : bucket[i]) {
                    Arrays.fill(chars, p, p+i, c);
                    p += i;
                }
            }
        }
        return new String(chars);
    }

4.5 進階練習

75. 顏色分類(Medium)

  • 思路

    • 遍歷數組,對三種顏色出現的次數進行統計,而後填充數組
  • 代碼

    public void sortColors(int[] nums) {
        int[] f = new int[3];
        for (int num : nums) {
            f[num]++;
        }
        int p = 0;
        for (int i = 0; i < 3; i++) {
            Arrays.fill(nums, p, p + f[i], i);
            p += f[i];
        }
    }
相關文章
相關標籤/搜索