【算法专题】快速排序

目录

1. 颜色分类

2. 排序数组

3. 数组中的第k个最大元素

4. 库存管理III


1. 颜色分类

75. 颜色分类 - 力扣(LeetCode)

        依据题意,我们需要把只包含0、1、2的数组划分为三个部分,事实上,在我们前面学习过的【算法专题】双指针算法-CSDN博客中,有一道题叫做移动零,题目要求是把0移动到数组的最后端,于是我们通过两个指针:一个用于遍历数组、另一个用于将数组划分为零区域和非零区域。类似的,我们可以通过三个指针:一个用于遍历、两个用于将数组划分为三部分,即0、1、2。

        接下来我们要考虑的是遍历数组时遇到不同的情况如何处理,我们可以画一个划分过程中的简单示意图:

接下来分析i遍历时会碰到的三种情况:

然后就是将算法原理转化为代码,建议大家可以拿出纸笔,找个例子来模拟一遍,这样能对原理有更深的理解,也有利于接下来代码的编写。

class Solution {
public:
    void sortColors(vector<int>& nums) 
    {
        int n = nums.size();
        int i = 0, left = -1, right = n;
        while(i < right)
        {
            if(nums[i] == 0) swap(nums[i++], nums[++left]);
            else if(nums[i] == 1) i++;
            else swap(nums[i], nums[--right]);
        }    
    }
};

2. 排序数组

912. 排序数组 - 力扣(LeetCode)

        这道题目要求我们将给定数组进行升序排列,既然本文标题是快速排序,这里当然是用快排来解决啦!快排的原理是:选择一个基准元素,然后让数组中小于等于基准元素的元素都排在基准元素左侧,大于基准元素的元素都排在基准元素右侧,然后对左侧区间和右侧区间分别做同样的操作,体现了分而治之的思想。

        不过一般的快排可不能解决这道题,因为快排虽然在平均情况下挺高效的,但出现相同元素的个数会影响快排的稳定性。

如上图所示,在最极端的情况下,整个数组都是相同的元素,则每次排序只能确定一个元素的位置,此时快排的时间复杂读退化到了O(n^2)。而本题的用例正好就出现了许多相同元素的情况,所以常规快排是会超出时间限制的,我们需要优化过的快速排序。

        相信大家都发现了,普通快排没有单独考虑相同元素的情况,于是会被相同元素影响到效率,故而我们可以针对这一点进行优化。单独考虑相同元素的情况后,我们将数组划分为三个区域:小于基准元素、等于基准元素、大于基准元素。没错,实际上这里的划分方式和上一道颜色分类是一样一样的。接下来要做的就是确定基准元素,方法有很多,不过算法导论中证明了,随机选取基准元素的快速排序的效率是最高的,因此我们在这里使用随机基准元素。

        

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) 
    {
        srand(time(NULL)); // 种下随机数的种子,之后我们就可以通过rand()函数得到随机数了
        qsort(nums, 0, nums.size() - 1);
        return nums;
    }
    // 值得注意的是,本题数据量比较大,我们传数组的引用就不需要进行拷贝了,能大大减少耗时
    void qsort(vector<int>&nums, int l, int r) 
    {
        if(l >= r) return;
        int key = GetRandom(nums, l, r);
        int i = l, left = l - 1, right = r + 1;
        while (i < right)
        {
            if(nums[i] < key) swap(nums[++left], nums[i++]);
            else if(nums[i] == key) i++;
            else swap(nums[--right], nums[i]);
        }
        qsort(nums, l, left);
        qsort(nums, right, r);
    }
    int GetRandom(vector<int>& nums, int left, int right)
    {
        int index = rand();
        // 随机数取模区间大小就是偏移量,再加上left,就得到了随机元素的下标
        return nums[index % (right - left + 1) + left]; 
    }
};

3. 数组中的第k个最大元素

