zoukankan      html  css  js  c++  java
  • 【Leetcode 堆、快速选择、Top-K问题 BFPRT】数组中的第K个最大元素(215)

    这道题很强大,引出了很多知识点

    题目

    在未排序的数组中找到第 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 ≤ 数组的长度。

    解答

    题目要求找出一个序列中第K大元素,可以很容易想到下面的解法:

    1,给序列排序,取出倒数第K个。快速排序为 n·log(n)

    2,小顶堆,维护一个大小为K的小顶堆,把序列元素大于堆顶的元素依次入堆,完了堆顶就是第K大。维护一个堆(插入O(1),删除log(n))的时间复杂度是log(n),此处为log(k),总的时间复杂度是n·log(k),空间复杂度为O(k)。

    PS:大顶堆也可以

    3,快速选择算法,随时选择一个基准, 然后进行快排的partation过程(将序列中小于基准的都放在它的左边,大于它的都放在右边),基准归位,此时基准已经在序列中排好序的位置;再判断要找的第 N - k 个元素与基准坐标的关系, 如果k正好等于基准位置,那么数组第k小的数就是基准,如果K小于基准坐标位置,则只递归左半部分,否则只递归右半部分。

    如果是快速排序算法,会在这里递归地对两部分进行快速排序,时间复杂度为n·log(n),而在这里,由于知道要找的第 N - k 小的元素在哪部分中,我们不需要对两部分都做处理,这样就将平均时间复杂度下降到O(n)。

    这种算法最好情况是每次基准都划分在了序列中间位置,时间复杂度为O(n);最坏情况是每次基准都划分在了边缘位置,时间复杂度为O(n^2)。第四种方法优化了基准的选取,用线性复杂度O(n)的时间就解决了问题。

    4,BFPRT, BFPRT算法就是在基准上做文章,能够保证每次所选的基准在数组的中间位置,那么时间复杂度就是O(N),BFPRT解法和快速选择解法唯一不同的就是在基准的选取上,所以只讲选取基准这一过程。

    第一步:我们将数组每5个相邻的数分成一组,后面的数如果不够5个数也分成一组。

    第二步:对于每组数,我们找出这5个数的中位数,将所有组的中位数构成一个median数组(中位数数组)。

    第三步:我们再求这个中位数数组中的中位数,此时所求出的中位数就是基准。

    第四步:通过这个基准进行partation过程,下面和常规解法就一样了。

    BFPRT是专门用来求 TOP-K 问题的, 时间复杂度为O(N)。

    通过代码如下:

    1,排序

    class Solution:
        def findKthLargest(self, nums: List[int], k: int) -> int:
            return sorted(nums)[-k]
    

    2,小顶堆

    from heapq import *
    
    class Solution:
        def findKthLargest(self, nums: List[int], k: int) -> int:
            l = []                               # 存储堆
            for x in nums:
                if l and len(l)==k and x>l[0]:   # 堆满并且x大于堆顶,pop堆顶,x入堆
                    heapreplace(l, x)
                if not l or len(l)<k:            # 堆没满,直接入堆
                    heappush(l, x)
            return l[0]
            # 或者直接return nlargest(k, nums)[-1]
    

    3,快速选择

    import random
    
    class Solution:
        def findKthLargest(self, nums, k):
            def partition(left, right, base):
                temp = nums[base]
                nums[base], nums[right] = nums[right], nums[base]  # 基准和末尾元素互换
    
                max_index = left
                for i in range(left, right):  # 把所有小于基准的移到左边
                    if nums[i] < temp:
                        nums[max_index], nums[i] = nums[i], nums[max_index]
                        max_index += 1
    
                nums[right], nums[max_index] = nums[max_index], nums[right]  # 基准归位
                return max_index
    
            def select(left, right, k_smallest):
                """在 nums[left, right] 找第k小的元素"""
                if left == right:  # 递归终止条件
                    return nums[left]
                pivot_index = random.randint(left, right)  # 随机选择基准(比固定选第一个要好)
                base_index = partition(left, right, pivot_index)  # 选第一个(left)为基准,并归位。
                if base_index == k_smallest:  # 判断目前已归位的基准,是不是第k_smallest位
                    return nums[k_smallest]
                elif k_smallest < base_index:  # go to 左半部分
                    return select(left, base_index - 1, k_smallest)
                else:  # go to 右半部分
                    return select(base_index + 1, right, k_smallest)
    
            return select(0, len(nums) - 1, len(nums) - k)  # 第k大,是第n-k小
    

    4,BFPRT

    class Solution:
        def findKthLargest(self, nums, k):
            def getmedian(lis):
                """返回序列lis中位数,在BFPRT中就是求每5个数小组的中位数"""
                begin = 0
                end = len(lis)-1
    
                sum = begin+end
                mid = sum//2 + sum % 2  # 这个地方加上sum%2是为了确保偶数个数时我们求的是中间两个数的后一个
                return sorted(lis)[mid]
    
            def BFPRT(nums, left, right):
                """分成每5个数一个小组,并求出每个小组内的中位数"""
                num = right-left+1
                offset = 0 if num % 5 == 0 else 1  # 最后如果剩余的数不足5个,我们也将其分成一个小组,和前面同等对待
                groups = num//5 + offset
                median = []  # 中位数数组
                for i in range(groups):
                    begin = left+i*5
                    end = begin + 4
                    Median = getmedian(nums[begin:min(end, right)+1])
                    median.append(Median)
                return getmedian(median)
    
            def partition(nums, left, right, base):
                """在 nums[left, right] 将基准base归位"""
                temp = nums[base]
                nums[base], nums[right] = nums[right], nums[base]  # 基准和末尾元素互换
    
                max_index = left
                for i in range(left, right):  # 把所有小于基准的移到左边
                    if nums[i] <= temp:  # 要等于啊!这里好坑的说.. 否则通不过样例[3, 3, 3, 3, 4, 3, 3, 3, 3]  k = 1
                        nums[max_index], nums[i] = nums[i], nums[max_index]
                        max_index += 1
                nums[right], nums[max_index] = nums[max_index], nums[right]  # 基准归位
                return max_index
    
            def select(nums, left, right, k_smallest):
                """在 nums[left, right] 找第k小的元素"""
                if left == right:  # 递归终止条件
                    return nums[left]
                # pivot_index = random.randint(left, right)
                base = BFPRT(nums, left, right)
                base_index = partition(nums, left, right, nums.index(base))  # 选base为基准,并归位。
                if base_index == k_smallest:  # 判断目前已归位的基准,是不是第k_smallest位
                    return nums[k_smallest]
                elif k_smallest < base_index:  # 递归左半部分
                    return select(nums, left, base_index - 1, k_smallest)
                else:  # 递归右半部分
                    return select(nums, base_index + 1, right, k_smallest)
            return select(nums, 0, len(nums) - 1, len(nums) - k)  # 第k大,是第n-k小
    

    BFPRT笔记 | 对于求包含重复值的序列第K大,应该先去重再入BFPRT。

    PS:大顶堆也可以,写都写出来了,不贴出来怪怪的..

    from heapq import *
    
    class Solution:
        # 用负数入堆建立大顶堆,pop()k次,就是第k大。时间复杂度最坏n·log(n),最好k·log(n),空间复杂度O(n)
        def findKthLargest(self, nums: List[int], k: int) -> int:
            l = []
            for x in nums:
                heappush(l, -x)         # 平均O(1),最坏log(n)
            for x in range(k):
                c = heappop(l)          # log(n)
            return -c
    
  • 相关阅读:
    [CSP-S模拟测试]:军训队列(DP+乱搞)
    [CSP-S模拟测试]:stone(结论+桶+前缀和+差分)
    [CSP-S模拟测试]:bird(线段树优化DP)
    [CSP-S模拟测试]:maze(二分答案+最短路)
    [CSP-S模拟测试]:优化(贪心+DP)
    uoj132/BZOJ4200/洛谷P2304 [Noi2015]小园丁与老司机 【dp + 带上下界网络流】
    Miiler-Robin素数测试与Pollard-Rho大数分解法
    hdu4336 Card Collector 【最值反演】
    loj2542 「PKUWC2018」随机游走 【树形dp + 状压dp + 数学】
    loj2540 「PKUWC2018」随机算法 【状压dp】
  • 原文地址:https://www.cnblogs.com/ldy-miss/p/12032321.html
Copyright © 2011-2022 走看看