快速排序、歸併排序、數組切分(Partition Array)、Quick Select算法總結

1. 快速排序

思路

  • 先整體有序,後局部有序
  • 先將整個大數組以某個pivot劃分成左右有序的狀態,然後縮小區間,重複這個過程
  • 具體的實現細節均在代碼中

代碼

class LeetCode912 {
    public List<Integer> sortArray(int[] nums) {
        List<Integer> result = new ArrayList<>();
        if (nums == null || nums.length == 0){
            return result;
        }
        int start = 0;
        int end = nums.length - 1;
        quickSort(nums,start,end);

        for (int num : nums) {
            result.add(num);
        }
        return result;
    }
    //0. 這是個遞歸函數,它的定義是 在閉區間[start,end]中,把所有小於 nums[(start+end)/2]的數都放在它左邊
    //   把所有大於這個的數都放在它右邊。然後不斷的去縮小這個區間
    //   也就是先整體有序 後局部有序
    private void quickSort(int[] nums, int start, int end) {
        //1. 遞歸出口
        if (start >= end){
            return;
        }
        //1.1 緩存區間端點 後面縮小區間的時候還需要這兩個端點的信息
        int left = start;
        int right = end;

        //2. get value not index
        int pivot = nums[(end + start)/2];
        //2.1 接下來的操作就是以pivot爲錨點 切分數組
        //    注意這裏的細節 left <= right,爲何要等於呢?
        //    就是爲了跳出while後,能達到這樣的狀態:start......right,left,.....end
        while (left <= right){
            //2.2 找到第一個應該在 pivot右邊的數
            while (left <= right && nums[left] < pivot ){
                left++;
            }
            //2.3 找到第一個應該在 pivot左邊的數
            while (left <= right && nums[right] > pivot){
                right--;
            }
            //2.4 找到後,完成交換
            if (left <= right){
                int temp = nums[left];
                nums[left] = nums[right];
                nums[right] = temp;
                left++;
                right--;
            }
        }
        //3. 跳出while,完成了本次區間內的切分,接下來縮小區間
        //3.1 此時left和right的狀態一定是  start......right,left,.....end
        quickSort(nums, start, right);
        quickSort(nums, left, end);
    }
}

2. 歸併排序

思路

  • 先局部有序,後整體有序
  • 先一直把數組二分下去,直到找到最小有序的部分,那麼就開始向上合併兩個有序的部分
  • 顯然最開始最小的有序部分就是單個數字本身嘛
  • 既然需要合併兩個有序的數組,那麼肯定就額外需要另一個數組來騰挪,這也是歸併相對於快排劣勢的一點,需要額外的開闢一個數組的額外空間
  • 細節都在代碼裏

代碼

class LeetCode912 {
    public List<Integer> sortArray(int[] nums) {
        List<Integer> result = new ArrayList<>();
        if (nums == null || nums.length == 0){
            return result;
        }
        int start = 0;
        int end = nums.length - 1;
        //用於合併兩個有序數組
        int[] temp = new int[nums.length];
        mergeSort(nums,start,end,temp);
        for (int num : nums) {
            result.add(num);
        }
        return result;
    }
    //0. 這是一個遞歸函數,其定義是:將閉區間[start,end]劃分爲兩半
    private void mergeSort(int[] nums, int start, int end, int[] temp) {
        //3.出
        //3.1 當切分到只剩一個元素的時候,就無需再繼續往下了,此時已經是最小的有序區間
        if (start >= end){
            return;
        }
        //1.分
        //  就是分治法的味道,先分下去
        int mid = (end - start)/2 + start;
        mergeSort(nums, start, mid, temp);
        mergeSort(nums, mid+1, end, temp);
        //2.合
        //  從上述遞歸中出來後,可以認爲 區間:[start,mid]和[mid+1,end]已經是有序了
        //  那麼接下來只需要合併這兩個有序區間即可
        merge(nums,start,mid,end,temp);
    }
    //此函數可認爲是合併兩個有序數組
    private void merge(int[] nums, int start, int mid, int end, int[] temp) {
        //1. 兩有序數組的起始點
        int left = start;
        int right = mid +1;
        //2. 有序數組的index
        int index = 0;
        //3. 這裏都是index,所以可以取到
        while (left <= mid && right <= end){

            if (nums[left] < nums[right]){
                temp[index++] = nums[left++];
            }else {
                temp[index++] = nums[right++];
            }
        }
        //3.1 double check
        while (left <= mid){
            temp[index++] = nums[left++];
        }
        while (right <= end){
            temp[index++] = nums[right++];
        }

        //4. 現在數組nums中[start,end]都是有序的,但是呢,這一部分的值還暫存在temp的[0,index]區間內
        //4.1 現在要把這個有序的部分賦值給nums的[start,end]部分
        //4.2 要注意這裏index不能取等,因爲你想,最後一個元素賦值給temp後,index完成了一次自加操作
        for (int i = 0; i < index; i++) {
            nums[start++] = temp[i];
        }
    }
}

3. 數組切分問題(Partition Array)

3.1 LintCode-31. Partition Array

