LeetCode912排序数组(快速排序机及其优化详解)

LeetCode912排序数组(快速排序及其优化详解)

Abstract

我首先试图使用快速排序算法解答此题,无奈超过时间限制。超出时间限制的两个测试用例是:1.数组中的所有元素均相同 2.数组已经是被排好序的升序数组。针对第一个测试用例,可以使用三路指针法(即对和主元相等的元素进行单列一块)。对第二个测试用例,可以使用随机选主元的方法来实现(使用c++的rand()函数 %待排序数组长度 + 待排序数组起始下标)。在本文中,我首先介绍了基本的快速排序方法,然后介绍了两种优化思路。最后,我同时使用了两种优化思路,终于通过了Leetcode912.

基本快速排序

与归并排序一样,快速排序也使用了分治思想。下面是对一个典型的子数组A[p…r]进行快速排序的三步分治过程。

分解:数组A[p…r]被划分为两个(可能为空)的子数组A[p…q-1]和A[q+1…r],使得A[p…q-1]中的每一个元素都小于等于A[q],而A[q]也小于等于A[q+1…r]中的每个元素。其中,计算下标q也是划分过程的一部分。

**解决:**通过递归调用快速排序,对子数组A[p…q-1]和A[q+1…r]进行排序。

合并: 因为子数组都是原址排序的,所以不需要合并操作:数组A[p…r]已经有序。

快速排序的伪代码

QuickSort(A, p,r)

​ if p<r

​ q = PARTITION(A, p,r)

​ QuickSort(A, p, q-1)

​ QuickSort(A, q+1, r)

为了排序一个数组A的全部元素,初始调用是QUICKSORT(A, 1, A.length)

数组的划分

算法的关键部分是PARTITION过程,它实现了对子数组A[p…r]的原址重排。

PARTITION(A, p,r)

​ x = A[r]

​ i = p-1

​ for j = p to r-1

​ if(A[j]<=x)

​ i++;

​ swap(A[j], A[i])

​ swap(A[i+1], A[r])

​ return i+1;

掌握该算法的关键点掌握哪些量是变化的,哪些量是不变化的。变化的是两个指针i, j, 不变的是p和r. 我们只需要知道在扫描的过程中,如何变i和j即可。

我们需要保证的是:

  • A[p…i]是<=x
  • A[i+1…j-1]是>=x.
  • A[j…r-1]是未知的
  • A[r]是等于x的。

如果A[j]<=x, 则需要将该元素放到小于等于x的部分去,首先i++, 然后将A[j]和A[i]进行交换,然后j++;

如果A[j]>x, 则只需要j++即可。

当我们扫描到A[r]时,就需要将该分界元素放到我们为分界元素所设定的位置上,即i+1的位置。所以将A[r]和A[i+1]交换,然后返回i+1;

快速排序思路总结以及优化

  • 基本思路:快速排序每一次都排定一个元素,然后递归地排它左边的部分和右边的部分,依次进行下去,直到数组有序。

  • 如图所示:这张图很好地体现了快速排序的本质其实是一个递归树,

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hOIWXXyb-1687253345354)(https://pic.leetcode.cn/1678475287-xpnAxX-%E5%9F%BA%E7%A1%80%E5%BF%AB%E6%8E%92%20-%20%E5%89%AF%E6%9C%AC.jpg)]

该划分存在的问题

1.如果该数组所有元素都相同,且元素数量特别多,则很可能会存在超时的情况。例如[2,2,2,2,2], 则调用完之后会有[2,2,2,2],然后是[2,2,2], 然后是[2,2],最后是[2]. 则时间复杂度为 O ( ∑ i = 1 n ) = O ( n 2 ) O(\sum_{i=1}^n) = O(n^2) O(i=1n)=O(n2). 此时基本排序的树为仅有一边的树。

2.如果数组元素基本已经排序好, 例如,1到50000的一个已经升序排列好的数组,对于这个数组,基本快排的划分为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1DtPzVDq-1687253345356)(2023-6-19快速排序详解/1.jpg)]

优化思路

对于这种树形结构,我们的优化可以从两个方面入手,一是尽可能使得树的两端平衡,二是尽可能减少树的高度。

针对渐进有序数组

  • 随机取得pivot, 树的两端的会变得更加平衡。

针对相同元素较多的数组

三路快排

  • 三路快排(三指针法),把等于pivot元素的所有元素放在分割区间的中间,也就是说我们每次递归确定的是这个元素以及和它相等的元素的位置,大量元素相等的情况下,递归区间大大减少。
  • 数组划分:
    • A[p…i] < x, A[i+1…j-1]==x, A[j…k-1] 未知, A[k…r-1]>x. A[r] == x.

Code

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        srand((unsigned)time(NULL));
        int size_nums = nums.size();
        quicksort(nums, 0, size_nums-1);
        return nums;
    }

    vector<int> partition(vector<int>& nums, int p, int r){
        int rand_index = rand() % (r-p+1)+p;
        int x = nums[rand_index];
        swap(nums[rand_index], nums[r]);
        int i = p -1;
        int j = p;
        int k = r;
        //循环不变量
        // nums[p..i] < x
        // nums[i+1..j-1] equals x
        // nums[j..k-1] unknown
        // nums[k..r-1] >x
        // nums[r] equals x
        // 循环不变量在第一轮迭代之前是成立的。
        while(j<=k-1){
            if(nums[j]<x){
                i++;
                swap(nums[j], nums[i]);
                j++;
            }
            else if(nums[j] == x){
                j++;
            }
            else{
                k--;
                swap(nums[j], nums[k]);
            }
        }

        swap(nums[k], nums[r]);
        vector<int> results;
        results.push_back(i);
        results.push_back(k);
        return results;
    }

    void quicksort(vector<int>& nums, int p, int r){
        if(p>=r){
            return;
        }

        if(p<r){
            //随机选取法
            vector<int> partitions = partition(nums, p, r);
            quicksort(nums, p, partitions[0]);
            quicksort(nums, partitions[1], r);
        }
    }
};

基本快速排序的另一种代码实现

快速排序有两个核心点,分别为“哨兵划分”和“递归”。

哨兵划分操作: 以数组某个元素(一般选取首元素)为基准数,将所有小于基准数的元素移动至其左边,大于基准数的元素移动至其右边。

通过一轮哨兵划分,可以将数组排序问题拆分为两个较短数组的排序问题(本文称之为左(右)子数组)。

递归: 对左子数组和右子数组递归执行哨兵划分,直至子数组长度为1时终止递归,即可完成对整个数组的排序。

示例代码

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        quickSort(strs, 0, nums.size() - 1);
        return nums;
    }
private:
    void quickSort(vector<string>& strs, int l, int r) {
        if(l >= r) return;
        int i = l, j = r;
        while(i < j) {
            while(nums[j] >= nums[l] && i < j) j--;
            while(nums[i] <= nums[l] && i < j) i++;
            swap(nums[i], nums[j]);
        }
        swap(nums[i], nums[l]);
        quickSort(nums, l, i - 1);
        quickSort(nums, i + 1, r);
    }
};

参考文献

[1] https://leetcode.cn/problems/sort-an-array/solution/kuai-su-pai-xu-you-hua-zhen-dui-duo-zhon-ryq4/

[2] 快速排序的第二种代码实现 https://leetcode.cn/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof/solution/mian-shi-ti-45-ba-shu-zu-pai-cheng-zui-xiao-de-s-4/

猜你喜欢

转载自blog.csdn.net/ChenglinBen/article/details/131311964