Skip to content

Latest commit

 

History

History
515 lines (370 loc) · 16 KB

专题-二分查找.md

File metadata and controls

515 lines (370 loc) · 16 KB

Index


两数相除 (LeetCode, Medium, No.0029, 2021-10)

位运算 二分查找

问题简述
不使用乘法、除法和 mod 运算符,返回两数相除的整数部分,如 10/3 返回 3。
题目描述
给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和 mod 运算符。

返回被除数 dividend 除以除数 divisor 得到的商。

整数除法的结果应当截去(truncate)其小数部分,例如:truncate(8.345) = 8 以及 truncate(-2.7335) = -2

示例 1:
    输入: dividend = 10, divisor = 3
    输出: 3
    解释: 10/3 = truncate(3.33333..) = truncate(3) = 3
示例 2:
    输入: dividend = 7, divisor = -3
    输出: -2
    解释: 7/-3 = truncate(-2.33333..) = -2

提示:
    被除数和除数均为 32 位有符号整数。
    除数不为 0。
    假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−2^31,  2^31 − 1]。本题中,如果除法结果溢出,则返回 2^31 − 1。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/divide-two-integers
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:二分查找
class Solution:
    def divide(self, dividend: int, divisor: int) -> int:
        """"""
        INT_MIN, INT_MAX = -2 ** 31, 2 ** 31 - 1

        # 按照题目要求,只有一种情况会溢出
        if dividend == INT_MIN and divisor == -1:
            return INT_MAX

        sign = (dividend > 0 and divisor > 0) or (dividend < 0 and divisor < 0)

        # 核心操作
        def div(a, b):
            if a < b:
                return 0

            cnt = 1
            tb = b
            while (tb + tb) <= a:
                cnt += cnt
                tb += tb

            return cnt + div(a - tb, b)

        ret = div(abs(dividend), abs(divisor))
        return ret if sign else -ret

核心操作说明,以 60 / 8 为例:

第一轮 div(60, 8): 8 -> 32 时停止,因为 32 + 32 > 60,返回 4
第二轮 div(28, 8): 8 -> 16 时停止,因为 16 + 16 > 28,返回 2
第三轮 div(8, 8):  8 -> 8  时停止,因为 8  +  8 >  8,返回 1
第三轮 div(0, 8):  因为 0 < 8,返回 0

因此结果为 1 + 2 + 4 = 7

将数据流变为多个不相交区间 (LeetCode, Hard, No.0352, 2021-10)

二分查找 模拟

问题简述
给你一个由非负整数 a1, a2, ..., an 组成的数据流输入,请你将到目前为止看到的数字总结为不相交的区间列表。

实现 SummaryRanges 类:
    SummaryRanges() 使用一个空数据流初始化对象。
    void addNum(int val) 向数据流中加入整数 val 。
    int[][] getIntervals() 以不相交区间 [starti, endi] 的列表形式返回对数据流中整数的总结。

进阶:如果存在大量合并,并且与数据流的大小相比,不相交区间的数量很小,该怎么办?

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/data-stream-as-disjoint-intervals
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

“进阶”:在插入过程中完成合并操作;

示例
输入:
    ["SummaryRanges", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals"]
[[], [1], [], [3], [], [7], [], [2], [], [6], []]
输出:
    [null, null, [[1, 1]], null, [[1, 1], [3, 3]], null, [[1, 1], [3, 3], [7, 7]], null, [[1, 3], [7, 7]], null, [[1, 3], [6, 7]]]

解释:
    SummaryRanges summaryRanges = new SummaryRanges();
    summaryRanges.addNum(1);      // arr = [1]
    summaryRanges.getIntervals(); // 返回 [[1, 1]]
    summaryRanges.addNum(3);      // arr = [1, 3]
    summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3]]
    summaryRanges.addNum(7);      // arr = [1, 3, 7]
    summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3], [7, 7]]
    summaryRanges.addNum(2);      // arr = [1, 2, 3, 7]
    summaryRanges.getIntervals(); // 返回 [[1, 3], [7, 7]]
    summaryRanges.addNum(6);      // arr = [1, 2, 3, 6, 7]
    summaryRanges.getIntervals(); // 返回 [[1, 3], [6, 7]]

提示:
    0 <= val <= 10^4
    最多调用 addNum 和 getIntervals 方法 3 * 10^4 次

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/data-stream-as-disjoint-intervals
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路1:暴力求解(Python)
  • 每次 getIntervals 时,先对数组排序,然后依次找出每个不相交的区间;