題意

  • 給定數組和一個數k,要求把數組中<k的數放在左邊,>=k的數放在右邊,返回第一個大於等於k的數的索引

思路

  • 這不就是快排中每一次劃分的算法嘛
  • 只要快排理解了,這一道題就很簡單了

代碼

 public int partitionArray(int[] nums, int k) {
        // write your code here

        if (nums == null || nums.length == 0){
            return 0;
        }

        int left = 0;
        int right = nums.length - 1;
        while (left <= right){

            while (left <= right && nums[left] < k){
                left++;
            }
            while (left <= right && nums[right] >= k){
                right--;
            }

            if (left <= right){
                int temp = nums[left];
                nums[left] = nums[right];
                nums[right] = temp;
            }
        }
        // start ... right left ... end
        return left;
    }

3.2 LintCode373. Partition Array by Odd and Even

題意

  • 給定數組
  • 將數組中的奇數放在前面(左邊)
  • 將數組中的偶數放在後面(右邊)

思路

  • 和上題完全一樣嘛,只是條件變了

代碼

 public void partitionArray(int[] nums) {

        if (nums == null || nums.length == 0){
            return;
        }

        int left = 0;
        int right = nums.length - 1;
        while (left <= right){

            //0. 找到第一個應該在 右側的偶數
            while (left <= right && nums[left] %2 == 1 ){
                left++;
            }
            //1. 找到第一個應該在 左側的奇數
            while (left <= right && nums[right] % 2 == 0){
                right--;
            }
            //2.交換
            if (left <= right){
                int temp = nums[left];
                nums[left] = nums[right];
                nums[right] = temp;
                left++;
                right--;
            }
        }

    }

3.3 LintCode49.Sort Letters by Case

題意

  • 給定字符數組
  • 把小寫字母放在前面
  • 把大寫字母放在後面

思路

  • 和上題完全一致,只是判斷條件變成了字母是否爲大小寫

代碼

 public void sortLetters(char[] chars) {
        // write your code here

        if (chars == null || chars.length == 0){
            return;
        }
        int left = 0;
        int right = chars.length - 1;
        while (left <= right){
            //0. 找到第一個應該 在右側的大寫字母
            while (left <= right && Character.isLowerCase(chars[left])){
                left++;
            }
            //1. 找到第一個應該 在左側的小寫字母
            while (left <= right && Character.isUpperCase(chars[right])){
                right--;
            }
            if (left <= right){
                char temp = chars[left];
                chars[left] = chars[right];
                chars[right] = temp;
                left++;
                right--;
            }
        }

    }

3.4 LintCode144-Interleaving Positive and Negative Numbers

題意

  • 給定數組
  • 要求將數組劃分爲正負相間的樣式

思路

  • 關鍵在於處理正負數數量的影響
  • 1.首先把所有的負數放在左邊 正數放在右邊
  • 2.然後統計數量
  • 3.數量多的代表第一個數和最後一個數都是它
  • 4.然後按照步長爲2前後交換即可

代碼

 public void rerange(int[] A) {
        // write your code here
        if (A == null || A.length == 0){
            return;
        }
        int left = 0;
        int right = A.length - 1;
        //0. 先把所有的負數放在左側 正數放在右側
        while (left <= right){
            while (left <= right && A[left] < 0){
                left++;
            }
            while (left <= right && A[right] > 0){
                right--;
            }
            if (left <= right){
                int t = A[left];
                A[left] = A[right];
                A[right] = t;
                left++;
                right--;
            }
        }

        //1. 統計正負數的數量
        int posNum = 0;
        int negNum = 0;
        for (int num : A) {

            if (num > 0){
                posNum++;
            }
            if (num < 0){
                negNum++;
            }
        }
        //2. 根據正負數數量來決定完成劃分後誰位於首位和尾位
        //2.1 數量多的那一方 佔據首尾
        //2.2 left和right代表交換的起始位置
        if (posNum > negNum){
            //2.3 那麼第一個數和最後一個數都是正數
            left = 0;
            right = A.length - 2;
        }else if (posNum < negNum){
            //2.4 那第一個數和最後一個數都是負數
            left = 1;
            right = A.length -1;
        }else {
            //2.5 一樣多,那就按照負正負正的次序進行排列即可
            left = 0;
            right = A.length - 1;
        }
        //3. 隨後進行交換,注意步長爲2
        while (left <= right){
            int t = A[left];
            A[left] = A[right];
            A[right] = t;
            left+=2;
            right-=2;
        }
    }

4. QuickSelect問題

4.1 LeetCode215. Kth Largest Element in an Array

題意

  • 給定無序數組和一個數k,要求找到改數組中第k大的數

思路

  • 利用數組劃分和快速排序思想
  • 不斷的縮小第k大可能存在的區間
  • 具體細節都在代碼註釋中

代碼

