【排序算法】☆☆快速排序+堆排序

数组中的第K个最大元素

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

❤❤基于快排的快速选择

思路

快速排序为典型的分治算法,对a[l...r]排序的过程可以分解如下:

  1. 分解:取任意下标pivot,并将数组划分成两个子数组,a[l...pivot-1](元素均<=a[pivot])和a[pivot+1...r](元素均>a[pivot])。
  2. 解决:对子数组a[l...pivot-1]a[pivot+1...r]重复执行(1)
  3. 合并:将子数组合并,若为原址排序,不需要这一步。

算法

由于每次分解都可以得到从小大到排序的第pivot+1个数,且第k个最大元素实为从小到大排序第n-k个数,故如下:

  • pivot==n-k时,则返回nums[pivot];
  • pivot<n-k时,递归a[pivot+1...r]
  • pivot>n-k时,递归a[l...pivot+1]

实现-递归

int __quickSelect(vector<int>& nums,int l,int r,const int idx){
    int curIdx=__randomQS(nums,l,r);
    if(curIdx==idx){
        return nums[curIdx];
    }
    else{
        return curIdx<idx?quickSelect(nums,curIdx+1,r,idx):quickSelect(nums,l,curIdx-1,idx);
    }
}
int __randomQS(vector<int>& nums,int l,int r){
    //基于随机值来选取pivot
    int pivot=rand()%(r-l+1)+l;
    swap(nums[pivot],nums[r]);
    return __patition(nums,l,r);
}
int __patition(vector<int>& nums,int l,int r){
    //根据x值来划分左右区间,满足左边<=x<右边
    int x=nums[r],j=l-1;
    for(int i=l;i<r;i++){
        if(nums[i]<=x){
            //j保留pivot左侧的位置
            swap(nums[i],nums[++j]);
        }
    }
    swap(nums[j+1],nums[r]);
    return j+1;
}
int findKthLargest(vector<int>& nums, int k) {
    srand(time(0));//保证每次随机值不相同
    return __quickSelect(nums,0,nums.size()-1,nums.size()-k);
}

//寻找中位数
int __median_of_three(const vector<int>& arr,const int i,const int j,const int k) const{
    int a=arr[i],b=arr[j],c=arr[k];
    if(a>b){
        if(b>c) return j;       //b;
        else if(a>c) return k;  //c;
        else return i;          //a;
    }
    else{//a<=b
        if(a>c) return i;           //a;
        else if(b>c) return k;  //c;
        else return j;          //b; 
    }
}

实现-双指针

int findKthLargest(vector<int>& nums, int k){
	srand(time(0));
    k=num.size()-k;
    int l=0,r=nums.size()-1;
    while(l<r){
    	int rlt=quickSort(nums,l,r);
        if(rlt==k) break;
        else if(rlt<l) l=rlt+1;
        else r=rlt-1;
    }
    return nums[k];
}
int quickSort(vector<int>& a,int l,int r){
	int __cnt=rand()%(r-l+1)+l;
    swap(nums[l],nums[__cnt]);
    int pivot=l;
    while(l<r){
    	while(nums[r]>=nums[pivot] && r>l) r--;
        while(nums[l]<=nums[pivot] && r>l) l++;
        swap(nums[l],nums[r]);
    }
    swap(nums[pivot],nums[l]);
    return l;
}

复杂度分析

  • 时间复杂度: O ( N ) O(N) O(N)
  • 空间复杂度: O ( l o g N ) O(log N) O(logN)

我们知道快速排序的性能和「划分」出的子数组的长度密切相关。直观地理解如果每次规模为 n 的问题我们都划分成 1n - 1,每次递归的时候又向 n - 1 的集合中递归,这种情况是最坏的,时间代价是 O ( N 2 ) O(N^2) O(N2)。我们可以引入随机化来加速这个过程,它的时间代价的期望是 O ( N ) O(N) O(N),证明过程可以参考「《算法导论》9.2:期望为线性的选择算法」

❤❤基于堆排序的选择方法

建堆的方法核心在:递归交换,堆实质上是**【父-左右孩子】**约束关系形成的数据结构
在第一次建堆时,需要从下往上递归的进行交换,以后每次添加或删除,都仅需要从上到下进行一次递归交换
当建堆完成时:

  • 对于pop,将根元素=最后一个元素,并从上到下递归交换(数组长度-1后交换)
  • 对于push,数组push_back后,从上到下递归交换(数组长度+1后交换)

思路


注意,每次交换后,都需要对下一层的子堆递归调整,因为交换后破坏了已调整的子堆结构。
假设将数组[0,1,2,3,4,...,n-1]看作一个完全二叉树,则对应父节点i,有

  • 左孩子:2*i+1
  • 右孩子: 2*i+2

大根堆的调整方法(递归交换):

从根节点开始

  1. 对当前**【父节点-左/右孩子】实施交换,并在下一层交换处(若为大根堆,则为左右子树大者)**递归实施交换。
  2. 递归到最后一个非叶子节点时结束。

大根堆的建树方法:

  1. 从最后一个非节点开始实施递归交换
  2. 直到根节点时结束。

算法

  • 交换:maxHeapify(arr,i,last)

递归的实现**【父节点-左/右孩子】的交换**

  • 弹出:
  1. 将root以大根堆的最后一个元素替换。
  2. 实施一次maxHeapify(arr,0,last)

实现

//从上到下递归交换
void __maxHeapify(vector<int>& nums,int i,int heapSize){
    int l=2*i+1,r=2*i+2,bigger=i;
    if(l<heapSize && nums[l]>nums[bigger]){
        bigger=l;
    }
    if(r<heapSize && nums[r]>nums[bigger]){
        bigger=r;
    }
    if(i!=bigger){  
        //仅当当前子树需要交换时,才递归到下一层
        swap(nums[i],nums[bigger]);
        __maxHeapify(nums,bigger,heapSize);
    }
}
//建树-从最后一个非根节点开始实施递归交换
void __buildHeap(vector<int>& nums,int heapSize){
    for(int i=(heapSize>>1);i>=0;i--){
        __maxHeapify(nums,i,heapSize);
    }
}

int findKthLargest(vector<int>& nums, int k) {
    int N=nums.size();
    __buildHeap(nums,N);
    //pop() k个点
    int popId=N-1;
    while(--k){
        nums[0]=nums[popId];
        __maxHeapify(nums,0,--N);
        --popId;
    }
    return nums[0];
}

复杂度分析

  • 时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn),建堆的代价为 O ( n ) O(n) O(n),删除的总代价为 O ( k l o g n ) O(klogn) O(klogn)

  • 空间复杂度: O ( l o g n ) O(logn) O(logn),递归使用栈空间的代价

题目标题难度划分

星级题目难度
简单
☆☆中等
☆☆☆困难

算法掌握难度程度划分

星级掌握难度
普通
经典,易掌握
❤❤经典,略难掌握
❤❤❤困难

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值