目录
二分法是啥
最近研究二分法,感觉对里面的各种边界及左右指针的指向产生了深深的怀疑,有时候要加等号,有时候要左闭右开区间,有时候要左开右闭区间。就很头疼,就花时间仔细研究了一下,一点点写左右指针移动的情况,最终略有所懂,分享一下。
说到二分法,就想到了以前中小学的一个问题,有N个鸡蛋,有一个鸡蛋比较重,其他都是一样重,现在只有一个平衡称,怎么最快进行找到这个比较独特的鸡蛋,就是一次分成两堆,然后每次把重的那堆拿出来再进行比较。一直重复,直到找到。只需要logN次查找就可以找到,缩短了原来的N次实验。
二分查找看起来其实很简单,就是每次都分成一半,然后再去找,细节却是魔鬼,要怎么样给mid值变换。它主要就是适用于单调的数组,这个条件不能忘,不是所有的都能适用,一定是得具有单调性。接下来,我就详细介绍二分法的各种场景
1.最基本的二分法模块---寻找一个数
这个就是最基本的,给定你一个数组,然后再给一个目标数target,在这个数组里面找到这个目标值的索引,找不到返回-1。
最基本的二分法框架(左闭右开)
def binarySearch(nums,target):
'''
:param nums: 目标数组
:param target: 目标元素
:return: 返回的目标元素的索引值,找不到的话返回-1
'''
left=0
right=len(nums)
while(left<right):
mid=(left+right)>>1
if (nums[mid]>target):
right=mid
elif(num[mid]<target):
left=mid+1
else:
return mid
return -1
首先就是这个左右指针的取值问题,现在是采用左闭右开的方式,即[left,right)咱就是在这个区间里面去寻找目标值,首先先取一半,mid=(left+right)>>1向下取整,然后如果这个值就是目标值,那就直接返回,找到了。
如果不是那就是得判断大了还是小了,当前值比目标值要大说明再左边,就是把右边界变成mid,那现在的区间就是[left,mid)因为mid已经判断过了,所以不再进行判断,如果是小了,就是在右边,现在区间就是[mid,right),一下子就把要寻找的区域变成了一半,这就是二分法的真谛,不断的去缩小搜寻区间找到最小值。
接着说一下最后退出的条件,因为是左闭右开的,最后一次进入判断都是左右间隔为1,假如为[2,3)这个时候区间就只为2一个值,进去mid=2判断如果成功就返回,如果不成功就是要么右边界减1,要么左边界加1。然后就退出循环,返回-1。以上就是这个基本框架,那么有人要问了我怎么见过有的地方是right=len(nums)-1呢,别急,接下来就是要说这种情况。
左闭右闭
def binarySearch(nums,target):
'''
:param nums: 目标数组
:param target: 目标元素
:return: 返回的目标元素的索引值,找不到的话返回-1
'''
left=0
right=len(nums)-1
while(left<=right):
mid=(left+right)>>1
if (nums[mid]>target):
right=mid-1
elif(num[mid]<target):
left=mid+1
else:
return mid
return -1
其实整体上只有3个变化,一个是变成左闭右闭了,这样就导致了循环退出条件得是left>right才能退出,为啥呢,因为假如现在区间是[2,2] 这个区间是一个值,如果是left<right,那就直接退出了,相当于忽略了2这个索引值的判断,因此也要随之进行改变,还有一个变换的点就是右边界的取值变成了mid-1,这个又是怎么理解呢,因为现在是左闭右闭区间了,即[left,right]这个时候如果mid不是要找的,就分成两个区间[left,mid-1]和[mid+1,right]这两个区间,就是现在的区间就是这两个之一了。
综上,其实就是看初始化的取值,看区间是左闭右开还是左闭右闭,然后就通过这个搜索区间去判断循环退出条件跟区间变换值
当你看到这,能理解的话,恭喜你,你已经掌握了二分法的大部分真谛了。
2.寻找第一个大于目标值的位置
相信这个题目也是比较清楚的,就是给你一个数组,然你去找第一个大于目标值target的位置,这个题适用在哪呢,想想看,是不是在插入排序这块就是用的这个东西。插入排序不就是在原数组中每次拿出一个值,找一下这个值应该放在哪,就是放在第一个比这个数要大的位置。接下来我给个代码再解释下就清楚多了。
一般情况下左闭右开跟左闭右闭都是可以使用的,但是习惯性问题,我下面讲的例子都采用左闭右开的方式
def search_loc(nums,target):
'''
这个就是找第一个比目标元素大的元素
:param nums: 目标数组
:param target: 目标元素
:return: 返回的是插入位置 找到第一个比目标值大的位置
'''
left=0
right=len(nums)
while(left<right):
mid=(left+right)>>1
if (nums[mid]>target):
right=mid
else:
left=mid+1
return left
最关键的点就是在于这个相等的情况,其他跟寻找对应元素的索引是一样的。为啥这里当找到目标元素的时候,咱要给他left=mid+1呢。这个就是要跟题目相关联了,题目说找到比目标元素大的位置,所以说当mid值就是目标元素的时候,说明实际的位置区间应该在mid右边,就是[mid+1,right)。
这里要注意就是当mid比目标元素大,这个时候有可能这个mid就是咱要找的位置,但是现在区间变成[left,mid)乍一看这个目标值好像在现在要找的区间之外了,是不是丢了。其实不然,这个时候循环退出条件就很重要了,现在假如这个mid就是这个要求的值,那么left会一直向他靠近,直到[mid-1,mid)然后最后一次判断mid-1还是不满足,left=mid-1+1 变成mid,循环退出。返回的位置还是这个mid。 是不是感觉很神奇?那么这种情况就讲完了,大家可以尝试写一下这个左闭右闭的情况。结果是一样的,都是可以进行成功返回的。
当然这个模板也可以实现寻找目标值的右边界,啥意思呢
就是比如[1,2,3,3,3,4,5] ,target=3 用这个方法 找到的就是4的索引值,如果return left-1 那么返回的就是 3的最右边了。其实思路是一样的。改变的就是返回值减1就行了
3.寻找第一个大于等于目标值的位置(左边界)
这个也是题目的意思,还是按上面给的例子
[1,2,3,3,3,,4,5] 现在咱要要找的不是上面的那个了,要找的是第一大于等于目标值的位置,这个其实也叫左边界
适用的就是有个最长上升子序列有用到这个思路。
这个怎么做呢,先不看代码分析下,就是mid等于目标值的时候,是不是咱要找的值要么是这个mid,要么就是在他左边,所以就是进行一个右边界的收缩。就是让right=mid
def search_loc(nums,target):
'''
:param nums: 目标数组
:param target: 目标元素
:return: 返回的是插入位置 这个就是找第一个大于等于目标元素的位置
'''
left=0
right=len(nums)
while(left<right):
mid=(left+right)>>1
if (nums[mid]>=target):
right=mid
else:
left=mid+1
return left
其实跟上面那个右边界的区别就是nums[mid]>=target ,就是当相等的时候,进行右边界的收缩。
4.其他采用二分法的题目
这里大家可以先看看里面的讲解,都很精彩,我的理解就是左闭右开跟左闭右闭还有一些区别,就是采用左闭右闭如果采用left<right的方式进行退出,会导致1.返回的值必定是在数组的长度索引之内2.就是如果只有一个元素可以直接返回不用进行判断,这是这种方式的优点,但是如果采用左闭右开的话,就是数组中只有一个元素的话,还是得进行判断。同时有可能返回数组的长度的索引,在整个数组的最外面。这是不同的题的一些区别,得看看题目中,是否隐藏了如果只有一个元素的话直接返回就可以的信息。