215. 数组中的第K个最大元素 - 力扣(LeetCode)

        正如大家所见,本题属于典型的TopK问题,大家可能第一时间就能想到直接将数组排序后返回第k个最大的元素或使用堆排序,但题目要求我们设计并实现时间复杂度为O(n)来解决,而使用库函数排序的时间复杂度是O(n*logn),使用堆排序的时间复杂度是O(n*logK),所以我们试着结合今天学的优化的快速排序来试着处理这个TopK问题。

        和前面两道题相同,我们随机选择基准元素,把数组划分为小于基准元素、等于基准元素、大于基准元素三个部分,分别命名为a、b、c,当c>=k时,说明第k个最大元素在c区域内,那么我们只需要对c区域再次进行排序即可;否则进一步判断b+c>=k时,说明第k个最大元素在b区域内,由于b区域元素全部都等于基准元素,则基准元素就是我们要找的第k个最大元素,直接返回即可;否则第k个最大元素只能是在a区域内了,同样我们还是只需要对a区域进行排序即可。

        相信通过阅读我前面描述的步骤,大家可以发现快速选择排序算法处理TopK问题时,只需要对数组的部分区域进行排序,时间复杂度是非常优秀的,可以逼近O(n),感兴趣的同学可以读读算法导论,上面有详尽的证明。

class Solution {
public:
    int qssort(vector<int> nums, int k, int l, int r)
    {
        if(l == r) return nums[l];
        int index = rand() % (r - l + 1) + l;
        int key = nums[index];
        int i = l, left = l - 1, right = r + 1;
        while(i < right)
        {
            if(nums[i] < key) swap(nums[++left], nums[i++]);
            else if(nums[i] == key) i++;
            else swap(nums[--right], nums[i]);
        }
        int b = right - left - 1, c = r - right + 1;
        if(c >= k) return qssort(nums, k, right, r);
        else if(b + c >= k) return key;
        else return qssort(nums, k - b - c, l, left);
    }
    int findKthLargest(vector<int>& nums, int k) 
    {       
        srand(time(NULL));
        return qssort(nums, k, 0, nums.size() - 1);       
    }
};

4. 库存管理III

LCR 159. 库存管理 III - 力扣(LeetCode)

        这道题目属于简单题,同样也是可以使用多种排序算法处理,但既然我们这篇文章讲的是优化快排,就用快速选择排序算法来做吧。

        因为本题仅要求我们返回最少余量,并不要求返回顺序,同样可以用快速选择排序的思路进行处理,随机选择基准元素,将数组划分为三部分:小于基准元素、等于基准元素、大于基准元素。三部分分别命名为a、b、c,当a>=cnt时,对a区域进行快速选择排序;否则进一步判断,当a+b>=cnt时,由于b区域全部都等于基准元素,不需排序,直接返回前k个元素即可;否则再进一步,a+b+c>=cnt时,ab区域我们肯定是要返回的,再在c区域中找出其中最小的前cnt-a-b个元素,所以对c区域单独进行排序即可。

class Solution {
public:

    vector<int> inventoryManagement(vector<int>& stock, int cnt) 
    {
        srand(time(NULL));
        qssort(stock, cnt, 0, stock.size() - 1);
        return {stock.begin(), stock.begin() + cnt};
    }
    void qssort(vector<int> &stock, int cnt, int l, int r)
    {
        if(l >= r) return;
        int index = rand() % (r - l + 1) + l;
        int key = stock[index];
        int i = l, left = l - 1, right = r + 1;
        while(i < right)
        {
            if(stock[i] < key) swap(stock[++left], stock[i++]);
            else if(stock[i] == key) i++;
            else swap(stock[--right], stock[i]);
        }
        int a = left - l + 1, b = right - left - 1;
        if(a >= cnt) qssort(stock, cnt, l, left);
        else if(a + b >= cnt) return;
        else qssort(stock, cnt - a - b, right, r);
    }
};

总结

        本篇文章带着大家从简单的区域划分开始,学习了优化的快速排序算法,并根据这个思想学习了快速选择排序算法,这是处理TopK问题的非常优秀的算法,时间复杂度可以逼近log(n),希望大家可以在做这几道题的过程中,可以思考快速选择排序的部分排序性所带来的优势,这正好非常适合用来处理TopK问题。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值