找中位数,找第k小,还存在问题

找第k小

上次介绍了找第二大使用的方法时,使用锦标赛的方法,找到最大,在最大的手下败将里找第二大,也就是亚军在冠军的手下败将里产生,亚军只败给过冠军,这种方法比较次数时(n-1) + (logn-1),这个时间复杂度最优的方案了为O(n)
那么怎么找第k大了,季军只能在冠军和亚军的手下败将里产生,第四名只能在前三名手下败将里产生。。。。这个方法也是O(n),但是需要记录每个选手的手下败将名单

还有一种分治的方案,思路来源于快排,快排每次划分子问题,划分成3个部分,小于pivot,等于pivot,大于pivot,假如我们要找的k,小于pivot的标号,那k肯定在左边,等于就就是pivot,大于就在右边,那么每次都排除了一边,问题规模缩小了一半,范围一步步缩小,最后就找到一个pivot他的index就为k。

前面讲了,快排在最坏的情况下,每次选择的都是边缘上的元素,每次问题规模只缩小了1,那他的时间复杂度还是n^2

随机快排在一定程度上可以避免最坏的情况,通过随机选取pivot,可以尽可能让每次划分子问题,差不多时均分的,假如我们找第k小的话,下一阶段就会落在左边或者右边或者pivot,问题规模就会等比缩减。所以这种方法平均时间复杂度是可以达到O(n)

随机快排的实现方法

import random        
def randomizedPartition(arr,low,high):
    def partition(arr,low,high):
        # 这时另外一种考虑方式,而且他是不需要额外空间的,他只使用一个指针来区分小于基准和大于基准的
        # pointer_less_than代表这个指针的左边全部都是小于基准的(包括自己,不包括首元素)
        # 然后从左往右扫描,遇到小于基准的元素,就把小于基准元素区域的后面紧接着的一个元素和他交换
        # 那么小于基准元素区域就多了一个元素,。。。就这样小于基准的元素就连在了一起
        # 首元素是基准元素,小于基准元素区域块,大于基准元素区域块,现在分成了三个部分
        # 把首元素和小于基准元素区域块最后一个元素交换,那三部分就变成,小于的,基准,大于的
        
        # 刚开始小于基准的元素为0,暂且指向首位值
        pointer_less_than = low
        # 然后一次扫描后面所有元素
        for i in range(pointer_less_than +1,high+1):
            # 遇到小于基准的,就把小于基准元素区域的后面紧接着的一个元素和他交换,小于的块相当于也更新了
            if arr[i] < arr[low] :
                pointer_less_than +=1
                arr[pointer_less_than],arr[i]=arr[i],arr[pointer_less_than]
        #  把首元素和小于基准元素区域块最后一个元素交换,那三部分就变成,小于的,基准,大于的       
        arr[low],arr[pointer_less_than] = arr[pointer_less_than],arr[low]
        
        return pointer_less_than
    
    index = random.randint(low,high)
    arr[low],arr[index]=arr[index],arr[low]
    return partition(arr,low,high)

def randomizedQuicksort_for_medium(arr,low,high,k):
     
    if low <= high:
        index = randomizedPartition(arr,low,high)
        if k == index:
            return arr[index]
        elif k < index :            
            return randomizedQuicksort_for_medium(arr,low,index-1,k)
        else:
            return randomizedQuicksort_for_medium(arr,index+1,high,k)


arr3 = [7,3,66,33,22,66,99,0,1]
print(arr3)
print(sorted(arr3))
print(randomizedQuicksort_for_medium(arr3,0,len(arr3)-1,4))

[7, 3, 66, 33, 22, 66, 99, 0, 1]
[0, 1, 3, 7, 22, 33, 66, 66, 99]
22

还有一种思路,就是那个pivot也不是随机选择的,怎么来了,他应该是在中位数附近,那么我们是不是可以,计算的来pivot了

下面有一种失败的方法,参考一下:

# 简单的插入排序
def insert_sort(arr,low,high):
    
    for i in range(low+1,high+1):
        temp = arr[i]
        j = i
        
        while arr[j-1] > temp and j >low:
            arr[j] = arr[j-1]
            j -=1
        arr[j] = temp

# 针对已分组的数据块排序
def insert_sort_node(arr,low,high):
    
    for i in range(low+1,high+1):
        temp = arr[i]
        j = i
        
        while arr[j-1][0] > temp[0] and j >low:
            arr[j] = arr[j-1]
            j -=1
        arr[j] = temp