class SummaryRanges:

    def __init__(self):
        self.ls = []

    def addNum(self, val: int) -> None:
        """"""
        self.ls.append(val)

    def getIntervals(self) -> List[List[int]]:
        """"""
        ls = sorted(self.ls)
        ret = []
        l = ls[0]
        for i in range(1, len(ls)):
            if ls[i] - ls[i-1] > 1:  # 判断是否需要合并
                ret.append([l, ls[i-1]])
                l = ls[i]
        
        ret.append([l, ls[-1]])

        return ret
思路2:分情况讨论(模拟,Python)
  • 明确每次 addNum 时,区间会发生那些变化:

    • 情况1:存在一个区间 [l, r] 满足 l <= val <= r
    • 情况2:存在一个区间 [l, r] 满足 r + 1 == val
    • 情况3:存在一个区间 [l, r] 满足 l - 1 == val
    • 情况4:存在两个个区间 [l0, r0][l1, r1] 满足 r0 + 1 == val == l1 - 1,即加入 val 后,会合并为一个区间 [l0, r1]
    • 情况5:以上均不满足,加入后 val 单独成为一个区间;
  • 这里使用了 SortedDict 降低了代码难度,也可以使用一个有序数组来模拟;

  • 时间复杂度: addNum O(NlgN)getIntervals O(N)

  • 空间复杂度: O(N)

from sortedcontainers import SortedDict
from bisect import bisect_right, bisect_left

class SummaryRanges:

    def __init__(self):
        self.ret = SortedDict()  # {l: r}
        # 加入首尾两个哨兵,防止区间不存在的情况,这样会徒增很多判断
        self.ret[-10] = -10
        self.ret[10010] = 10010

    def addNum(self, val: int) -> None:
        ret = self.ret
        L = list(self.ret.keys())
        R = list(self.ret.values())

        # 二分找出 val 的相邻区间
        idx = bisect_left(L, val)  # idx = ret.bisect_left(val)
        pre = L[idx - 1], R[idx - 1]
        nxt = L[idx], R[idx]

        if pre[0] <= val <= pre[1] or nxt[0] <= val <= nxt[1]:  # 情况1
            pass
        elif pre[1] + 1 == val == nxt[0] - 1:  # 情况4
            ret.pop(nxt[0])
            ret[pre[0]] = nxt[1]
        elif pre[1] + 1 == val:  # 情况2
            ret[pre[0]] = val
        elif nxt[0] - 1 == val:  # 情况3
            ret.pop(nxt[0])
            ret[val] = nxt[1]
        else:  # 情况5
            ret[val] = val

    def getIntervals(self) -> List[List[int]]:
        return list(self.ret.items())[1:-1]  # 去除两个哨兵
  • 上面的代码中用到了 SortedDict,示例:
>>> d = SortedDict()
>>> d[3] = 33
>>> d[2] = 22
>>> d[4] = 44
>>> d[6] = 66
>>> d[7] = 77
>>> d
SortedDict({2: 22, 3: 33, 4: 44, 6: 66, 7: 77})
>>> d.bisect_left(4)  # 二分查找返回的是插入位置
2
>>> d.bisect_right(4)  # left 和 right 的区别是如果插入值已存在,则 left 会插到前面,right 会插到后面
3

山峰数组的顶部 (剑指Offer2, Easy, No.0069, 2021-10)

二分查找

问题简述
找出山脉数组中山峰的下标(保证给出的数组是一个山脉数组)
思路
  • N[mid] > N[mid+1] 时,山峰必在左侧;反之,在右侧;
  • 因为从中间划分后,左右分别满足相反的性质,因此可以使用二分查找;
题目描述
符合下列属性的数组 arr 称为 山峰数组(山脉数组) :

    arr.length >= 3
    存在 i(0 < i < arr.length - 1)使得:
        arr[0] < arr[1] < ... arr[i-1] < arr[i]
        arr[i] > arr[i+1] > ... > arr[arr.length - 1]
    
    给定由整数组成的山峰数组 arr ,返回任何满足 arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1] 的下标 i ,即山峰顶部。

示例 1:
    输入:arr = [0,1,0]
    输出:1
示例 2:
    输入:arr = [1,3,5,4,2]
    输出:2
示例 3:
    输入:arr = [0,10,5,2]
    输出:1
