建立“二分查找”的通用模型

案例

[5, 7, 7, 8, 8, 10]

返回非递减数组中第一个 ≥8 的数的位置,如果所有数都<8,返回数组长度

暴力做法:遍历每个数,询问是否 ≥8? 时间复杂度 O(n)

二分查找的模型

红蓝染色法:约定如下

≥ target 表示在 target 右侧标记为蓝色

< target 表示在 target 左侧标记为红色

1. 左闭右闭

func lowerBound(nums []int, target int) int {
    left := 0
    right := len(nums) - 1
    // 闭区间[left, right]
    for left <= right {  // 区间不为空,继续轮询查询目标
        // right - left 防止溢出
        mid := left + (right - left) / 2
        /*
        循环不变量(即通过这次循环可以得知的具体不变的信息)
        nums[left] < target
        nums[right] >= target
        */
        if nums[mid] >= target {
            // 范围缩小到[left, mid-1]
            right = mid - 1
        } else {
            // 范围缩小到(mid+1, right)
            left = mid + 1
        }
    }

    // 此时 left 等于 right + 1
    // 因为 nums[left - 1] < target 且 nums[left] >= target,所以答案是 left  / right + 1
    return left
}

2. 左闭右开

func lowerBound(nums []int, target int) int {
    left := 0
    right := len(nums)
    // 左闭右开[left, right)
    for left < right {  // 区间不为空,继续轮询查询目标
        // right - left 防止溢出
        mid := left + (right - left) / 2
        /*
        循环不变量(即通过这次循环可以得知的具体不变的信息)
        nums[left] < target
        nums[right] >= target
        */
        if nums[mid] >= target {
            // 范围缩小到[left, mid)
            right = mid
        } else {
            // 范围缩小到(mid+1, right)
            left = mid + 1
        }
    }

    // 此时 left 等于 right
    // 因为 nums[left - 1] < target 且 nums[left] >= target,所以答案是 left  / right
    // 循环结束后 left == right
    return left
}

3. 左开右开


func lowerBound(nums []int, target int) int {
    left := -1
    right := len(nums)
    // 开区间 (left, right)
    for left + 1 < right {  // 区间不为空,继续轮询查询目标
        // right - left 防止溢出
        mid := left + (right - left) / 2
        /*
        循环不变量(即通过这次循环可以得知的具体不变的信息)
        nums[left] < target
        nums[right] >= target
        */
        if nums[mid] >= target {
            // 范围缩小到(left, mid)
            right = mid
        } else {
            // 范围缩小到(mid, right)
            left = mid
        }
    }

    // 此时 left 等于 right - 1
    // 因为 nums[right - 1] < target 且 nums[right] >= target,所以答案是 right
    return right
}

4. 总结

1. 左闭右闭

left=0, right=len(nums)-1

区间不为空的循环条件:left ≤ right

mid ≥ target: right = mid - 1

mid < target: left = mid + 1

2. 左闭右开


left = 0, right = len(nums)

区间不为空的循环条件: left < right

mid ≥ target: right = mid

mid < target: left = mid + 1

3. 左开右开


left = -1, right = len(nums)

区间不为空的循环条件:left + 1 < right

mid ≥ target: right = mid

mid < target: left = mid

举一反三

1. 获取第一个大于等于 target

ans = f(x)

2. 获取第一个大于 target

ans = f(x+1)

3. 获取最后一个小于 target

ans = f(x) - 1

4. 获取最后一个小于等于 target

ans = f(x+1)-1


func TestLowerBound(t *testing.T) {
	nums := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

	target := 5
	// 找第一个大于等于 target 的下标  5

	fmt.Println(lowerBound(nums, target))

	// 找第一个大于 target 的下标  6

	fmt.Println(lowerBound(nums, target+1))

	// 找最后一个小于等于 target 的下标 5

	fmt.Println(lowerBound(nums, target+1) - 1)

	// 找最后一个小于 target 的下标  4

	fmt.Println(lowerBound(nums, target) - 1)
}

题目

以如下题目,来分析二分查找的多种模型(固定模式)

34. 在排序数组中查找元素的第一个和最后一个位置

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:

输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:

输入:nums = [], target = 0
输出:[-1,-1]

使用二分查找模型解决相关题目 ➡️

参考

【0x3f-灵神基础算法精讲 04】

posted @ 2024-09-14 15:28  JonPan  阅读(13)  评论(0编辑  收藏  举报