#  规约子问题的方法:
# 按照每5个一组,在每组中位数里取中位数,然后把小于中位数的元素放在左边,大于的放在右边
def partion_group_sort_size_5(arr,left,right):
    
    # 我们是在arr上原地操作的
    # 下面是每5个一组
    low =left
    high = left +4
    
    # 保存一下中位数数组,便于求中位数组的中位数
    # 此法相对于快排的规约,选择首元素或者随机选择,这个pivot是通过计算得出
    # 每5个分成一组,最后5的余数,特殊处理
    medium =[]
    if right -left > 4:
        while high <= right:
            insert_sort(arr,low,high)
            medium.append((arr[low+2],low+2))
            low +=5
            high +=5
    
    # 假如输入刚好为5个或者少于5个,直接插入排序,返回最中间的index
    # 插入排序对已有序大的序列,效率高,在次情况下,比较次数很少
    # 这种情况下直接返回中位数的标号
    else:
        insert_sort(arr,low,high)
        return (low+high)//2 -1
    

    # 对中位数数组排序,取得中位数,也就是分解子问题的pivot
    insert_sort_node(medium,0,len(medium)-1)

    # 把小于pivot的数据放左边,把大于pivot数据放右边
    # 分组里面的左上角可以直接放在前后和右下角的数据可以直接放在后面
    # 左下角和右上角,以及最后的余数需要比较之后,再决定放左边还是右边
    medium_num = (len(medium) -1)//2
    # 现在中位数为medium[medium_num],把小于medium[medium_num]放到左边
    
    # 因为没有足够的空位,所以临时放在list里,然后最后复制回去
    list = [-1]*(right-left +1)
    # 小于pivot的指针,大于pivot的指针
    i =0
    j =right-left
    
    # 先处理左边的数据,处理左上角
    for k in range(medium_num):
        #左上角可以直接放进左边
        # 理论上是直接把左上角放在head,右下角放在end,然后再处理左下角和右上角,以及最后的余数
        # 这样可以尽量保证有序,减少插入排序的工作量
        list[i]=arr[medium[k][1]-2]
        i +=1
        list[i]=arr[medium[k][1]-1]
        i +=1
        list[i]=arr[medium[k][1]]
        i +=1

    # 处理中位数后面的分组,处理右下角
    for k in range(len(medium)-1,medium_num,-1):
        # 从最后面开始处理,因为这里的数都比较大
        list[j]=arr[medium[k][1]+2]
        j -=1
        list[j]=arr[medium[k][1]+1]
        j -=1
        list[j]=arr[medium[k][1]]
        j -=1

    # 处理中位数那一组上边,上面的放左边
    list[i]=arr[medium[medium_num][1]-2]
    i +=1
    list[i]=arr[medium[medium_num][1]-1]    
    i +=1

    # 处理中位数那一组,下面的放右边,为什么在这里了,因为他在大于pivot里面算是较小的,
    # 为了保证划分之后的子问题尽量有序,先下后上
    list[j] = arr[medium[medium_num][1] +2]
    j -=1
    list[j] = arr[medium[medium_num][1] +1]
    j -=1  
    
    # arr[medium[medium_num][1] 位置还不清楚最后添加
    
    # 处理左下角
    for k in range(medium_num):        
        # 左下角需要比较之后才能决定放左边还是右边,先上后下,上面的比较小
        if arr[medium[k][1] + 1] > medium[medium_num][0]:
            list[j] = arr[medium[k][1] +1]
            j -=1
        else:
            list[i]=arr[medium[k][1] +1]
            i +=1
            
        if arr[medium[k][1] + 2] > medium[medium_num][0]:
            list[j] = arr[medium[k][1] +2]
            j -=1
        else:
            list[i]=arr[medium[k][1] +2]
            i +=1            
        
    # 处理右上角        
    for k in range(len(medium)-1,medium_num,-1):        
        if arr[medium[k][1] - 1] > medium[medium_num][0]:
            list[j] = arr[medium[k][1] -1]
            j -=1
        else:
            list[i]=arr[medium[k][1] -1]
            i +=1 
            
        if arr[medium[k][1] - 2] > medium[medium_num][0]:
            list[j] = arr[medium[k][1] -2]
            j -=1
        else:
            list[i]=arr[medium[k][1] -2]
            i +=1 

    # 处理最后的余数
    for k in range(low,right+1):

        if arr[k] > medium[medium_num][0]:
            list[j] = arr[k]
            j -=1
        else:
            list[i]=arr[k]
            i +=1
            
    # 把最后的中位数放入       
    list[i] = medium[medium_num][0]
    # 把临时结果放回原来的数组
    arr[left:right+1] = list[:]
    # 返回中位数的index
    return(left+i)

