二分查找刷题总结

二分查找原理

特点:

  • 输入是个有序的元素列表
  • 要查找的元素包含在列表中,二分查找返回其位置
  • 对于包含n个元素的列表,用二分查找最多需要log2 n步

原理:

​ 首先定义两个变量指向有序列表的头(low)和尾(high),然后每次都检查中间的元素。如果猜小了,就修改low;如果猜大了,就修改high。

def binary_search(list, item):
	low = 0
	high = len(list) - 1
	
	while low <= high:
		mid = (low + high) / 2 #如果不是偶数,python将自动向下取整
		guess = list[mid]
		if guess == item:
			return mid
		if guess < item:
			low = mid + 1
		if guess > item:
			high = mid - 1
	
	return None

三个模板总结:

// 二分查找 --- [left, right]
    // 数组已经是有序的了!
    public static int binarySerach1(int[] nums, int target) {
        if (nums == null || nums.length == 0) {
            return -1;
        }
        int left = 0, right = nums.length-1;
        while (left <= right) {
            // 防止溢出 等同于(left + right)/2
            int mid = left + (right-left)/2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] > target) {
                // target 在左区间,所以[left, middle - 1]
                right = mid-1;
            } else {
                // target 在右区间,所以[middle + 1, right]
                left = mid+1;
            }
        }

        return -1;
    }

    // 二分查找 --- [left, right)
    // 数组已经是有序的了!
    int binarySearch2(int[] nums, int target){
        if(nums == null || nums.length == 0)
            return -1;
        // 定义target在左闭右开的区间里,即:[left, right)
        int left = 0, right = nums.length;
        // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
        while(left < right){
            int mid = left + (right - left) / 2;
            if(nums[mid] == target){
                return mid;
            }
            else if(nums[mid] < target) {
                //  target 在右区间,在[middle + 1, right)中
                left = mid + 1;
            }
            else {
                // target 在左区间,在[left, middle)中
                right = mid;
            }
        }

        // Post-processing:
        // End Condition: left == right
        if(left != nums.length && nums[left] == target) return left;
        return -1;
    }

    // 二分查找 --- (left, right)
    // 数组已经是有序的了!
    int binarySearch3(int[] nums, int target) {
        if (nums == null || nums.length == 0)
            return -1;

        int left = 0, right = nums.length - 1;
        while (left + 1 < right){
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                //  target 在右区间,在(middle, right)中
                left = mid;
            } else {
                // target 在左区间,在(left, middle)中
                right = mid;
            }
        }

        // Post-processing:
        // End Condition: left + 1 == right
        if(nums[left] == target) return left;
        if(nums[right] == target) return right;
        return -1;
    }

刷题经验:

  1. 二分查找的核心就是找到具有单调性的正确查找对象

  2. 当遇到指针不知道往哪移动的情况时,将二分查找退为普通遍历

  3. 对于有序数组,二分算法是最快的

  4. 选择二分查找方法,最重要的是要找到二分的分界,即分成了哪两个部分呢?

  5. 当数据规模达到 \(10^5\) 以上时,考虑使用二分算法更快

  6. 当二分查找的对象不在给定的数组中时,在答案的可能取值区间,进行二分查找

  7. 涉及到大范围\(10^9\)、第K个最大最小,K个数量这种很多都是二分解法

二分查找刷题

1. 69题:x的平方根--普通二分查找

  • 题目描述
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留整数部分 ,小数部分将被舍去 。

注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
  • 题目示例
示例 1:

输入:x = 4
输出:2
示例 2:

输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
  • 题目分析

给定的是非负整数 X ,可以把这个整数转换成一个有序的 0 ~ X的数组,这样就可以使用二分法来快速找到它的算术平方根了。由于是取下届作为X的算术平方根, 我们可以将每次遍历到\(mid *mid <x\)的位置存下来,这样当循环终止的时候,\(ans\) 总是停留在最接近X算术平方根的位置。

  • 算法实现
class Solution:
    def mySqrt(self, x: int) -> int:
        l, r, ans = 0, x, -1
        while l <= r:
            mid = (l + r) // 2
            if mid * mid <= x:
                ans = mid
                l = mid + 1
            else:
                r = mid - 1
        return ans
  • 复杂度分析
    • 时间复杂度:\(O(logn)\)
    • 空间复杂度: \(O(1)\)

2. 1760. 袋子里最少数目的球 --在答案的可能取值区间进行二分查找

  • 题目描述:
    给你一个整数数组 \(nums\) ,其中 \(nums[i]\) 表示第 i 个袋子里球的数目。同时给你一个整数 \(maxOperations\) 。
    你可以进行如下操作至多 \(maxOperations\) 次:
    选择任意一个袋子,并将袋子里的球分到 2 个新的袋子中,每个袋子里都有 正整数 个球。
    比方说,一个袋子里有 5 个球,你可以把它们分到两个新袋子里,分别有 1 个和 4 个球,或者分别有 2 个和 3 个球。
    你的开销是单个袋子里球数目的最大值 ,你想要最小化开销。

    请你返回进行上述操作后的最小开销。

  • 题目示例:

