LeetCode 215 数组中第k大的数(快排的应用:快速选择)

题目描述
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
示例 2:

输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4
说明:

你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。

快速排序,在排序的过程中要有一个基准,以这个基准左边是小于它的数字,右边是大于它的所有数字,也就是 j - l +1 可以表示为小于该点的数字的个数。

  • 找到分界点 x ,x 可以取左端点位置的数,也可以取右端点位置的数还可以取中间位置的数,甚至可以随机选择
  • 左边所有数 l <=x , 右边所有数 r >=x,注意此时x不一定恰好等于数组中间的元素
  • 递归排序 l ,递归排序r
  • 快速选择基准如何选择,基准选择方式参考链接1, 基准选择方法参考链接2
    • 总体的基准选择策略是:
      方法1 固定基准元
      如果输入序列是随机的,处理时间是可以接受的。如果数组已经有序时,此时的分割就是一个非常不好的分割。因为每次划分只能使待排序序列减一,此时为最坏情况,快速排序沦为冒泡排序,时间复杂度为Θ(n2)。而且,输入的数据是有序或部分有序的情况是相当常见的。因此,使用第一个元素作为基准元是非常糟糕的,应该立即放弃这种想法。
      方法2 随机基准元
      这是一种相对安全的策略。由于基准元的位置是随机的,那么产生的分割也不会总是会出现劣质的分割。在整个数组数字全相等时,仍然是最坏情况,时间复杂度是O(n2)。实际上,随机化快速排序得到理论最坏情况的可能性仅为 1/(2n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。
      方法3 三数取中
      引入的原因:虽然随机选取基准时,减少出现不好分割的几率,但是还是最坏情况下还是O(n^2),要缓解这种情况,就引入了三数取中选取基准。
      分析:最佳的划分是将待排序的序列分成等长的子序列,最佳的状态我们可以使用序列的中间的值,也就是第N/2个数。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为基准元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为基准元。显然使用三数中值分割法消除了预排序输入的不好情形,并且减少快排大约5%的比较次数。
  • 在排序过程中选择的基准位置是会发生变化的,并不是固定不变的。简单地想,给定的数组序列是无序的,选择的基准就一定放置在了它本应该放置的位置吗?这肯定不是啊。况且,那个基准只有放置在合适的位置上左右两边才是比它小和比它大的序列。再从另一个角度想,基准两边的数字左边是小于等于基准,右边是大于等于基准,比如例子[3,1,2,3,5] 选择的基准是最左边的3,一开始 i 指向3,j 指向 5,i 不满足条件,j 满足条件向前移动,j 指向3,不满足大于基准的条件,因此这两个3要交换,此时基准位置已经发生了变化。
  • 为什么初始化的时候i=l-1,j=r+1?在排序的过程中如果遇到需要交换的数字,首先要将两个数字交换,然后再将两个指针移动。但是在该模板中不管三七二十一,首先要先移动指针,然后再交换,这样对于边界情况而言,要想移动到边界,那么首先需要在边界外才可以移动到边界上,也就是首先下标要是-1和r+1。传入的参数 l=0,r=n-1,也就是数组最右边数字的下标(n表示数组的个数)
  • 在确定下一步递归的时候, quick_sort(q,l,j)与quick_sort(q,j+1,r); 对于边界选用 j 的时候不可以使用右边界来作为基准,同理,如果使用quick_sort(q,l,i-1)与quick_sort(q,i,r); 不可以使用左边界作为基准。比如样例[1,2] 选择左边为基准,i 一开始指向1,并不满足条件,j 一开始指向2,满足条件,向前移动,此时 j 指向 1,下一次迭代的时候区间范围变成了l,i-1 = [0,-1],i,r = [0,1] 这样一个区间在下一次递归还是这个区间,这样就陷入了死循环中,出不来了。

快排模板如下:

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N=1e5+10;
int q[N];
void quick_sort(int q[],int l,int r)
{
    if(l>=r) return ;//只有一个数或者没有数的时候直接返回
    int i=l-1,j=r+1,x=q[(l+r)>>1];
    while(i<j)
    {
        do i++; while(q[i]<x);//如果大于等于就跳出循环
        do j--; while(q[j]>x);//如果小于等于就跳出循环
        if(i<j) swap(q[i],q[j]);
    }
    quick_sort(q,l,j);
    quick_sort(q,j+1,r);
}
int main()
{
    int n;
    cin>>n;
    for(int i=0;i<n;i++) scanf("%d",&q[i]);
    quick_sort(q,0,n-1);
    for(int i=0;i<n;i++) printf("%d ", q[i]);
    
    return 0;
}

快速选择算法在快速排序的基础上减少了搜索的区间范围

  • 如果在遍历中l==r,意味着区间长度为1,在进行搜索的过程中,保证k是在区间左侧或者右侧的,因此这个数就一定是答案,此时应该返回q[l]。
  • k<= j 去左区间搜索,反之右区间搜索
  • 传递进去的k 应该减1 ,因为 j 的下标开始位置就是减去1的,也就是假如给定 j ,那么其到区间开始位置的长度为 j+1。if(k<=j) 中j 与 k 都是下标。还有一开始传递到函数里的参数 l = 0 r=num.size()-1

本题目中求解的是最大的第 k 个数,需要把快排中的模板do while那块即改变一下大于号以及小于号。

class Solution {
public:
    int quick_sort(vector<int>& nums,int l,int r,int k)
    {
        if(l==r) return nums[k];
        int x=nums[l+r>>1],i=l-1,j=r+1;
        while(i<j)
        {
            do i++; while(nums[i]>x);
            do j--; while(nums[j]<x);
            if(i<j) swap(nums[i],nums[j]);
        }
        if(k<=j) return quick_sort(nums,l,j,k);
        else return quick_sort(nums,j+1,r,k);
    }
    int findKthLargest(vector<int>& nums, int k) {
        return quick_sort(nums,0,nums.size()-1,k-1);
    }
};

扩展:寻找第k个数呢?

与上边的类似,直接套用快速排序中的模板,且do while 循环中大于号小于号一致。

#include <iostream>

using namespace std;

const int N = 1000010;

int q[N];

int quick_sort(int q[], int l, int r, int k)
{
    if (l == r) return q[l];

    int i = l - 1, j = r + 1, x = q[l + r >> 1];
    while (i < j)
    {
        do i ++ ; while (q[i] < x);
        do j -- ; while (q[j] > x);
        if (i < j) swap(q[i], q[j]);
    }

    if (k<=j) return quick_sort(q, l, j, k);
    else return quick_sort(q, j + 1, r, k);
}

int main()
{
    int n, k;
    scanf("%d%d", &n, &k);

    for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);

    cout << quick_sort(q, 0, n - 1, k-1) << endl;

    return 0;
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值