# 使用分治获取中位数
def partion_group_sort_size_5_for_medium(arr,low,high,k):
        
    # 递归出口,当左右指针重合时,便是找到了第k小的数组
    if low <= high:
        # 取经过计算的pivot分组,这个pivot 的index应该接近中位数的index,这样就可以很快的收敛
        index = partion_group_sort_size_5(arr,low,high)
        # 这个index恰好为中位数的index时,就可以直接返回中位数大小
        if k == index:
            return arr[index]
        # 当index>k时,代表在左半部分
        elif k < index :            
            return partion_group_sort_size_5_for_medium(arr,low,index-1,k)
        # 否则就在右半部分
        else:
            return partion_group_sort_size_5_for_medium(arr,index+1,high,k)        
    
            
    
arr3 = [7,3,66,33,22,66,9,0,1,11,14,17,15,22,88,91,10,5,11,77,88,45,990,1]
print(arr3)
print(partion_group_sort_size_5_for_medium(arr3,0,len(arr3)-1,len(arr3)//2-1))

a =sorted(arr3)
print(a)
print(a[len(arr3)//2-1])  

[7, 3, 66, 33, 22, 66, 9, 0, 1, 11, 14, 17, 15, 22, 88, 91, 10, 5, 11, 77, 88, 45, 990, 1]
15
[0, 1, 1, 3, 5, 7, 9, 10, 11, 11, 14, 15, 17, 22, 22, 33, 45, 66, 66, 77, 88, 88, 91, 990]
15

为什么是失败的方法了?这里给中位数数组求中位数的方法是插入排序?你是没睡醒吗?n/5的规模使用插入排序,你说鸡肋不鸡肋,虽然后面的数组基本都是有序的,但是第一次的工作量就有O(n^2)的工作量。

我们的目标是求中位数,划分子问题中位数划分最均衡,所以用分治求中位数效率比较高,那么我们求中位数数组时就应该使用分治的方法,正确的方法是:求中位数数组的中位数,递归调用自身,得到pivot后,左半边要调用自身,右半边也要调用自身,也就是三个地方都需要调用自身。

# -*- coding: utf-8 -*-

# 简单的插入排序
def insert_sort(arr,low,high):
    
    for i in range(low+1,high+1):
        temp = arr[i]
        j = i
        
        while arr[j-1] > temp and j >low:
            arr[j] = arr[j-1]
            j -=1
        arr[j] = temp
        
        
def group_sort_size_5(arr):
    
    # 我们是在arr上原地操作的
    # 下面是每5个一组
    low =0
    high = 4
    
    # 保存一下中位数数组,便于求中位数组的中位数
    # 此法相对于快排的规约,选择首元素或者随机选择,这个pivot是通过计算得出
    # 每5个分成一组,最后5的余数,特殊处理
    medium =[]
    while high < len(arr):
        insert_sort(arr,low,high)
        medium.append(arr[low+2])
        low +=5
        high +=5 
        
    insert_sort(arr,low,len(arr)-1)
    
    return arr,medium

def partion(arr,m_star,medium_num):
    
    # 因为没有足够的空位,所以临时放在list里,然后最后复制回去
    list = [-1]*(len(arr))
    # 小于pivot的指针,大于pivot的指针
    i =0
    j =len(arr)-1
    
    if medium_num == 0:
        left = arr[:len(arr)//2-1]
        right =arr[len(arr)//2:]
        return len(arr)//2-1,left,right
        
    
    # 先处理左边的数据,处理左上角
    for k in range(medium_num):
        #左上角可以直接放进左边
        # 理论上是直接把左上角放在head,右下角放在end,然后再处理左下角和右上角,以及最后的余数
        # 这样可以尽量保证有序,减少插入排序的工作量
        if arr[5*k + 2] < m_star:
            list[i]=arr[5*k + 2-2]
            i +=1
            list[i]=arr[5*k + 2-1]
            i +=1
            list[i]=arr[5*k + 2]
            i +=1

    # 处理中位数后面的分组,处理右下角
        # 从最后面开始处理,因为这里的数都比较大
        elif  arr[5*k + 2] > m_star:
            list[j]=arr[5*k + 2+2]
            j -=1
            list[j]=arr[5*k + 2+1]
            j -=1
            list[j]=arr[5*k + 2]
            j -=1
            
        else:
            # 处理中位数那一组上边,上面的放左边
            list[i]=arr[5*k + 2-2]
            i +=1
            list[i]=arr[5*k + 2-1]    
            i +=1

            # 处理中位数那一组,下面的放右边,为什么在这里了,因为他在大于pivot里面算是较小的,
            # 为了保证划分之后的子问题尽量有序,先下后上
            list[j] = arr[5*k + 2 +2]
            j -=1
            list[j] = arr[5*k + 2 +1]
            j -=1  
    
    # arr[medium[medium_num][1] 位置还不清楚最后添加
    
    # 处理左下角
    for k in range(medium_num):        
        # 左下角需要比较之后才能决定放左边还是右边,先上后下,上面的比较小
        if arr[5*k + 2] < m_star:
            if arr[5*k + 2 + 1] > m_star:
                list[j] = arr[5*k + 2 +1]
                j -=1
            else:
                list[i]=arr[5*k + 2 +1]
                i +=1
                
            if arr[5*k + 2 + 2] > m_star:
                list[j] = arr[5*k + 2 +2]
                j -=1
            else:
                list[i]=arr[5*k + 2 +2]
                i +=1            
        
        if arr[5*k + 2] > m_star:
            if arr[5*k + 2 - 1] > m_star:
                list[j] = arr[5*k + 2 -1]
                j -=1
            else:
                list[i]=arr[5*k + 2 -1]
                i +=1 
                
            if arr[5*k + 2 - 2] > m_star:
                list[j] = arr[5*k + 2 -2]
                j -=1
            else:
                list[i]=arr[5*k + 2 -2]
                i +=1 

    # 处理最后的余数
    for k in range(5*medium_num,len(arr)):

        if arr[k] > m_star:
            list[j] = arr[k]
            j -=1
        else:
            list[i]=arr[k]
            i +=1
            
    # 把最后的中位数放入       
    list[i] = m_star
    # 把临时结果放回原来的数组
    arr[:] = list[:]
    left = arr[:i]
    right =arr[i+1:]
    return i,left,right
    
    
def select_recursive(arr,k):
    if len(arr) == 1:
        return arr[0]
    
    if len(arr) >1:                    
        arr,medium = group_sort_size_5(arr)
        medium_num = len(medium)
#        print(medium_num)
        m_star = select_recursive(medium,medium_num/2-1)
        print("m_star",m_star)
        
        index,left,right = partion(arr,m_star,medium_num)
        print(arr[index],left,right)
        
        if k == index:
            return arr[index]
        
        elif k < index:
            return select_recursive(left,k)
        else:
            return select_recursive(right,k-index-1)
    
arr = [7,3,66,33,22,66,9,0,1,11,14,17,9,10,13,99,44,33,77,88,99,101,404,87,44,22,11,99,43,22]
print(arr)
a= sorted(arr)
print(a)
print(a[len(a)//2-1])
print(select_recursive(arr,len(arr)/2-1))

runfile('D:/share/test/common_select_k.py', wdir='D:/share/test')
[7, 3, 66, 33, 22, 66, 9, 0, 1, 11, 14, 17, 9, 10, 13, 99, 44, 33, 77, 88, 99, 101, 404, 87, 44, 22, 11, 99, 43, 22]
[0, 1, 3, 7, 9, 9, 10, 11, 11, 13, 14, 17, 22, 22, 22, 33, 33, 43, 44, 44, 66, 66, 77, 87, 88, 99, 99, 99, 101, 404]
22
m_star 22
22 [9, 13, 22] [77, 99]
m_star None
9 [] [13, 22]
m_star None
13 [] [22]
m_star 22
22 [3, 7, 0, 1, 9, 9, 10, 13, 11, 22, 11, 14, 17] [-1, 44, 87, 33, 44, 66, 43, 99, 99, 101, 404, 77, 88, 99, 33, 66]
m_star None
44 [] [88, 99]
m_star None
88 [] [99]
m_star None

按照如下实现代码还是有问题,原因在于第一次调用自身时,出栈时划分时,这时的index,left,right = partion(arr,m_star,medium_num),这个划分函数arr不是我们期望的arr,他这时是medium,找没想到解决的办法

在这里插入图片描述

三次递归调用自身的方法目前实在是实现不了,经过网上游荡发现了一种叫BFPRT的算法,这里实在太长了

猜你喜欢

转载自blog.csdn.net/weixin_40759186/article/details/85006912