输入:nums = [2,4,8,2], maxOperations = 4
输出:2
解释:
-将装有 8 个球的袋子分成装有 4 个和 4 个球的袋子。[2,4,8,2] -> [2,4,4,4,2] 。
-将装有 4 个球的袋子分成装有 2 个和 2 个球的袋子。[2,4,4,4,2] -> [2,2,2,4,4,2] 。
-将装有 4 个球的袋子分成装有 2 个和 2 个球的袋子。[2,2,2,4,4,2] -> [2,2,2,2,2,4,2] 。
-将装有 4 个球的袋子分成装有 2 个和 2 个球的袋子。[2,2,2,2,2,4,2] -> [2,2,2,2,2,2,2,2] 。
装有最多球的袋子里装有 2 个球,所以开销为 2 并返回 2 。
  • 数据大小:
    \(1 <= nums.length <= 10^5\)
    \(1 <= maxOperations, nums[i] <= 10^9\)

  • 题目分析:
    我们无法在nums数组中进行二分查找找到答案,所以要考虑如何求得\(maxOperations\)呢?
    首先转换成判定问题,即:

    给定 \(maxOperations\)次操作次数,能否可以使得单个袋子里球数目的最大值不超过 y?

    由于当 y 增加时,操作次数会减少,因此 y 具有单调性,我们可以通过二分查找的方式得到答案。
    事实上,如果单个袋子里有 x 个球,那么操作次数即为:

    \[\lfloor \frac{x-1}{y} \rfloor \]

    其中 \(\lfloor x \rfloor\)表示将 x 进行下取整。因此我们需要找到最小的 y,使得:

    \[\sum_{x \in nums} \lfloor \frac{x-1}{y} \rfloor \leq maxOperations \]

    成立。

  • 算法实现:

class Solution(object):
    def minimumSize(self, nums, maxOperations):
        """
        :type nums: List[int]
        :type maxOperations: int
        :rtype: int
        """
        # 事先不知道答案,在答案的可能取值区间,进行二分查找
        left = 1
        right = max(nums)
        #print(right)
        ans = 0
        while left <= right:
            y = (left + right) // 2
            ops = 0
            for num in nums:
                ops += (num - 1) // y
            if ops <= maxOperations:
                ans = y
                right = y - 1
            else:
                left = y + 1
        return ans 
  • 复杂度分析:
  1. 时间复杂度:\(O(nlog\space n)\)
  2. 空间复杂度:\(O(1)\)

2. 436.寻找右区间

  • 问题描述:
    给你一个区间数组 intervals ,其中 \(intervals[i] = [start_i, end_i]\) ,且每个 \(start_i\) 都 不同 。

    区间 i 的右侧区间可以记作区间 j ,并满足 \(start_j >= end_i\) ,且 \(start_j\) 最小化 。

    返回一个由每个区间 i 的 右侧区间 的最小起始位置组成的数组。如果某个区间 i 不存在对应的 右侧区间 ,则下标 i 处的值设为 -1 。

  • 示例:

输入:intervals = [[3,4],[2,3],[1,2]]
输出:[-1,0,1]
解释:对于 [3,4] ,没有满足条件的“右侧”区间。
对于 [2,3] ,区间[3,4]具有最小的“右”起点;
对于 [1,2] ,区间[2,3]具有最小的“右”起点。
  • 方法一:二分查找
    对区间 \(intervals\) 的起始位置进行排序,并将每个起始位置 \(intervals[i][0]\)对应的索引 i 存储在数组 \(startIntervals\) 中,然后枚举每个区间 i 的右端点 \(intervals[i][1]\),利用二分查找来找到大于等于 \(intervals[i][1]\) 的最小值 \(val\) 即可,此时区间 i 对应的右侧区间即为右端点 \(val\) 对应的索引。
  • 算法实现
class Solution:
    def findRightInterval(self, intervals: List[List[int]]) -> List[int]:
        for i, interval in enumerate(intervals):
            interval.append(i)
        intervals.sort()

        n = len(intervals)
        ans = [-1] * n
        for _, end, id in intervals:
            i = bisect_left(intervals, [end])
            if i < n:
                ans[id] = intervals[i][2]
        return ans
  • 复杂度分析

时间复杂度:O(nlogn),其中 nn 为区间数组的长度。排序的时间为 O(nlogn),每次进行二分查找花费的时间为 O(logn),一共需要进行 nn 次二分查找,因此总的时间复杂度为 O(nlogn)。

空间复杂度:O(n),其中 n 为区间数组的长度。startIntervals 一共存储了 n 个元素,因此空间复杂度为 O(n)。

posted @ 2022-04-30 19:29  小李努力亿点  阅读(43)  评论(0)    收藏  举报