楼主: RELAY2014
跳转到指定楼层
上一主题 下一主题
收起左侧

[二分/排序/搜索] 关于Binary Search的疑惑

 
🔗
hhcctvwwa 2021-7-13 09:37:15 | 只看该作者
全局:
看过这个帖子就不会有疑问了 https://www.zhihu.com/question/36132386,我以前也很烦边界问题,看了这个以后再也不纠结了。这个帖子也可以很好地解释为什么很多语言的build in range 是左闭右开。
回复

使用道具 举报

🔗
hezitaohaha 2021-7-13 09:48:09 | 只看该作者
全局:
我一度也因为自己不能完全弄明白while loop终止条件而懊恼,每次要用的时候都要在草稿纸上模拟一下剩余两三个数的时候,左右指针如何移动,同时合适结束搜索。后来干脆硬背三个模板(就是楼主贴代码那三个),瞬间二分搜索都变得简单了,无脑写代码两分钟搞定。
回复

使用道具 举报

🔗
sdszilong 2021-7-13 12:14:52 | 只看该作者
全局:
用一个板子就好了,我最喜欢的就是九章的那个
只是有一点点麻烦的就是在最后还是要判断一下,比如说要找最左边,就要先判断left,要找最右边就要先判断right,之类的。
不过,这个万能套用,我还没碰到过不能套用这个模板的题目。
回复

使用道具 举报

全局:
我其实也有跟楼主类似的困惑,正好在突击刷Binary search tag,就整理一下一点个人理解,希望能抛砖引玉。可能有点啰嗦冗长,请多包涵。

1. pivot/mid index
感觉这个相对好梳理一些,以Python 3为例有如下模版:
mid = (left + right) // 2
mid = int((left + right) / 2)
mid = (left + right) >> 1  # bitwise operation --> 好处是比除法运算快
以上共同的缺点是:如果(left+right)超过最大可存的整数(i.e. integer overflow),那么就有bug,可以用如下模版应对:
mid = left + (right - left) // 2  # 如果left和right都没有over,那么(right - left)也就不会。
mid = left + ((right - left) >> 1)  # 同上,另外此处用bit operation运算比较快。综合来说应该是最好的选择,这个就是之前一个层主推荐的。BTW,mid = left + (right - left) >> 1有bug,因为这等同于(left + (right - left)) >> 1。

还有时候用法是: mid = left + (right + 1 - left) >> 1,这个区别可看个人喜好了,但算是个小细节吧,有时候情急之下debug可能会有帮助。
Exampleleft=0, right=0left=0, right=1left=0, right=2left=0, right=3
mid = left + ((right - left) >> 1) mid=0 mid=0 mid=1 mid=1
mid = left + ((right + 1 - left) >> 1) mid=0 mid=1 mid=1 mid=2

所以,mid = left + (right - left) >> 1的到的mid要么在left和right正中间,要么更靠近left。这种情况下,如果while loop里面二分法的时候left=mid的话,那就会有infinite loop的bug。同理,用mid = left + (right + 1 - left) >> 1的话,如果right=mid,那么也会bug。

2. Binary search模版和思路
楼主提到的
“int left = 0, right = nums.length;
有时又看到,right = nums.length -1;
while loop里,有时left < right 有时left <=right ;”

这几个情况在Leetcode的Explore Card - Binary Search提到的三个模版中的前两个,也是我个人现在最常切换使用的两个模版。我也刷了不少上面的examples才摸出点感觉,有的题可能有一个模版更简洁一些,有的题用两种都差不多。感觉思路决定模版,模版代表思路,所以举个例子来比较一下不同模版/思路。

Leetcode 35: Given a sorted array of distinct integers and a target value, return the index if the target is found. If not, return the index where it would be if it were inserted in order.

模版1: l, r = 0, n & while l < r


def searchInsert(self, nums: List[int], target: int) -> int:
        n = len(nums)
        l, r = 0, n
        while l < r:
            m = l + ((r-l) >> 1)
            if nums[m] == target:
                return m
            elif nums[m] < target:
                l = m+1
            else:
                r = m  #这里不能换成r=m-1,因为m此时还有可能是solution,所以不舍去
        # at this point, we have l == r
        return r
