基于有序数组的二分查找法
查找问题是计算机中非常重要的一类基础问题,也是我们生活中常见的问题。
在我们的生活中,要想方便地“找东西”,就要在“存东西”的时候多花点心思。例如:我们在“存东西”的时候,会有意识地“分门别类”放好。在我们的计算机中,文件夹呈现的树形结构,就是一种分类存放的策略,我们想要找张三的资料,肯定不会去李四的文件夹下查找。我们去图书馆要找一本《算法》的书,我们肯定不会去标明“文学”的那个书架上找。
学习过 Java 语言的朋友们一定知道 hash 表,我们可以给放进集合中的对象计算一个 hash 值,以后取出来的时候通过计算 hash 值得到之前放进去的对象,这也是一种存放数据和读取数组的策略的策略。
那么“二分查找法”要求数据是怎么存的呢?很简单,只要数据是顺序摆放的,我们就可以用“二分查找算法”。即二分查找法的使用前提:对于有序的数列才能使用二分查找法,如果数组无序,就不能使用二分查找法。
“二分查找法”能够根据数组的索引大大减少每次查找所需的比较次数。因为每次查找都能缩短一半的查找区间。
二分查找法的思想在 1946 年就被提出来了。但是第 1 个没有 bug 的二分查找法在 1962 年才出现。这个 bug 就在于查找两个数的中间数,如何避免为了求中间数而导致的整形溢出,避免这个 bug 的方法我们在介绍归并排序时介绍过。
二分查找算法要求数组是顺序数组,但是在我们的生活中,还有这样的场景:我们购买的物品是一件一件买回来的,有时,我们还会丢弃一些物品,那么是不是我们每增加或者丢弃一些物品的时候,都要将他们重新整理一下呢?
二分查找问题在 LeetCode 上的模板问题是第 704 题。
LeetCode 第 704 题:二分查找
传送门:704. 二分查找。
给定一个
n
个元素有序的(升序)整型数组nums
和一个目标值target
,写一个函数搜索nums
中的target
,如果目标值存在返回下标,否则返回-1
。示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9 输出: 4 解释: 9 出现在 nums 中并且下标为 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2 输出: -1 解释: 2 不存在 nums 中因此返回 -1
提示:
- 你可以假设
nums
中的所有元素是不重复的。n
将在[1, 10000]
之间。nums
的每个元素都将在[-9999, 9999]
之间。
下面是标准的二分查找代码:
Python 代码:
class Solution:
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
size = len(nums)
if size == 0:
return -1
l = 0
r = size - 1
while l <= r:
mid = l + (r - l) // 2
if nums[mid] == target:
return mid
elif nums[mid] > target:
# [1,2,3,4,5,6,7,8]
r = mid - 1
else:
l = mid + 1
return -1
二分查找法的模板,可以参考二分查找算法模板。自从我看到了这个模板以后,二分问题都这样思考,不会错。
Python 代码1:
class Solution:
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
size = len(nums)
if size == 0:
return -1
l = 0
r = size - 1
while l < r:
mid = l + (r - l + 1) // 2
if nums[mid] <= target:
# mid 在,因此,不能排除 mid
l = mid
else:
assert nums[mid] > target
# mid 不在,所以可以排除 mid
r = mid - 1
# 为了避免不存在的情况出现,最后要判断一下
if nums[l] == target:
return l
return -1
Python 代码2:
class Solution:
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
size = len(nums)
if size == 0:
return -1
l = 0
r = size - 1
while l < r:
mid = l + (r - l) // 2
if nums[mid] < target:
# mid 可以排除,所以要 + 1
l = mid + 1
else:
assert nums[mid] >= target
# mid 不能排除,所以至少是 mid 这个位置
r = mid
# 为了避免不存在的情况出现,最后要判断一下
if nums[l] == target:
return l
return -1
如何写好二分查找法的
在理解的基础上记住模板。
1、无脑地先写上 while l < r
作为循环可以继续下去的条件;
说明:这样写的好处是,我们就不必纠结应该返回 l
还是 r
了,此时返回 l
或者 r
都行 。 因为使用 l < r
,退出循环的时候,就一定满足 l == r
为 true
。不过这里要特别强调的一点是:如果你能确定给出的数组中一定包含给定的元素,那么直接返回 l
就可以了。否则,还要单独做判断。
2、无脑地先写上 mid = l + ((r - l) >> 1)
,根据循环体的情况,判断是否要修改成 mid = l + ((r - l + 1) >> 1)
;
说明:
Python 代码:下面取的是靠近左边界的中点
mid = l + ((r - l) >> 1)
Python 代码:下面取的是靠近右边界的中点
mid = l + ((r - l + 1) >> 1)
你可以拿数组只有 个元素的情况来理解这个事实,这一点一定要非常清楚,否则我们写出来的代码有可能陷入死循环。
3、逻辑上真正实现“二分”,我们不用写 个分支作判断,根据写好的分支判断第 步取中点是靠左还是靠右。
这一步的关键是要判断清楚中点是不是你要找的元素,如果是,就不能排除它,如果不是就可以排除它。
二分查找的 个变形问题
1、查找第一个值等于给定值的元素
Python 代码:
# 查找第一个值等于给定值的元素
def binary_search_1(nums, target):
size = len(nums)
l = 0
r = size - 1
while l < r:
mid = l + ((r - l) >> 1)
# 1,2,3,3,3,3,4,5
if nums[mid] < target:
l = mid + 1
else:
assert nums[mid] >= target
# mid 有可能是最优解
r = mid
if nums[l] != target:
return -1
return l
if __name__ == '__main__':
nums = [1, 2, 3, 3, 3, 3, 4, 5]
target = 6
result = binary_search_1(nums, target)
print(result)
2、查找最后一个值等于给定值的元素
Python 代码:
# 查找最后一个值等于给定值的元素
def binary_search_2(nums, target):
size = len(nums)
l = 0
r = size - 1
while l < r:
mid = l + ((r - l + 1) >> 1)
# 1,2,3,3,3,3,4,5
if nums[mid] > target:
r = mid - 1
else:
assert nums[mid] <= target
# mid 有可能是最优解
l = mid
if nums[l] != target:
return -1
return l
if __name__ == '__main__':
nums = [1, 2, 3, 3, 3, 3, 4, 5]
target = 3
result = binary_search_2(nums, target)
print(result)
3、查找第一个大于等于给定值的元素(floor
)
Python 代码:
# 查找第一个大于等于给定值的元素
def binary_search_3(nums, target):
size = len(nums)
l = 0
# 注意,r 向右边加了 1 位,因为 target 可能比 nums[-1] 还要大
r = size
while l < r:
mid = l + ((r - l) >> 1)
# 1,3,3,3,3,4,5
if nums[mid] >= target:
r = mid
else:
assert nums[mid] < target
# mid 有可能是最优解
l = mid + 1
# 特判
# if l == size - 1 and nums[-1] < target:
# return size
return l
def binary_search_3_1(nums, target):
size = len(nums)
l = 0
r = size - 1
while l < r:
mid = l + ((r - l) >> 1)
# 1,3,3,3,3,4,5
if nums[mid] >= target:
r = mid
else:
assert nums[mid] < target
# mid 有可能是最优解
l = mid + 1
# 特判
if l == size - 1 and nums[-1] < target:
return size
return l
if __name__ == '__main__':
nums = [1, 3, 3, 3, 3, 4, 5]
target = 3
result = binary_search_3_1(nums, target)
print(result)
4、查找最后一个小于等于给定值的元素( ceiling
)
Python 代码:
# 查找最后一个小于等于给定值的元素
def binary_search_4(nums, target):
size = len(nums)
# 注意,r 向左边减了 1 位,因为 target 可能比 nums[0] 还要小
l = -1
r = size - 1
while l < r:
mid = l + ((r - l + 1) >> 1)
# 1,3,3,3,3,4,5
if nums[mid] <= target:
# 不能排除 mid,并且最终值可能比 mid 大
l = mid
else:
assert nums[mid] > target
# mid 有可能是最优解
r = mid - 1
# 特判
# if l == size - 1 and nums[-1] < target:
# return size
return l
if __name__ == '__main__':
nums = [1, 3, 3, 3, 3, 4, 5]
target = 9
result = binary_search_4(nums, target)
print(result)
本文源代码
(本节完)