【算法-面试】二分搜索专题

# coding = "utf-8"

'''
二分搜索专题

第一个,最基本的二分查找算法:
因为我们初始化 right = nums.length - 1
所以决定了我们的「搜索区间」是 [left, right]
所以决定了 while (left <= right)
同时也决定了 left = mid+1 和 right = mid-1
因为我们只需找到一个 target 的索引即可
所以当 nums[mid] == target 时可以立即返回


第二个,寻找左侧边界的二分查找:
因为我们初始化 right = nums.length
所以决定了我们的「搜索区间」是 [left, right)
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid
因为我们需找到 target 的最左侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧右侧边界以锁定左侧边界


第三个,寻找右侧边界的二分查找:
因为我们初始化 right = nums.length
所以决定了我们的「搜索区间」是 [left, right)
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid
因为我们需找到 target 的最右侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧左侧边界以锁定右侧边界
又因为收紧左侧边界时必须 left = mid + 1
所以最后无论返回 left 还是 right,必须减一

1. 如果nums是升序或者降序数组,就可以考虑二分搜索
2. 更广泛的, 如果func(i)函数是在i上单调的函数,一定可以使用二分查找技巧优化 for 循环。
'''


def bin_search(nums, target):
    if len(nums) == 0:
        return -1

    l, r = 0, len(nums) - 1
    while l <= r:
        mid = l + (r - l) // 2
        if target == nums[mid]:
            return mid
        elif target > nums[mid]:
            l = mid + 1
        elif target < nums[mid]:
            r = mid - 1
    return -1


def left_bound(nums, target):
    '''
    二分查找数组的左边界.如果找到返回该值最左侧值的索引,如果没有找到,则返回-1
    :param nums:
    :param target:
    :return:
    '''
    if len(nums) == 0:
        return -1
    l, r = 0, len(nums) - 1
    while l <= r:
        mid = l + (r - l) // 2
        if target == nums[mid]:
            r = mid - 1  # [l...mid-1]  往左压缩区间
        elif target > nums[mid]:
            l = mid + 1  # [mid+1...r]
        elif target < nums[mid]:
            r = mid - 1  # [l...mid-1]
    if l >= len(nums) or nums[l] != target:
        return -1
    return l

def right_bound(nums, target):
    '''
    二分查找数组的右边界,如果找到,返回该值最右侧的索引;如果没找到,返回-1
    :param nums:
    :param target:
    :return:
    '''
    if len(nums) == 0:
        return -1
    l, r = 0, len(nums) - 1
    while l <= r:
        mid = l + (r - l) // 2
        if target == nums[mid]:
            l = mid + 1  # 向右侧区间压缩 [mid+1...r]
        elif target > nums[mid]:
            l = mid + 1  # [mid+1...r]
        elif target < nums[mid]:
            r = mid - 1  # [l...mid-1]
    if r < 0 or nums[r] != target:
        return -1
    return r


def left_floor(nums, target):
    '''
    二分法查找上界
    1. 如果找不到target,则以最后一个个比target小的值的索引为返回值
    2. 如果找到target,则以最左侧的target值的索引作为返回值
    '''
    if len(nums) == 0:
        return -1

    l, r, find = 0, len(nums) - 1, False
    while l <= r:
        mid = l + (r - l) // 2
        if target == nums[mid]:
            find = True
            r = mid - 1
        elif target < nums[mid]:
            r = mid - 1
        elif target > nums[mid]:
            l = mid + 1
    if find:
        return l
    return l - 1


def right_cell(nums, target):
    '''
    二分法查找上界
    1. 如果找不到target,则以第一个比target大的值的索引为返回值
    2. 如果找到target,则以最右侧的target值的索引作为返回值
    '''
    if len(nums) == 0:
        return -1

    l, r, find = 0, len(nums) - 1, False
    while l <= r:
        mid = l + (r - l) // 2
        if target == nums[mid]:
            find = True
            l = mid + 1
        elif target < nums[mid]:
            r = mid - 1
        elif target > nums[mid]:
            l = mid + 1
    if find:
        return r
    if r == len(nums) - 1 and target > nums[-1]:  # 说明target比最右侧的值还要大
        return -1
    return r + 1



'''
二分搜索相关的场景题
1.吃香蕉问题 875
2.货物搬运问题 1011

抽象:
    1. 可以表示位y=f(x)的问题
    2. f是单调函数
    3. 满足约束条件f(x) == target时的x的值
    比如: 给你一个升序排列的有序数组nums以及一个目标元素target,请你计算target在数组中的索引位置,如果有多个目标元素,返回最小的索引。
'''