思路:
  • 初始r=n是为了保证初始search space [l, r]包含target solution;如果r=n-1,那么不能保证初始search space还能一定包含solution(e.g. solution=n的时候)。
  • 二分操作l=m+1和r=m之后,也都仍然保证solution还在剩余的half search space里。这样可以用induction证明search space无论怎么二分,之后剩下的search space都肯定包含solution。这个结论下面会有用。这也解释了为什么不能把r=m替换成r=m-1,因为m目前仍有可能是solution。
  • 感觉如果写出的代码如果能完全满足这个inductive logic的话,可能是最自然的方法(之一)了。
  • 因为用了while l < r,被二分操作的最小的search space是size=2(i.e. l and r相邻)。同时,最后跳出while loop后,terminal condition是l==r,剩余search space是[r],这个item r从来没有被二分法检验过。但我们知道/证明了那就是solution,所以可以直接return r这个结果。


模版 2: l, r = 0, n-1 & while l < r
def searchInsert(self, nums: List[int], target: int) -> int:
        n = len(nums)
        l, r = 0, n-1
        while l < r:
            m = l + ((r-l) >> 1)
            if nums[m] == target:
                return m
            elif nums[m] < target:
                l = m+1
            else:
                r = m  # 如果此处换成r=m-1也可以work,但是是另一种思路,解释请见下面
        # at this point, we have l == r
        if nums[r] >= target:
            return r      
        else:
            return n  # or r+1
思路:
  • 区别于模版1,这里用了初始r=n-1,导致不能直接用刚才induction证明的结果,因为初始search space并不一定包含solution。所以只有在“solution在l, r初值之间”的情况下,之后二分操作才能保证remaining search space contains the solution。“solution不在l, r初值之间”的情况还需要另外考虑。
  • 另一种角度分析就是,这里的二分操作只能保证discarded space does not contain the solution,如果通过二分检验把所有不含solution的部分都舍弃了,那么还需要检验最后剩下的search space才能得到solution。
  • 相同于模版1,因为用了while l < r,最后terminal condition还是l==r。
  • 结果就是,最后while loop结束后,剩余一个item r没检验过,不知道是否是solution,所以要额外检验(i.e. solution=n的特殊情况),代码稍微长一点。
  • BTW,这里如果把r=m换成r=m-1也还能work,但是不能保证discarded space does not contain the solution,因为m有可能是solution。但如果m确实是solution,又被discard了话,那么意味着r=m-1以后的r满足nums[r] <= target,所以r在之后的循环里不会再改变了,于是代码最后那个return r+1的还能account for这个情况的solution。在while loop外的检验也能照顾到nums[r]==target的情况。如果之前没用过这个思路的话,可能需要花点时间想清楚。


模版 3: l, r = 0, n-1 & while l <= r [Leetcode 35 答案]
def searchInsert(self, nums: List[int], target: int) -> int:
        n = len(nums)
        l, r = 0, n-1
        while l <= r:
            m = l + ((r-l) >> 1)
            if nums[m] == target:
                return m
            elif nums[m] < target:
                l = m+1
            else:
                r = m-1  # 这里不能换成r=m,不然当l+1==r时,配合m = l + ((r-l) >> 1),会陷入infinite loop。
        # at this point, we have l == r+1
        return r+1  # or l
思路:
  • 这里和模版2都用了r=n-1,但区别在于这里while l <= r。
  • 区别:因为用了while l <= r,最后terminal condition是l==r+1。
  • 跟模版2的r=m-1情况分析起来一样,这个模版里的二分法既不能保证discarded space does not contain the solution,也不能保证remaining search space contains the solution。但again,如果m确实是solution,又被discard了话,那么意味着r=m-1以后的loop里的r满足nums[r] <= target,所以r在之后的循环里不会再改变了,于是代码最后那个return r+1的还能account for这个情况的solution。
  • 再看discarded space does not contain the solution的情况,就又和模版2的思路一样:舍弃不含solution的space,剩余的search space再作分析/检验。
  • 但这里不像模版2那样需要在while loop结束后再做额外的check。得益于while l <= r,被二分操作检验的最小的search space是size=1(i.e. l and r相同),所以所有的item都被while loop内的二分法检验过;while loop结束后剩余的search space为空集,无须再检验。
  • 另外,对于solution=n的特殊情况,r会一直保持初值n-1不变,所以return r+1可以照顾到这个特殊情况。


