• BFPRT算法


    首先讲一下bfprt算法是干嘛的?

    bfprt算法是用来求数组中第k小的元素的算法,bfprt算法可以在O(n)时间内求出答案。

    算法思想:

    对于求数组中第k小的元素的问题,我们已经有很好的常规算法了,这个算法在最好的情况下时间复杂度是O(n),但在最坏的情况下是O(n^2)的,其实bfprt算法就是在这个基础上改进的。

    常规解法:

    我们随机在数组中选择一个数作为划分值(number),然后进行快排的partation过程(将小于number的数放到数组左边,等于number的数放到数组中间,大于number的数放到数组右边),然后判断k与等于number区域的相对关系,如果k正好在等于number区域,那么数组第k小的数就是number,如果k在等于number区域的左边,那么我们递归对左边再进行上述过程,如果k在等于number区域的右边,那我们递归对右边再进行上述过程。

    对于常规解法,我们分析一下它的时间复杂度:

    递归函数复杂度计算:

    T(N)=aT(N/b)+o(N^d);
    当log(b,a)>d时 复杂度为O(N^log(b,a));
    当log(b,a)=d时 复杂度为O(N^d
    log(2,N));
    当log(b,a)<d时 复杂度为O(N^d);
    N为样本数据个数 即数组中元素的个数。
    N/b为将这N个数划分为更小的部分,每个部分的样本数据个数,一般为均分,那么b等于2。
    a为划分为多个部分后,小部分执行的次数。
    d为递归函数调用完成后剩下的操作的时间复杂度。

    对于最好的情况:每次所选的number正好在数组的正中间,那么上式中a等于1,b等于2,对于partation过程,时间复杂度是O(n),所以d等于1。所以T(N)= T(N/2)+ O(N),此时 log( 2 , 1 ) < 1,故时间复杂度为O(N)。【第一次是O(N), 第二次是O(N/2), 第三次是 O(N/4) ... 第N次是O(N/N),总时间复杂度就是O(N + N/2 + N/4....+1) = O(2N-1)=O(N)】

    对于最坏情况:每次所选的number正好在数组最边上,那么时间复杂度为O(N ^ 2)。【第一次是O(N), 第二次是O(N-1), 第三次是 O(N-2) ... 第N次是O(N-N),总时间复杂度就是O(N + (N-1) + (N-2)....+(N-N))=O(N^2)】

    bfprt算法就是在这个number上做文章,bfprt算法能够保证每次所选的number在数组的中间位置,那么时间复杂度就是O(N)。

    bfprt解法:

    bfprt解法和常规解法唯一不同的就是在number的选取上,其他地方一模一样,所以我们只讲选取number这一过程。

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

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

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

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

    接下来我们分析一下为什么bfprt算法每次选number的时候都能够在数组的中间位置。

    我们假设这就是分出来的每5个数的小组,每一列代表一个小组。

    图中红框内的数我们假设就是每一组的中位数。我们假设总数组的数字个数是N,那么中位数数组中数字的个数就是 N / 5 。

    我们假设用蓝框框起来的数是中位数数组的中位数(divide),那么由中位数的性质可知,中位数数组中有一半的数比这个divide大,所以总共有 N / 10个数比这个divide大。

    用紫色框框出的数肯定也是比divide大,所以至少有N / 10 + ( 2N ) / 10 = ( 3N ) / 10 个数比divide大,那么以divide为划分的partation过程能够使得divide在数组的靠近中间的位置,最坏情况也能够在数组的 ( 3N ) / 10 或者 ( 7N ) / 10 的位置。时间复杂度为O(N)。
    讲一下BFPRT的空间复杂度,select分治过程用到递归,需要栈空间存储,递归次数即是栈空间大小,由于每次只处理划分后的一半,因此最差情况需要递归log(N)次,空间复杂度为log(N)。

    代码如下:

    """
    BFPRT算法,简称 TOP K 问题,用来求数组中第k小元素的算法
        - 可以在O(n)时间内求出答案
    """
    class Solution:
        def get_median(self, nums):
            '''计算5个数的中位数'''
            begin, end = 0, len(nums) - 1
    
            sum = begin + end
            mid = sum // 2 + sum % 2  # 这个地方加上sum%2是为了确保偶数个数时我们求的是中间两个数的后一个
            return sorted(nums)[mid]  # 常量级别,影响不大,也可改用插入排序
    
        def BFPRT(self, nums, left, right):
            '''选择partition基准: 划分每5个一组,求每组的中位数'''
            num = right-left + 1
            offset = 0 if num % 5 == 0 else 1  # 最后如果剩余的数不足5个,我们也将其分成一个小组,和前面同等对待
            group = num//5 + offset
    
            median = []  # 中位数数组
            for i in range(group):
                begin = left + i * 5
                end = begin + 4
                Median = self.get_median(nums[begin:min(end, right) + 1])
                median.append(Median)
            return self.get_median(median)  # 这里是求中位数数组的中位数,再调用getmedian不就行了,为啥有的人还要用下面递归select,虽然也是正确的,但直接getmedian快一点啊
            # return select(median, 0, groups-1, groups//2)  # 求出生成好的median数组的中位数,作为partation函数的划分值
    
        def partition(self, nums, left, right, base):
            '''将基准base归位'''
            if left >= right:
                return
    
            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[max_index], nums[right] = nums[right], nums[max_index]
            return max_index
    
        def select(self, nums, left, right, K):
            '''快速选择过程'''
            if left == right:
                return nums[left]
    
            base = self.BFPRT(nums, left, right)                              # 选择基准
            base_index = self.partition(nums, left, right, nums.index(base))  # 基准归位
    
            if base_index == K:   # 判断目前已归位的基准,是不是第K位
                return nums[base_index]
            elif base_index > K:  # 递归左半部分
                return self.select(nums, left, base_index - 1, K)
            else:                 # 递归右半部分
                return self.select(nums, base_index + 1, right, K)
    
        def MoreThanHalfNum_Solution(self, numbers):
            if not numbers:
                return 0
    
            left, right = 0, len(numbers) - 1
            length = len(numbers)
            K = right // 2
            ans = self.select(numbers, left, right, right - K + 1)  # 第K大,也是第N-K+1小
    
            # 核实
            cnt = 0
            for x in numbers:
                if x == ans:
                    cnt += 1
                    if cnt == length // 2 + 1:
                        return ans
            return 0
    
    
    # 数组中出现次数超过一半的数字
    s = Solution()
    ans = s.MoreThanHalfNum_Solution([1, 2, 3, 2, 2, 2, 5, 4, 2])
    print(ans)
    

    https://blog.csdn.net/qq_40938077/article/details/81213820#commentBox

  • 相关阅读:
    Ubuntu apache 虚拟主机配置
    Apache Directory 指令
    Sphinx 全文检索
    双系统Linux(ubuntu)进入windows的NTFS分区之挂载错误
    Composer 中国镜像
    Liunx下全局安装 Composer
    提高代码质量技巧
    mysql 中浮点型与定点型记录
    cookie安全
    php 小坑记录
  • 原文地址:https://www.cnblogs.com/ldy-miss/p/12031077.html
Copyright © 2020-2023  润新知