二分查找及python实现
1. 最基本的二分查找
二分查找是very very经典的算法,它最简单的题面如图所示。
对于有过一定编程基础的同学,这样的题目可以说是探囊取物。总之,二分查找是一种时间复杂度为O(log n)的查找算法,使用场景一般是有序数组及其变形的查找,这里可以给出二分查找的三个模板。
模板1
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left + 1 < right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid
else:
right = mid
if nums[left] == target:
return left
if nums[right] == target:
return right
# 最后剩余2个数需要做后处理
return -1
模板2
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
if nums[mid] < target:
left = mid + 1
else:
right = mid - 1
# 最后不需要做后处理
return -1
模板3
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid
if nums[left] == target:
return left
# 最后需要对1个数做后处理
return -1
以上3个模板都比较好理解,但是就应用范围来说模板1>模板2>模板3。
2. 搜索插入位置
接下来的是二分查找的一个简单变形,题面如下所示。
这题相对于二分查找的变形在于目标值可能不出现在这个排序数组,所以其实需要找到目标值的“左邻右舍”,例如示例1,就需要找到nums[left]=3, nums[right]=5;示例2,就需要找到nums[left]=1, nums[right]=3,而模板1恰好可以找到这样的一对“左邻右舍”,而且比较好理解,python代码如下。(其实这个代码有可以优化的地方,此处只是选择最接近模板的版本)
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
if nums[left] > target:
return 0
if nums[right] < target:
return right + 1
# 首先需要排除目标值小于最小值,大于最大值的特殊情况,这样可以保证以下一定可以找到“左邻右舍”
while left + 1 < right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid
else:
right = mid
# 可以保证left和right一定是目标值的“左邻右舍”
# 如果nums[left]==target,就返回left;如果nums[left]<target<=nums[right],就返回right
if nums[left] == target:
return left
return right
这题用模板2也比较好理解,只需要找到最小的大于等于目标值的数的索引即可,python代码如下。
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
l = len(nums)
ans, left, right = l, 0, l - 1
# ans为最终返回的索引,如果没有大于等于目标值的数,返回的索引正是l+1
while left <= right:
mid = (left + right) // 2
if target <= nums[mid]:
# 每次只要出现大于等于target的数就更新
ans = mid
right = mid - 1
else:
left = mid + 1
return ans
3. 寻找旋转排序数组中的最小值
接下来是二分查找的进阶变形,这里的使用场景是有序数组的变形,同时也没有了所谓的目标值,题面如下所示。
这道题不同的地方在于是一个排序数组的变形,同时也没有所谓的目标值,仔细分析这道题,其实我们的目标是在寻找一个最小的数,而二分查找的本质就是左指针和右指针不断向内移动,直到找到了最小的数。在这道题中,我们就可以把右指针指向的数当成一个所谓的“目标值”,如果中间值比“目标值”大,那么左指针右移;如果中间值比“目标值”小,那么右指针左移(目标值也随之更新),根据模板3修改的python代码如下所示。
class Solution:
def findMin(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
while left + 1 < right:
mid = (left + right) // 2
if nums[mid] < nums[right]:
right = mid
else:
left = mid
return min(nums[left], nums[right])
这题用模板2也是比较好理解的,其实模板2就是在移动的过程中,就加入了判断,python代码如下所示。
class Solution:
def findMin(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
if nums[0] <= nums[right]:
return nums[0]
if nums[0] > nums[1]:
return nums[1]
# 这里的判断是为了防止在之后判断nums[mid]是否为最小值时出现越界,针对nums数组长度为1,数组长度为2(mid-1会越界),以及不发生旋转(不发生旋转的话最后一次mid会停留在nums数组的最后一位,mid+1会越界)
while left <= right:
mid = (left + right) // 2
if nums[mid] < nums[mid - 1]:
return nums[mid]
if nums[mid] > nums[mid + 1]:
return nums[mid + 1]
# 如果nums[mid]比它的前一位小,那么这一位就是最小值;如果nums[mid]比它的后一位大,那么后一位就是最小值
if nums[mid] < nums[right]:
right = mid - 1
else:
left = mid + 1
变形
在原题的基础上,又发生了一些变化,题面如下所示。
这里的变化就是场景由单调上升数组的旋转变成单调不下降数组的旋转,同时会出现重复的值。当nums[mid]小于nums[right]时,就说明nums[mid]的右半部分可以忽略,当nums[mid]大于nums[right]时,就说明nums[mid]的左半部分可以忽略。那么,当nums[mid]等于nums[right]时呢?由于存在重复元素,所以不能确定,最小值是在nums[mid]的左侧还是右侧,就比如2 1 2 2 2
,2 2 2 1 2
,所以不能莽撞地忽略某一部分,但是可以知道的是,因为nums[mid]和nums[right]相同,所以nums[right]一定存在一个替代品,因此右端点可以忽略,可以将右指针左移1位。所以,如果用模板1的话,python代码如下。
class Solution:
def findMin(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
while left + 1 < right:
mid = (left + right) // 2
if nums[mid] < nums[right]:
right = mid
else:
left = mid
else:
right -= 1
return min(nums[left], nums[right])
那么在这里,我们也试着用一下模板3吧。
class Solution:
def findMin(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] > nums[right]:
left = mid + 1
elif nums[mid] < nums[right]:
right = mid
else:
right = right - 1
return nums[left]
4. 搜索旋转排序数组
最后是最基本的二分查找的变形了,先来看一看题吧~
这题的关键在于每次nums[mid]和target在比较时,要判断出target是会在nums[mid]的左边还是右边。那么细想,无论mid在哪个位置,它的左半部分和右半部分中,至少有一部分是有序的,那么我们就可以根据这一部分去判断target在nums[mid]的左边还是右边,话不多说,直接上模板1的python代码。
class Solution:
def search(self, nums: List[int], target: int) -> int:
l = len(nums)
if l == 0:
return -1
left, right = 0, l - 1
while left + 1 < right:
mid = (left + right) // 2
if nums[0] < nums[mid]:
if nums[0] <= target < nums[mid]:
right = mid
else:
left = mid
else:
if nums[mid] < target <= nums[l - 1]:
left = mid
else:
right = mid
if nums[left] == target:
return left
if nums[right] == target:
return right
return -1
没错,既然旋转了,那就来个变形呗~(是的,数组的数又要重复啦)
这里,大家可以看一下“寻找旋转排序数组最小值”的变形,就可以理解了,其实就是在原先的基础上加入了右指针左移的过程,相信大家一定懂得~
class Solution:
def search(self, nums: List[int], target: int) -> bool:
left, right = 0, len(nums) - 1
if right == -1:
return False
while left + 1 < right:
mid = (left + right) // 2
if nums[mid] < nums[right]:
if nums[mid] < target <= nums[right]:
left = mid
else:
right = mid
elif nums[mid] > nums[right]:
if nums[left] <= target < nums[mid]:
right = mid
else:
left = mid
else:
right -= 1
return nums[left] == target or nums[right] == target
祝大家coding愉快!