704. 二分查找
➡️题目链接
参考链接:https://www.programmercarl.com/0704.二分查找.html#_704-二分查找
第一种解法(暴力解法):
使用暴力解法,遍历数组。
class Solution: def search(self, nums: List[int], target: int) -> int: for i in range(len(nums)): if nums[i] == target: return i else: return -1
执行用时:40 ms, 在所有 Python3 提交中击败了76.04%的用户
内存消耗:16 MB, 在所有 Python3 提交中击败了50.88%的用户
通过测试用例:47 / 47
第二种解法(二分查找):
这道题目的前提是数组为有序数组,同时题目强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件。当看到题目满足上述条件时,就可以思考是不是可以使用二分法。
二分法查找涉及很多的边界条件,逻辑比较简单,但是边界条件容易混乱。到底是 while(left < right) 还是while(left <= right),到底是right=middle还是right = middle-1呢。
我们在写二分法时经常写乱,通常是对区间的定义没有搞清楚,区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
写二分法,区间的定义一般有两种,左闭右闭即[left, right],或者左闭右开即[left, right]。
下边我们用两种区间定义分别讲解两种不同的二分写法。
class Solution: # 左闭右闭 def search(self, nums: List[int], target: int) -> int: left, right = 0, len(nums) - 1 # 定义target在左闭右闭的区间里,[left, right] while left <= right: middle = left + (right - left) // 2 if nums[middle] > target: right = middle - 1 # target在左区间,所以[left, middle - 1] elif nums[middle] < target: left = middle + 1 # target在右区间,所以[middle + 1, right] else: return middle # 数组中找到目标值,直接返回下标 return -1 # 未找到目标值
class Solution: # 左闭右开 def search(self, nums: List[int], target: int) -> int: left, right = 0, len(nums) # 定义target在左闭右开的区间里,即:[left, right) while left < right: # 因为left == right的时候,在[left, right)是无效的空间,所以使用 < middle = left + (right - left) // 2 if nums[middle] > target: right = middle # target 在左区间,在[left, middle)中 elif nums[middle] < target: left = middle + 1 # target 在右区间,在[middle + 1, right)中 else: return middle # 数组中找到目标值,直接返回下标 return -1 # 未找到目标值
二分法第一种写法
第一种写法,我们定义target是在一个左闭右闭的区间里,也就是[left, right](这个很重要)。
区间的定义这就决定了二分法的代码应该怎么写,因为定义target在[left, right]区间,所以有如下两点:
1. while (left <= right)要使用<=,因为left==right是有意义的,所以用<=
2. if (nums[middle] > target) right要赋值为middle-1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是middle-1
例如在数组:1,2,3,4,7,9,10中查找元素2,如图所示:
如果定义target是在一个左闭右开的区间里,也就是[left, right),那么二分法的边界处理方式则截然不同。
有如下两点:
1. while (left < right),这里使用<,因为left == right在区间[left, right)是没有意义的
2. if (nums[middle] > target) right更新为middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]
在数组:1,2,3,4,7,9,10中查找元素2,如图所示:(注意和方法一的区别)
复杂度:
二分查找大大缩减了找的次数,举个例子,我们每次都猜中间的数字,那么那样每次就能至少排除一般的数字,最多只需要log2(n),则时间复杂度是O(logn),空间复杂度是O(1),因为使用了一个整数型变量mid来记录中间值。
总结
二分法是非常重要的基础算法,重要的是对区间定义的理解,在循环中要始终坚持根据查找区间的定义来做边界处理。区间的定义就是不变量,那么在循环中坚持根据查找区间的定义来做边界处理,就是循环不变量规则。
还要注意一个小细节,python中的//和/,在python中,/表示浮点整除法,返回浮点结果,也就是结果为浮点数;而在python中表示整数除法,返回大于结果的一个最大整数,也就是除法结果向下取整。
print("6 // 4 = " + str(6 // 4)) #结果:1 print("6 / 4 =" + str(6 / 4)) #结果:1.5