class LeetCode215 {
    public int findKthLargest(int[] nums, int k) {
        if (nums == null || nums.length == 0){
            return -1;
        }
        
        return quickSelect(nums,0,nums.length - 1,k);
        
    }
    //0. 思想就在於通過切分數組 不斷的壓縮 k-th可能在的區間
    private int quickSelect(int[] nums, int start, int end, int k) {
        //2.遞歸出口
        //2.1 爲何相等的時候就要退出了呢,因爲此時區間中就只有一個數了嘛,那這個數肯定就是要找的
        if (start == end){
            return nums[start];
        }


        int left = start;
        int right = end;
        int pivot = nums[(left + right)/2];
        //0. 以pivot爲界,把數組元素切分爲兩部分,左側大於它,右側小於它
        //0.1 注意這裏和快排不同,因爲這裏是求第k大
        while (left <= right){

            //0.2 找到第一個應該在右邊的數
            while (left <= right && nums[left] > pivot){
                left++;
            }
            while (left <= right && nums[right] < pivot){
                right--;
            }
            //0.3 交換
            if (left <= right){
                int t = nums[left];
                nums[left] = nums[right];
                nums[right] = t;
                left++;
                right--;
            }
        }
        //1. 完成劃分,現在left和right的位置關係有兩種可能
        //1.1 start....right,left....end
        //1.2 start.....right,A,left...end 這種隔了一個的狀態是由於上面while中的if導致的
        //1.3 當if中二者 left = right後,滿足添加,執行if內的代碼,然後會 left++,right--,就會導致這種剛好錯開一個的情況
        //1.4 我們要找第k大,那麼就需要判斷 第k大 可能會落在哪個區間
        //1.5 現在一個好消息在於區間[start,right]中的數都大於區間[left,end]
        //1.6 那麼需要判斷k是否落在這兩個區間內,第k大是基於1的,而上述的left這些都是索引,基於0,所以需要k-1
        if (start + k - 1 <= right){
            //1.7 第k大在左半區間,所以扔掉右邊的一半
            return quickSelect(nums, start, right, k);
        }
        if (start + k - 1 >= left){
            //1.8 第k大在右半區間,所以扔掉左邊的一半
            //1.8.1 左邊一半的數量是多少呢
            return quickSelect(nums,left,end,k - (left - start));
        }
        //1.9 如果落在中間,即情況1.2
        return nums[right+1];
    }
}

4.2 LintCode80. Median

題意

  • 給定未排序數組,求其中位數

思路

  • 只要分析清楚中位數是第幾大的數
  • 那麼問題就轉換爲了在無序數組中求第k大的數問題
  • 分奇數和偶數不難分析出中位數都是第n/2 - 1大的數

代碼

public class Solution {
  public int median(int[] nums) {

        //0. 簡單分析發現,不管數字個數是奇數還是偶數,其中位數都是第(n/2) + 1大的數
        int k = nums.length/2 + 1;
        //1. 那麼剩餘的工作就是在一個未排序的數組中找第k大的數
        return findKthLargest(nums, k);
    }

    public int findKthLargest(int[] nums, int k) {
        if (nums == null || nums.length == 0){
            return -1;
        }

        return quickSelect(nums,0,nums.length - 1,k);

    }
    //0. 思想就在於通過切分數組 不斷的壓縮 k-th可能在的區間
    private int quickSelect(int[] nums, int start, int end, int k) {
        //2.遞歸出口
        //2.1 爲何相等的時候就要退出了呢,因爲此時區間中就只有一個數了嘛,那這個數肯定就是要找的
        if (start == end){
            return nums[start];
        }


        int left = start;
        int right = end;
        int pivot = nums[(left + right)/2];
        //0. 以pivot爲界,把數組元素切分爲兩部分,左側大於它,右側小於它
        //0.1 注意這裏和快排不同,因爲這裏是求第k大
        while (left <= right){

            //0.2 找到第一個應該在右邊的數
            while (left <= right && nums[left] > pivot){
                left++;
            }
            while (left <= right && nums[right] < pivot){
                right--;
            }
            //0.3 交換
            if (left <= right){
                int t = nums[left];
                nums[left] = nums[right];
                nums[right] = t;
                left++;
                right--;
            }
        }
        //1. 完成劃分,現在left和right的位置關係有兩種可能
        //1.1 start....right,left....end
        //1.2 start.....right,A,left...end 這種隔了一個的狀態是由於上面while中的if導致的
        //1.3 當if中二者 left = right後,滿足添加,執行if內的代碼,然後會 left++,right--,就會導致這種剛好錯開一個的情況
        //1.4 我們要找第k大,那麼就需要判斷 第k大 可能會落在哪個區間
        //1.5 現在一個好消息在於區間[start,right]中的數都大於區間[left,end]
        //1.6 那麼需要判斷k是否落在這兩個區間內,第k大是基於1的,而上述的left這些都是索引,基於0,所以需要k-1
        if (start + k - 1 <= right){
            //1.7 第k大在左半區間,所以扔掉右邊的一半
            return quickSelect(nums, start, right, k);
        }
        if (start + k - 1 >= left){
            //1.8 第k大在右半區間,所以扔掉左邊的一半
            //1.8.1 左邊一半的數量是多少呢
            return quickSelect(nums,left,end,k - (left - start));
        }
        //1.9 如果落在中間,即情況1.2
        return nums[right+1];
    }
}
發佈了96 篇原創文章 · 獲贊 26 · 訪問量 6252
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章