一道排序題引發的思考

Veritas_des_Liberty 發表於 2021-05-03

題目:912. 排序陣列

給你一個整數陣列 nums,請你將該陣列升序排列。

 

示例 1:

輸入:nums = [5,2,3,1]
輸出:[1,2,3,5]
示例 2:

輸入:nums = [5,1,1,2,0,0]
輸出:[0,0,1,1,2,5]
 

提示:

1 <= nums.length <= 50000
-50000 <= nums[i] <= 50000

 

思考一:樸素快速排序

  之所以叫做樸素快速排序是因為在選取樞軸量的時候就是選取最左邊的那個元素來當做樞軸量,但是這樣有一個很大的問題就是如果所給的排序序列都已經有序的話,這種情況下快速排序的時間複雜度會退化到O(n^2),最後提交的時候也會有一組資料顯示超時。

Code:

class Solution {
public:

    void quickSort(vector<int>& nums, int l, int r) {
        if (l >= r) return;
        int pivot = partion(nums, l, r);
        quickSort(nums, l, pivot - 1);
        quickSort(nums, pivot + 1, r);
    }

    int partion(vector<int>& nums, int l, int r) {
        int temp = nums[l];
        while (l < r) {
            while (l < r && nums[r] >= temp) r--;
            nums[l] = nums[r];
            while (l < r && nums[l] <= temp) l++;
            nums[r] = nums[l];
        }
        nums[l] = temp;
        return l;
    }

    vector<int> sortArray(vector<int>& nums) {
        int len = nums.size();
        quickSort(nums, 0, len - 1);
        return nums;
    }
};

結果:

一道排序題引發的思考

 

 

思路二:歸併排序

  既然樸素快速排序在特定的條件下會退化到O(n^2),如果選擇歸併排序的話就不會存在時間複雜度退化到O(n^2)的情況,平均時間複雜度為O(nlogn),而且歸併排序也是一種穩定的排序演算法。但是,歸併排序存在的缺點是需要一個額外的儲存空間因此空間複雜的變為O(n)。而且平均情況下歸併排序的常量因子k(O(knlogn))要比快速排序的常量因子大。

Code:

class Solution {
public:

    vector<int> dummy;

    void mergeSort(vector<int>& nums, int l, int r) {
        if (l >= r) return;
        int m = (l + r) / 2;
        mergeSort(nums, l, m);
        mergeSort(nums, m+1, r);
        merge(nums, l, m, m+1, r);
    }

    void merge(vector<int>& nums, int l1, int r1, int l2, int r2) {
        int start = l1;
        int end = r2;
        int index = l1;
        while(l1 <= r1 && l2 <= r2) {
            if (nums[l1] <= nums[l2]) {
                dummy[index++] = nums[l1++];
            } else {
                dummy[index++] = nums[l2++];
            }
        }
        while (l1 <= r1) {
            dummy[index++] = nums[l1++];
        }
        while (l2 <= r2) {
            dummy[index++] = nums[l2++];
        }
        for (int i = start; i <= end; ++i) {
            nums[i] = dummy[i];
        }
    }

    vector<int> sortArray(vector<int>& nums) {
        int len = nums.size();
        dummy.resize(len);
        mergeSort(nums, 0, len - 1);
        return nums;
    }
};

結果:

一道排序題引發的思考

 

 思路三:三者取其中快速排序

  在《資料結構》(嚴蔚敏、吳偉民)這本書中介紹了這種方法,具體的做法就是在選取樞軸量的時候不是選取第一個元素作為樞軸量,而是在nums[l], nums[r], nums[mid]中選取一箇中間值作為樞軸量,這樣的話,如果原來的序列已經有序的話可以減少交換的次數(快速排序是一種基於交換的排序演算法,不穩定),從而減少在最壞情況下的時間複雜度,但是仍然不能夠做到在對已經有序的序列進行排序時做到O(n)。

Code:

 

class Solution {
public:
    int findMid(vector<int>& nums, int l, int r) {
        int m = (l + r) / 2;
        int x1 = nums[l];
        int x2 = nums[m];
        int x3 = nums[r];
        int t;
        if (x1 > x2) {
            t = x1;
            x1 = x2;
            x2 = t;
        }
        if (x1 > x3) {
            t = x1;
            x1 = x3;
            x3 = t;
        }
        if (x2 > x3) {
            t = x2;
            x2 = x3;
            x3 = t;
        }
        if (nums[l] == x2) return l;
        else if (nums[m] == x2) return m;
        else return r;
    }

