算法日志2:二分法之茴字的四种写法
前言
对证明的最透彻理解,那就是你把证明背下来
本文的作者为本人的第二人格,参考了二分从入门到入睡这篇文章,
二分查找本质上是把问题归约到可解的子问题,这点同数学归纳法,动态规划等一致
我感觉的二分法和动态规划,数学归纳法区别:
注意这里的可解子问题不一定要选最小可解子问题,比如到一个差不多合适的颗粒度就可以了,你懂我意思吗,可以手动对齐颗粒度。
举例来说,二分法数组大小缩小到3,就可以直接枚举了,把这三个元素都检查一下找到就直接返回,没找到就返回错误。
但是要注意数组大小3,2,1,0最好都写一下枚举,虽然可能用不到,但是用不到又不大可能,有些问题你可能一开始没考虑到,但是考虑不到又不大可能。
如果要化为最小子问题,则为下面四种情况:
有如下的口诀(口诀来自二分从入门到入睡):
-
左闭右开,交叉中止
-
左闭右闭,相等中止
-
左开右开,相邻中止
-
左开右闭,相等中止
c++和python中,最符合直觉理解最舒服的时左闭右闭,剩下几种可以练习一下
短暂切换回第一人格:
二分子问题实战
-
问题要素
-
l 左边界
-
r 右边界
-
m 中间位置
-
nums 问题数组
-
t 待查找值
-
res 最终返回的序号,也可能是-1
-
-
概念对应表, 同级别的概念放同一行, 不要搞错对应
l r m res nums[l]nums[r]nums[m] -
补充知识:
-
m下取整 = (r-l)/2 + l
-
m 上取整 = m下取整 +1
-
-
题目最后要返回的是是res
-
万事俱备,开始拼装
python四种尝试:
左闭右闭
class Solution:
'''
尝试左闭右闭,相错中止
注意对应 m对应序号,nums[m]对应值,和target进行比较是值比较
'''
def search(self, nums: List[int], taret: int) -> int:
t = target #浅拷贝,t和target是对同一块内存的引用
l = 0
r = len(nums) - 1
res = -1 #最终的结果
while(l <= r): # 相错即为r<l
m = (r-l)//2 + l # //为整数除法, 这句代码为下取整,因为正数除法会将小数部分舍去
if t == nums[m]:
res = m
return res
elif t < nums[m]:
l = l
r = m - 1
elif t > nums[m]:
r = r
l = m + 1
return res
如果复用上面的代码,直接左闭右开,发现有问题
class Solution:
'''
尝试左闭右开,相等中止
注意对应 m对应序号,nums[m]对应值,和target进行比较是值比较
'''
def search(self, nums: List[int], target: int) -> int:
t = target #浅拷贝,t和target是对同一块内存的引用
l = 0
r = len(nums) - 1
res = -1 #最终的结果
while(l < r): # 相等即为r==l
m = (r-l)//2 + l # //为整数除法, 这句代码为下取整,因为正数除法会将小数部分舍去
if t == nums[m]:
res = m
return res
elif t < nums[m]:
l = l
r = m ## 开区间需要多取一个元素,如果r取m-1, 那开区间[l,r)少包住一个元素m-1,万一正好t = nums(m-1),就寄了
elif t > nums[m]:
r = r
l = m + 1
return res
上面的代码,假设数组大小为1,只有一个元素时,while(l < r) 这个循环会直接进不去,直接返回-1,如下图所示
一个自然的想法是,需要特判吗?
错误的,其实根本因为开始时的设置错了,错误设置了r = len(nums) - 1
我们来具体分析下面两种设置
-
l = 0 , r = len(nums) - 1
-
l = 0, r = len(nums)
其实本质上,第一种设置,就是把最开始的数组当成了闭区间,第二种则是把最开始的数组设成了开区间
所以,我们可以得出的一个原则是:
开始,中间,结束,即整个过程中的区间开闭要保持一致
搞清了这点,可将代码修改如下:
class Solution:
'''
修改了r 的初始值
尝试左闭右开,相等中止
注意对应 m对应序号,nums[m]对应值,和target进行比较是值比较
'''
def search(self, nums: List[int], target: int) -> int:
t = target #浅拷贝,t和target是对同一块内存的引用
l = 0
r = len(nums) ####### 将初始值从len(nums)-1 改为 len(nums)
res = -1 #最终的结果
while(l < r): # 相等即为r==l
m = (r-l)//2 + l # //为整数除法, 这句代码为下取整,因为正数除法会将小数部分舍去
if t == nums[m]:
res = m
return res
elif t < nums[m]:
l = l
r = m ## 开区间需要多取一个元素,如果r取m-1, 那开区间[l,r)少包住一个元素m-1,万一正好t = nums(m-1),就寄了
elif t > nums[m]:
r = r
l = m + 1
return res
可以看到,代码被一把抓住,顷刻炼化!
左开右开也是如此,需要保持前后区间一致
class Solution:
'''
尝试左开右开,相临中止
注意对应 m对应序号,nums[m]对应值,和target进行比较是值比较
'''
def search(self, nums: List[int], target: int) -> int:
t = target #浅拷贝,t和target是对同一块内存的引用
l = -1 ## 将初始值从0改为-1, 保证开始,过程,结束中区间一致
r = len(nums)
res = -1 #最终的结果
while(l + 1 < r): # 相临即为r==l+1
m = (r-l)//2 + l # //为整数除法, 这句代码为下取整,因为正数除法会将小数部分舍去
if t == nums[m]:
res = m
return res
elif t < nums[m]:
l = l
r = m ## 开区间需要多取一个元素,如果r取m-1, 那开区间(l,r)少包住一个元素m-1,万一正好t = nums(m-1),就寄了
elif t > nums[m]:
r = r
l = m ## 开区间需要多取一个元素,如果l取m+1, 那开区间(l,r)少包住一个元素m+1,万一正好t = nums(m+1),就寄了
return res
下面要进行最神人的代码书写,左开右闭, 按照刚才的套路进行重写
class Solution:
'''
尝试左开右闭,相等中止
注意对应 m对应序号,nums[m]对应值,和target进行比较是值比较
'''
def search(self, nums: List[int], target: int) -> int:
t = target #浅拷贝,t和target是对同一块内存的引用
l = -1
r = len(nums)-1
res = -1 #最终的结果
while(l < r):
m = (r-l)//2 + l # //为整数除法, 这句代码为下取整,因为正数除法会将小数部分舍去
if t == nums[m]:
res = m
return res
elif t < nums[m]:
l = l
r = m - 1
elif t > nums[m]:
r = r
l = m ## 开区间需要多取一个元素,如果l取m+1, 那开区间(l,r]少包住一个元素m+1,万一正好t = nums(m+1),就寄了
return res
发现又寄了,发生肾么事了?
我们来看左闭右开和左开右闭的情况
假设在[5]里找5
左闭右开
[5, 假想)-->5的序号是0,假想的序号是1,由于向下取整,则访问(1-0)/2+0 == 0号元素,可以访问到,且就是5
左开右闭
(假想,5]-->假想的序号是-1,5的序号是0,由于向下取整,我们访问了-1号元素,即nums[-1],但这个元素不存在,是假想的!导致访问超出地址!
解决方案就是向上取整!
class Solution:
'''
尝试左开右闭,相等中止
注意对应 m对应序号,nums[m]对应值,和target进行比较是值比较
'''
def search(self, nums: List[int], target: int) -> int:
t = target #浅拷贝,t和target是对同一块内存的引用
l = -1
r = len(nums)-1
res = -1 #最终的结果
while(l < r):
m = (r-l)//2 + l + 1 # //改成向上取整,为整数除法, 这句代码为下取整,因为正数除法会将小数部分舍去
if t == nums[m]:
res = m
return res
elif t < nums[m]:
l = l
r = m - 1
elif t > nums[m]:
r = r
l = m ## 开区间需要多取一个元素,如果l取m+1, 那开区间(l,r]少包住一个元素m+1,万一正好t = nums(m+1),就寄了
return res
喜欢用第四种方法的小馋猫家里得请哈基高了
总结
| 模版 | 初始值 | 循环条件 | 中间值下标 ( c ) | 左右界 |
|---|---|---|---|---|
| 模版一 相错终止 |
( l = 0, r = n - 1 ) ( [l, r] ) 左闭右闭 |
( while(l <= r) ) | ( c = l + (r - l) / 2 ) 下取整 |
( l = c + 1 ) ( r = c - 1 ) |
| 模版二 相等终止 |
( l = 0, r = n ) ( [l, r) ) 左闭右开 |
( while(l < r) ) | ( c = l + (r - l) / 2 ) 下取整 |
( l = c + 1 ) ( r = c ) |
| 模版三 相等终止 |
( l = -1, r = n - 1 ) ( (l, r] ) 左开右闭 |
( while(l < r) ) | ( c = l + (r - l + 1) / 2 ) 上取整 |
( l = c ) ( r = c - 1 ) |
| 模版四 相邻终止 |
( l = -1, r = n ) ( (l, r) ) 左开右开 |
( while(l + 1 < r) ) | ( c = l + (r - l) / 2 ) 下取整 |
( l = c ) ( r = c ) |
循环不变量分析
在循环中,要抓住不变量,就是l的左侧始终小于target, r的右侧始终大于target
首先对于任意数组nums,我们进行如下扩充
num[-1] = -inf, num[len(nums)] = inf
例如数组[1,2,3,4] --> [-inf, 1,2,3,4, inf]
开始时l = 0, r = len(num)-1, 也满足l 的左侧始终小于target, r的右侧始终大于target,
假设找5, 则最后 变成 r- > nums[r] = 3 -> 4, l -> nums[l] = 4 -> inf,仍然满足 l左侧始终小于target, r右侧始终大于target
假设找2.5,最后变成 r -> nums[r] = 1 -> 2 , l -> nums[r] = 2 -> 3, 循环不变量仍然成立
假设找3,则能直接找到,并且在找的过程中 l从0 变成了2,r始终没动是3, 仍然满足循环不变量
如果要找大于等于target的范围
循环不变量为,l左侧始终小于target, r的右侧始终大于等于target,
最后变成 [..., r, l, ...] ,由循环不变得出
[...,r]每个元素小于target
[l,...]每个元素大于等于target, 即 target <= [l, ...]
又由于数组是单调不减的, 则 l为 target的数组上界
l 的情况又分为两种,因为我们扩充了数组,所以不可能越界,具体为
-
l < len(num) --> nums[l] in origin nums --> 上界为具体序号
-
l == len(num) --> nums[l] == inf -->返回未找到(找到的上界是我们自己定义的序号)
大于等于,小于,小于等于也是如此这般
练习题
python代码
class Solution(object):
def searchRange(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
l= 0
r = len(nums)- 1
lower_edge = len(nums)
while l<=r :
m = l+(r-l)/2
if(nums[m]<target):
l = m+1 ## l左侧的数都小于target
else:
r = m-1 ## r 右侧的数都大于等于target
#### [..., r, l, ...], 故 [l, ...] 都大于等于target, 即找到了下界l
lower_edge = l
l = 0
r = len(nums)-1
upper_edge = -1
while l<=r:
m = l+(r-l)/2
if(nums[m]>target):
r = m-1 ## r的右侧都大于target
else:
l = m+1 ## l的左侧都小于等于target
#### [...,r,l,...], 故 [...,r]都小于等于target,找到了上界r
upper_edge = r
if lower_edge == -1 or lower_edge == len(nums):
return [-1,-1]
if upper_edge == -1 or upper_edge == len(nums):
return [-1,-1]
if nums[lower_edge] == target and nums[upper_edge] == target:
return [lower_edge, upper_edge]
else:
return [-1,-1]

浙公网安备 33010602011771号