0. 学习资料来源
1. 学习笔记
这道题目的前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了。
二分法的主要思路是初始时,左右指针分别指向头部和尾部,
然后算出middle指针的值,和target比较,
如果大了,取左半边重复上述操作,如果小了,取右半边重复上述操作 直至while条件不满足,返回-1 或者找到target为止
主要思路如上,不是很复杂,但是其中细节的边界条件是很容易搞错。
循环不变量是区间的定义,代码的边界也要根据区间的定义写好。
Example1:找得到元素的例子
Example2.找不到元素的例子
二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如到底是 while(left < right)
还是 while(left <= right)
,到底是right = middle
呢,还是要right = middle - 1
呢?
大家写二分法经常写乱,主要是因为对区间的定义没有想清楚,区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
代码实现
二分法第一种写法,定义target在左闭右闭的区间里
第一种写法,我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right] (这个很重要非常重要)。
区间的定义这就决定了二分法的代码应该如何写,因为定义target在[left, right]区间,所以有如下两点:
- while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
- if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
# 左闭右闭 迭代法
# time complexity: O(logN)
# space complexity: O(1)
class Solution(object):
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
left = 0
right = len(nums)-1
while left <= right:
middle = (left+right)//2
potentialMatch = nums[middle]
if potentialMatch == target:
return middle
elif potentialMatch > target:
right = middle-1
elif potentialMatch < target:
left = middle + 1
return -1
# 左闭右闭 递归法
# time complexity: O(logN)
# space complexity: O(logN) ?not sure
class Solution(object):
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
def binarySearch(nums,target,left,right):
if left > right:
return -1
middle = (left+right)//2
potentialMatch = nums[middle]
if potentialMatch == target:
return middle
elif potentialMatch > target:
return binarySearch(nums,target,left,middle-1)
elif potentialMatch < target:
return binarySearch(nums,target,middle+1,right)
return binarySearch(nums,target,0,len(nums)-1)
二分法第二种写法
如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) ,那么二分法的边界处理方式则截然不同。
有如下两点:
- while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
- if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]
在数组:1,2,3,4,7,9,10中查找元素2,如图所示:(注意和方法一的区别)
# 左闭右开 迭代法
# time complexity: O(logN)
# space complexity: O(1)
class Solution(object):
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
left = 0
right = len(nums)
while left < right:
middle = (left+right)//2
potentialMatch = nums[middle]
if potentialMatch == target:
return middle
elif potentialMatch > target:
right = middle
else:
left = middle + 1
return -1
# 左闭右开 递归
# time complexity: O(logN)
# space complexity: O(1)
class Solution(object):
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
def binarySearch(nums,target,left,right):
if left >= right:
return -1
middle = (left+right)//2
potentialMatch = nums[middle]
if potentialMatch == target:
return middle
elif potentialMatch > target:
return binarySearch(nums,target,left,middle)
else:
return binarySearch(nums,target,middle+1,right)
return binarySearch(nums,target,0,len(nums))
2. 自己看到题目的第一想法
用二分法,循环的判断选择要处理的区间,因为之前做过,所以说知道要坚持区间不变量,没看解析之前自己写的代码如下:
# 第一遍自己做
# 方法一:左闭右闭区间
class Solution(object):
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
def binarySearch(nums,target,left,right):
# 因为是左闭右闭区间,所以这里的left=right的情况才有意义
while left <= right:
middle = (left+right)//2
potentialMatch = nums[middle]
if potentialMatch == target:
return middle
# 因为左闭右闭,所以这里right直接变成middle-1了
if potentialMatch > target:
right = middle -1
# 因为左闭右闭,所以这里left直接变成middle+1了
if potentialMatch < target:
left = middle + 1
return -1
return binarySearch(nums,target,0,len(nums)-1)
# 方法二:左闭右开区间
class Solution(object):
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
def binarySearch(nums,target,left,right):
while left < right:
middle = (left+right)//2
potentialMatch = nums[middle]
if potentialMatch == target:
return middle
if potentialMatch > target:
right = middle
if potentialMatch < target:
left = middle + 1
return -1
return binarySearch(nums,target,0,len(nums))
# 第一遍做的时候这里写错了,len(nums)写成了len(nums)-1
# 没有从头到尾贯彻区间不变量
自己写的有如下几个问题:
1. 总体来说是用的迭代法,但是抽象了另一个函数,其实是没必要的,要么递归,要么迭代
2. 没有彻底的贯彻区间不变量,在初始化left和right指针的时候也要坚持区间不变量,在左闭右开的时候,right初始要定义成len(nums)
3. 看完代码随想录想法
学习完代码随想录的文章和视频之后,我感觉整个代码的思路顿时清晰,不会因为<还是<=而迷茫,middle+1还是-1,还是不加不减而迷茫,在思考这些问题的时候,直接从定义的区间是什么来出发去思考,就不会写错。别忘了初始化指针的时候,就开始要遵循区间不变量了。
4.学习总结
这道题大概花了2个小时的时间,虽然写过,但是重新写的时候仍然在重要的细节上犯错,区间不变量原则贯彻不彻底。