def min_eating_speed(nums, h):
    '''
    有n堆香蕉, 每堆香蕉数量不一定一样,要求在h小时内,吃掉所有香蕉的最小速度
    ps.如果某堆香蕉的个数小于吃香蕉的速度,则等到下一个小时再开始吃香蕉
    思路:
        1. 吃香蕉时间,f(x)=sum(每堆吃掉的小时数)
        2. 吃的速度越快,用时越短,很明显是个单调递减的函数
        3. 左右边界,l=1和r=10^9+1 最慢1香蕉/h, 最快香蕉堆中的最大值/h
        4. 二分查找x,使得x==h
    :return:
    '''
    import math
    def f_hours(arr, x):
        # 求x个香蕉/h的情况下,吃掉nums列表中的香蕉需要多少时间
        hours = 0
        for i in range(len(arr)):
            hours += arr[i] // x
            if arr[i] % x != 0:  # 吃了hours之后,还剩下 nums[i]%x 个香蕉,要在下一个小时吃掉
                hours += 1
        return hours

    l, r = 1, int(math.pow(10, 9))  # [l, r)是求取速度x的区间
    while l < r:
        mid = l + (r - l) // 2
        if f_hours(nums, mid) <= h:
            r = mid  # 单调递减,因此将r更新为mid值
        else:
            l = mid + 1

    return l


def ship_within_days(weights, d):
    '''
    要求weight[...]中的货物在d天内送到对面港口,求载重量x的货运船的最小载重量
    思路:
        1. 载重量函数f(x)=sum(运送天数), 载重量为x
        2. 载重量越大,天数越少,即单调递减
        3. 运用二分法,求解载重量最小,即最左边的值
    :return:
    '''

    def f_days(w, x):
        weights_sum, days = 0, 0
        for i in range(len(w)):
            if weights_sum + w[i] <= x:
                weights_sum += w[i]
                continue
            weights_sum = w[i]
            days += 1

        # if weights_sum <= x:
        #     days += 1
        days += 1  # 最后剩余的一堆货,在[l,r)区间内
        return days

    #  确定搜索区间
    l, r = 0, 1
    for i in range(len(weights)):
        l = max(l, weights[i])
        r += weights[i]

    while l < r:  # [l,r)开区间
        mid = l + (r - l) // 2
        if f_days(weights, mid) <= d:
            r = mid
        else:
            l = mid + 1
    return l


def split_array_to_get_max(nums, m):
    '''
    寻找nums数组中分成m个子数组的最大子数组和的最小值
    leetcode:410 快手面试题
    思路:
        拆分:
            1. nums拆成m个子数组
            2. 每个子数组都有各自的sum值,s1...s_k
            3. 在这些sum值中,取最大值s_max
            4. 由于子数组拆分的种类有多种,因此产生了多个s_max,在这些s_max中找到最小值
        思路1:深度优先遍历,暴力循环
        思路2:转换思路,在[max(nums), sum(nums)]区间内的值必定有nums子数组的sum值,二分查找满足这些sum值,能构成子数组的情况
            满足:1. 子数组个数为n 2.sum值最小,即上述区间的左边界值
    :return:
    '''

    def split(nums, max):
        count, s = 1, 0
        for i in range(len(nums)):
            if s + nums[i] > max:
                count += 1
                s = nums[i]
            else:
                s += nums[i]
        return count

    l, r = max(nums), sum(nums) + 1  # [l, r)
    while l < r:
        mid = l + (r - l) // 2
        n = split(nums, mid)
        if n == m:
            r = mid
        elif n < m:  # 说明切割的子数组数少, 产生的结果是子数组和偏大, 因此要调整右边界减小sum值
            r = mid
        elif n > m:  # 说明切割的子数组多, 产生的结果是子数组和偏小, 因此要调整左边界,增加sum值
            l = mid + 1
    return l


if __name__ == "__main__":
    import math

    # print(bin_search([1, 2, 5, 8, 12, 16], 8))
    # print(bin_search([1, 2, 5, 8, 12, 16], 6))
    # print(bin_search([1, 2, 5, 8, 12, 16], 16))
    # print(right_bound([1, 2, 2, 2, 3], 2))
    # print(left_bound([1, 2, 2, 2, 3], 2))
    # print(left_bound([1, 2, 2, 2, 3], 4))
    # print(right_bound([1, 2, 2, 2, 3], 4))
    # print(right_bound([1, 2, 2, 2, 3], 2))

    print(min_eating_speed([312884470], 2))
    # print(ship_within_days([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 5))

    # print(left_floor([1, 2, 2, 2, 3], 2))  # 1
    # print(left_floor([3, 3, 3], 2))  # -1
    # print(left_floor([1, 2, 5, 8, 12, 16], 8))  # 3
    # print(left_floor([1, 2, 5, 8, 8, 8, 8, 12, 16], 8))  # 3
    # print(left_floor([1, 2, 2, 3, 7, 9], 4))  # 3
    # print(left_floor([1, 2, 2, 3, 7, 9], 8))  # 4
    # print('\n')
    #
    # # print('\n')
    # print(right_cell([1, 1, 1], 2))  # -1
    # print(right_cell([1, 2, 2, 2, 3], 2))  # 3
    # print(right_cell([1, 2, 5, 8, 12, 16], 8))  # 3
    # print(right_cell([1, 2, 5, 8, 8, 8, 12, 16], 8))  # 5
    # print(right_cell([1, 2, 2, 3, 7, 9], 5))  # 4

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值