    void quickSort(vector<int>& nums, int l, int r) {
        if (l >= r) return;
        int pivot = partion(nums, l, r);
        quickSort(nums, l, pivot - 1);
        quickSort(nums, pivot + 1, r);
    }

    int partion(vector<int>& nums, int l, int r) {
        int index = findMid(nums, l, r);
        int temp = nums[index];
        nums[index] = nums[l];
        while (l < r) {
            while (l < r && nums[r] >= temp) r--;
            nums[l] = nums[r];
            while (l < r && nums[l] <= temp) l++;
            nums[r] = nums[l];
        }
        nums[l] = temp;
        return l;
    }

    vector<int> sortArray(vector<int>& nums) {
        int len = nums.size();
        quickSort(nums, 0, len - 1);
        return nums;
    }
};

結果:

一道排序題引發的思考

從結果中我們可以看出,改進後的快速排序可以通過所有的測試用例,並且比歸併排序所用的空間要少。

 

是否還可以對快速排序繼續進行優化?

 

思路四:交換逆序對+快速排序+氣泡排序

  在指標r--和l++的過程中,如果相鄰兩元素是逆序對的話,那麼將逆序對進行交換,這樣做可以減少最後氣泡排序交換的次數。當要排序的數字數量很少的時候,快速排序並不能體現出它的優勢,這時選用氣泡排序或許是一個更好的選擇,所以採用兩者相結合的方法,從而提高排序的效率。

Code:

class Solution {
public:
    int findMid(vector<int>& nums, int l, int r) {
        int m = (l + r) / 2;
        int x1 = nums[l];
        int x2 = nums[m];
        int x3 = nums[r];
        int t;
        if (x1 > x2) {
            t = x1;
            x1 = x2;
            x2 = t;
        }
        if (x1 > x3) {
            t = x1;
            x1 = x3;
            x3 = t;
        }
        if (x2 > x3) {
            t = x2;
            x2 = x3;
            x3 = t;
        }
        if (nums[l] == x2) return l;
        else if (nums[m] == x2) return m;
        else return r;
    }

    void quickSort(vector<int>& nums, int l, int r) {
        if (l >= r) return;
        int pivot = partion(nums, l, r);
        if (r - l < 10) {
            for (int i = l; i <= r; ++i) {
                for (int j = r; j > l; --j) {
                    if (nums[j] < nums[j-1]) {
                        pivot = nums[j];
                        nums[j] = nums[j-1];
                        nums[j-1] = pivot;
                    }
                }
            }
        } else {
            quickSort(nums, l, pivot - 1);
            quickSort(nums, pivot + 1, r);
        }
    }

    int partion(vector<int>& nums, int l, int r) {
        int index = findMid(nums, l, r);
        int temp = nums[index];
        nums[index] = nums[l];
        while (l < r) {
            while (l < r && nums[r] >= temp) {
                r--;
                if (nums[r] > nums[r+1] && l < r) {  // 相鄰兩個元素處於逆序時進行交換
                    index = nums[r];
                    nums[r] = nums[r+1];
                    nums[r+1] = index;
                }
            }
            nums[l] = nums[r];
            while (l < r && nums[l] <= temp) {
                l++;
                if (nums[l] < nums[l-1] && l < r) {
                    index = nums[l];
                    nums[l] = nums[l-1];
                    nums[l-1] = index;
                }
            }
            nums[r] = nums[l];
        }
        nums[l] = temp;
        return l;
    }

    vector<int> sortArray(vector<int>& nums) {
        int len = nums.size();
        quickSort(nums, 0, len - 1);
        return nums;
    }
};

結果:

一道排序題引發的思考

 

 從執行的結果可以看出這樣做比之前只使用單一的快速排序(240ms)要節省大量的時間。

 

擴充套件:

  網上也有一些介紹三路劃分快速排序的方法,主要是針對待排資料中含有大量重複元素時,運用這種排序方法可能會更好,其基本的思路就是把等於樞軸量的元素放在中間區域,小於樞軸量的元素放在左邊,大於樞軸量的元素放在右邊。這樣一次快速排序結束後中間部分的元素就確定了最終的位置,之後再遞迴的對左邊和右邊的元素進行求解就好了。這種演算法主要適用於待排資料中含有大量重複元素的情況,比如在對一個學校的學生成績進行排名時,使用這種排序方法會更加的高效。

  另外,針對快速排序不穩定的性質,有的論文也給出了一種穩定的快速排序方法,具體的做法就是使用一塊輔助空間來完成穩定的性質,如果只是想要使用穩定的效能的話直接使用歸併排序就好了。