示例 4:
    输入:arr = [3,4,5,1]
    输出:2
示例 5:
    输入:arr = [24,69,100,99,79,78,67,36,26,19]
    输出:2

提示:
    3 <= arr.length <= 10^4
    0 <= arr[i] <= 10^6
    题目数据保证 arr 是一个山脉数组
 
进阶:很容易想到时间复杂度 O(n) 的解决方案,你可以设计一个 O(log(n)) 的解决方案吗?

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/B1IidL
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
Python
class Solution:
    def peakIndexInMountainArray(self, arr: List[int]) -> int:
        """"""
        left, right = 1, len(arr) - 2

        ans = 0
        while left <= right:
            mid = left + (right - left) // 2
            if arr[mid] > arr[mid + 1]:  # 山峰在左侧
                ans = mid  # 目前已知 mid 位置的值是最大的,因为保证 arr 是一个山脉数组,所以一定会来到这个分支
                right = mid - 1
            else:  # 山峰在右侧
                left = mid + 1

        return ans

排列硬币 (LeetCode, Easy, No.0441, 2021-10)

二分查找 数学

问题简述
你总共有 n 枚硬币,并计划将它们按阶梯状排列。对于一个由 k 行组成的阶梯,其第 i 行必须正好有 i 枚硬币。阶梯的最后一行 可能 是不完整的。

给你一个数字 n ,计算并返回可形成 完整阶梯行 的总行数。

示例 1:
    输入:n = 5
    输出:2
    解释:因为第三行不完整,所以返回 2 。

提示:
    1 <= n <= 2^31 - 1

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/arranging-coins
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路1:二分查找
  • 因为时间复杂度为 O(logN),所以直接在 [1, n] 的范围里找即可
class Solution:
    def arrangeCoins(self, n: int) -> int:
        left, right = 1, n
        while left < right:
            mid = (left + right + 1) // 2
            if mid * (mid + 1) <= 2 * n:
                left = mid
            else:
                right = mid - 1
        return left
思路2:数学
  • 解方程 $(1+x)*x/2 = n$
  • 去掉小于 0 的解,保留:$x=(-1+\sqrt{8n+1})/2$
class Solution:
    def arrangeCoins(self, n: int) -> int:
        return int((-1 + (8 * n + 1) ** 0.5) / 2)

搜索旋转排序数组 (LeetCode, Medium, No.0033, 2021-10)

二分查找

问题简述
在一个旋转过的有序数组中搜索某值,若存在返回下标,否则返回 -1。
思路
  • “二分”的本质是两段性,而不是单调性;即只要二分后,左边满足某个性质,右边不满足某个性质,即可使用二分;

  • 比如本题二分后,有前半段满足 >= nums[0],而后半段不满足;

    LogicStack-LeetCode/33.搜索旋转排序数组(中等)

题目描述
整数数组 nums 按升序排列,数组中的值 互不相同 。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

示例 1:
    输入:nums = [4,5,6,7,0,1,2], target = 0
    输出:4
示例 2:
    输入:nums = [4,5,6,7,0,1,2], target = 3
    输出:-1
示例 3:
    输入:nums = [1], target = 0
    输出:-1
 

提示:
    1 <= nums.length <= 5000
    -10^4 <= nums[i] <= 10^4
    nums 中的每个值都 独一无二
    题目数据保证 nums 在预先未知的某个下标上进行了旋转
    -10^4 <= target <= 10^4
 
进阶:你可以设计一个时间复杂度为 O(log n) 的解决方案吗?

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
二分查找(Python)
  • 将数组从中间分开成左右两部分时,一定有一部分的数组是有序的。
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        if not nums:
            return -1

        L = len(nums) - 1
        l, r = 0, L
        while l <= r:
            mid = l + (r - l) // 2  # 中点下标

            if nums[mid] == target:
                return mid

            if nums[0] <= nums[mid]:  # [0, mid) 是有序的
                # 如果目标在[0, mid),则将搜索范围缩小到 [0,mid-1],反之 [mid+1,L]
                if nums[0] <= target < nums[mid]:
                    r = mid - 1
                else:
                    l = mid + 1
            else:  # (mid, L] 是有序的
                # 同理,如果目标在(mid, L],则将搜索范围缩小到 [mid+1,L],反之 [0,mid-1]
                if nums[mid] < target <= nums[L]:
                    l = mid + 1
                else:
                    r = mid - 1

        return -1