模版 4: l, r = 0, n & while l <= r
def searchInsert(self, nums: List[int], target: int) -> int:
        n = len(nums)
        l, r = 0, n
        while l <= r:
            m = l + ((r-l) >> 1)
            if m == n:
                return n
            if nums[m] == target:
                return m
            elif nums[m] < target:
                l = m+1
            else:
                r = m-1
        # at this point, we have l == r+1
        return r+1  # l # or m
思路:
  • 这个思路基本跟模版3本质上一样。细微的区别在于这里用了r=n初值,但没帮上啥忙,反倒需要在while loop里要考虑到m==n的情况,提前输出结果,以免后面nums[m] index out of range.


写完发现有点太长了,抱歉。。。总结一下,感觉选模版关键在于选以下哪种思路:
  • remaining/current search space always contains solution
  • discarded search space does not contain any solution
  • if the solution is discarded by binary search/bisection, we could tell where it is relative to left or right bound. When the solution is not discarded, we're back to case 1 or 2


评分

参与人数 2大米 +4 收起 理由
tttooz + 2 给你点个赞!
CriticalSection + 2 很有用的信息!

查看全部评分

回复

使用道具 举报

🔗
evan_zhang 2021-7-13 15:50:17 | 只看该作者
全局:
二分查找感觉还是理解思路和边界条件更重要一点,lz可以看看leetcode里面有人写的关于二分查找的思路帖子(htt打码ps:/打码/leetcode-cn.com/problems/search-insert-position/solution/te-bie-hao-yong-de-er-fen-cha-fa-fa-mo-ban-python-打码/),非常有用。我一开始也是靠模板,后面看了这个帖子仔细学习后就不用模板也可以解决各类二分问题了
回复

使用道具 举报

🔗
Goathered 2021-7-13 17:14:40 | 只看该作者
全局:
同疑惑,最近做题也是靠模板,其实并没有怎么理解边界终止...
回复

使用道具 举报

🔗
oicq455669 2021-7-13 22:03:38 | 只看该作者
全局:
红A 发表于 2021-7-12 14:26
while loop板子有3个
left

关键是想不清这些模板是咋回事。。。
回复

使用道具 举报

全局:
说实话 graph的各种题做熟了后都不觉得难。但binary search的边界问题我一直搞不清,觉得难。
回复

使用道具 举报

🔗
hydrology 2021-7-13 22:52:23 | 只看该作者
全局:
借楼问一下,什么情况下设置 left = -1 或者right = n+1?
比如这道
https://leetcode.com/problems/ma ... -repeated-subarray/
用rolling hash的话最后的解法有这样的:
  1.         lo, hi = 0, min(len(A), len(B)) + 1
  2.         while lo < hi:
  3.             mi = (lo + hi) // 2
  4.             if check(mi):
  5.                 lo = mi + 1
  6.             else:
  7.                 hi = mi
  8.         return lo - 1
复制代码

也有这样的
  1.         nums1, nums2 = (nums1, nums2) if len(nums1) <= len(nums2) else (nums2, nums1)
  2.         lo, hi = -1, len(nums1)
  3.         while lo < hi:
  4.             mid = lo+hi+1 >> 1
  5.             if fn(mid):
  6.                 lo = mid
  7.             else:
  8.                 hi = mid-1
  9.         return lo
复制代码

评分

参与人数 1大米 +1 收起 理由
RELAY2014 + 1 赞一个!

查看全部评分

回复

使用道具 举报

🔗
renjiayi 2021-7-13 23:02:06 | 只看该作者
全局:
以前也老搞不清,直到看见了这个:二分查找有几种写法?它们的区别是什么? - Jason Li的回答 - 知乎
https://www.zhihu.com/question/36132386/answer/530313852
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册账号
隐私提醒:
  • ☑ 禁止发布广告,拉群,贴个人联系方式:找人请去🔗同学同事飞友,拉群请去🔗拉群结伴,广告请去🔗跳蚤市场,和 🔗租房广告|找室友
  • ☑ 论坛内容在发帖 30 分钟内可以编辑,过后则不能删帖。为防止被骚扰甚至人肉,不要公开留微信等联系方式,如有需求请以论坛私信方式发送。
  • ☑ 干货版块可免费使用 🔗超级匿名:面经(美国面经、中国面经、数科面经、PM面经),抖包袱(美国、中国)和录取汇报、定位选校版
  • ☑ 查阅全站 🔗各种匿名方法

本版积分规则

>
快速回复 返回顶部 返回列表