前言
Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky…
二分搜索的想法很直白,但细节很致命
本文假定所有数组均是升序排列的,且可能有重复元素。
二分搜索适用于寻找target、寻找左边界(计算小于target的元素个数)、寻找右边界(计算大于target的元素个数)。
二分搜索模板:
int binarySearch(int[] nums, int target) {
int left = 0, right = ...;
while(...) {
int mid = (right + left) / 2;
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = ...
} else if (nums[mid] > target) {
right = ...
}
}
return ...;
}
下面我们来讨论模板里的三个组成部分:
- 初始化i,j
- 何时结束循环
- 更新i,j值
初始化i,j
关于左右指针的初始化,有人喜欢j = N,有人喜欢j = N-1所以,我觉得首先要明确一下这两种写法下初始mid的物理位置。
假设数组长度为N,下面分奇偶讨论:
N/2 | (N-1)/2 | |
---|---|---|
奇数 | 正中间一个 | 正中间一个 |
偶数 | 中间偏右的 | 中间偏左的 |
建议自己随便取几个数试试。
循环条件
初始化右指针有两种,j = N与j = N-1 ;循环的条件亦有两种写法,i<j与i<=j,一共有四种组合:
- 若初始化j = N,则搜索区间为 [0,N),此时循环条件应为i<j,停止循环时i == j,[i,j) 为空
- 若初始化j = N,则搜索区间为 [0,N),此时循环条件若为i<=j,则可能越界
- 若初始化j = N-1,则搜索区间为 [0,N-1],此时循环条件可以是i<=j,停止循环时i+1 == j
- 若初始化j = N-1,则搜索区间为 [0,N-1],此时循环条件可以是i==j,停止循环时i == j,需手动检查一下停止时指向的数字
可以看出,是左闭右开还是左闭右闭只与右边界有关。极端情况下,j一直不动,只有i在向j逼近,需考虑j的位置;正常情况下,i与j都会更新,直到搜索区间为空。
要注意的是,有时候需在循环内部加上退出条件,否则会陷入死循环,至于循环中如何更新i与j的位置则视需求而定,具体的应用请阅读下面三栏代码
寻找一个数
def bi(nums,target):
left = 0
right = len(nums)-1
while (left <= right):
mid = (left + right) // 2
if (nums[mid] == target):
return mid
elif (nums[mid] < target):
left = mid + 1
elif (nums[mid] > target):
right = mid - 1
return -1
nums = [2,3,3,6,6,8,10]
target1 = 7
target2 = 10
print('Done! index = ',bi(nums,target1))
print('Done! index = ',bi(nums,target2))
#Output:Done! index = -1
#Output:Done! index = 6
等价于
def bi(nums,target):
left = 0
right = len(nums)
while (left < right):
mid = (left + right) // 2
if (nums[mid] == target):
return mid
elif (nums[mid] < target):
left = mid + 1
elif (nums[mid] > target):
right = mid - 1
return -1
nums = [2,3,3,6,6,8,10]
target1 = 7
target2 = 10
print('Done! index = ',bi(nums,target1))
print('Done! index = ',bi(nums,target2))
#Output:Done! index = -1
#Output:Done! index = 6
等价于
def bi(nums,target):
left = 0
right = len(nums)-1
while (left < right):
mid = (left + right) // 2
if (nums[mid] == target):
return mid
elif (nums[mid] < target):
left = mid + 1
elif (nums[mid] > target):
right = mid - 1
if nums[left] == target:
return left
else:
return -1
nums = [2,3,3,6,6,8,10]
target1 = 7
target2 = 10
print('Done! index = ',bi(nums,target1))
print('Done! index = ',bi(nums,target2))
我们需要 nums[mid]== target,所以,当nums[mid] != target 时直接将 mid 抛弃,缩放左右边界为mid±1。
寻找左边界
def bi(nums,target):
left = 0
right = len(nums)
#区间为[left,right)
while (left < right):
mid = (left + right) // 2
if (nums[mid] == target):
# 更新right 下一次在[left,mid)中搜索
# 若[left,mid)中的数均target了 left也会++ 最后也会和mid重合
# 指向第一个等于target的数
right = mid
elif (nums[mid] < target):
# 当前nums[mid]<target 直接舍弃掉
# 下一次在[mid+1,right)中搜索
left = mid + 1
elif (nums[mid] > target):
# 当前mid>target 更新right 下一次在[left,mid)中搜索
# 即使[left,mid)中没有target了 left也会++ 最后也会和mid重合
# 但此时指向的是第一个大于target的数
right = mid
return left
nums = [2,3,3,6,6,8,10]
target1 = 3
target2 = 7
print('Done! index = ',bi(nums,target1))
print('Done! index = ',bi(nums,target2))
#Output:Done! index = 1
#Output:Done! index = 5
寻找右边界
def bi(nums,target):
left = 0
right = len(nums)
#区间为[left,right)
while (left < right):
mid = (left + right) // 2
if (nums[mid] == target):
# 更新right 下一次在[mid+1,right)中搜索
# ij重合时指向最后一个等于target的数右边的数
left = mid + 1
elif (nums[mid] < target):
# 当前nums[mid]<target 下一次在[mid+1,right)中搜索
left = mid + 1
elif (nums[mid] > target):
# 当前nums[mid]>target 更新right 下一次在[left,mid)中搜索
# 若[left,mid)中所有数均小于target left也会++ 最后也会和mid重合
# 指向的是第一个不小于target的数
right = mid
return left - 1
nums = [2,3,3,6,6,8,10]
target1 = 3
target2 = 7
print('Done! index = ',bi(nums,target1))
print('Done! index = ',bi(nums,target2))
#Output:Done! index = 2
#Output:Done! index = 4