二分搜索大家都很熟悉,首先我们先来看看基本框架
func binarySearch(nums []int, target int) int {
    left, right := 0, ...
    for ... {
        mid := left + (right-left)/2
        if nums[mid] == target {
            ...
        } else if nums[mid] < target {
            left = ...
        } else if nums[mid] > target {
            right = ...
        }
    }
    return ...
}
 
看完代码之后我们会发现:
- 数组得是有序的才能成立
 - 在存在两个及以上的答案时,只能找出一个,至于下标的情况我觉得可以自定义
 - 如果数组长度为偶数,初始化时mid在中间偏左的那一格
 
接下来在实战中看:
寻找一个数
这可以说是最简单的内容
func binarySearch(nums []int, target int) int {
    left := 0 //
    right := len(nums) - 1 
    for left <= right {
        mid := left + (right - left) / 2 
        if nums[mid] == target { 
            return mid
        } else if nums[mid] < target { 
            left = mid + 1 // [mid + 1, right]
        } else if nums[mid] > target { // 目标值在左半部分,注意
            right = mid - 1 // [left, mid - 1]
        }
    }
    return -1 // 未找到
}
 
这里需要注意一点,为何是 for left <= right 而不是 for left < right ?
我们可以先从最极端的情况入手,假设要寻找的数在最后一个,那么在前一步应该是 left = right - 1, mid = left ,然后 left = mid + 1 = right,在进行循环时,循环就会进不去。
从理论上解释,因为在之前设定值时,数组为闭区间 [left, right] ,所以得保证边界值,也就是说,循环的条件设定和搜索区间设定是相关联的
寻找左侧边界
那么接下来看一下右侧开区间的做法:
func leftBound(nums []int, target int) int {
    left := 0
    right := len(nums)
    for left < right {
        mid := left + (right - left) / 2
        if nums[mid] == target {
            right = mid
        } else if nums[mid] < target {
            left = mid + 1 // [mid + 1, right)
        } else if nums[mid] > target {
            right = mid // [left, mid)
        }
    }
    return left
}
 
不难发现,随着区间的修改,在对应条件下的操作也会对应转换。
在这里阐述一点我的个人想法,二分查找的最大特点在于区间设定,而在对应条件下做出的操作有点像递归,基本盘并没有改变
寻找右侧边界
func right_bound(nums []int, target int) int {
    left, right := 0, len(nums)
    
    for left < right {
        mid := left + (right-left)/2
        if nums[mid] == target {   
            left = mid + 1        
        } else if nums[mid] < target {
            left = mid + 1
        } else if nums[mid] > target {
            right = mid
        }
    }
    return left - 1  
}
 
这里面的精华在于
if nums[mid] == target {   
	 left = mid + 1 
 
他并没有直接的收网,而是继续直到最右侧的诞生,这也得益于mid的设置,可以把他看成以left为基准向前进
实战
我们来看力扣的34. 在排序数组中查找元素的第一个和最后一个位置
直接把方法摆上既可以解决
func searchRange(nums []int, target int) []int {
    left := leftBound(nums, target)
    right := rightBound(nums, target)
    return []int{left, right}
}
func leftBound(nums []int, target int) int {
    left, right := 0, len(nums)-1
    for left <= right {
        mid := left + (right - left) / 2
        if nums[mid] < target {
            left = mid + 1
        } else if nums[mid] > target {
            right = mid - 1
        } else if nums[mid] == target {
            // 别返回,锁定左侧边界
            right = mid - 1
        }
    }
    // 判断 target 是否存在于 nums 中
    if left < 0 || left >= len(nums) {
        return -1
    }
    // 判断一下 nums[left] 是不是 target
    if nums[left] == target {
        return left
    }
    return -1
}
func rightBound(nums []int, target int) int {
    left, right := 0, len(nums)-1
    for left <= right {
        mid := left + (right - left) / 2
        if nums[mid] < target {
            left = mid + 1
        } else if nums[mid] > target {
            right = mid - 1
        } else if nums[mid] == target {
            // 别返回,锁定右侧边界
            left = mid + 1
        }
    }
    // 判断 target 是否存在于 nums 中
    // if left - 1 < 0 || left - 1 >= len(nums) {
    //     return -1
    // }
    if right < 0 || right >= len(nums) {
        return -1
    }
    if nums[right] == target {
        return right
    }
    return -1
}
 
当然也有另外一种解法,力扣显示这种时间复杂度更低
 



















