关于二分查找时的边界分类问题
前言
众所周知,我们在优化有关查找的算法时,都会用到二分,但是不同的题用到的二分查找,其左右边界/中点的处理大多都不相同,所以在这里写一下关于一些常见二分边界的种类。
正文
注意,二分查找基于数组有序
查找左右边界
-
左边界
查找左边界,也就是某个数字的起始位置,由于我们采取向下取整的方式,所以在更新变量l的时候,需要+1来避免陷入无限循环(比如l = 1, r = 2 此时进入到else分支的时候)
然后最关键的就是边界怎么找?
其实很简单,由于我们现在想要找到左边界,所以当我们查找的数字有多个的时候,肯定会出现这个情况:mid = target,此时我们只需要变更一下二分查找的右边界,直到mid != target,再变更左边界,这样一来,在循环的最后,l = x[n], r = x[x + 1], 此时 r = target,而l刚好是target前一个元素,这样不就找到了我们的左边界吗?
for l < r { mid := (l + r) / 2 if x[mid] >= target { r = mid } else { l = mid + 1 } }
-
右边界
此时不要以为将左边界里面的等号去掉就万事大吉了,事实上,这是错误的做法
由于此时我们寻找的是右边界,所以等号去掉仅仅是第一步,我们可以来看一个例子:
{1,2,3,4},target = 2,如果仅仅是将等号去掉,你猜猜这样得到的答案是什么?ans = 2,仔细分析我们就可以弄清楚,当我们得到了x[mid] == target的情况,倘若我们再对mid + 1,此时l这个边界很可能对应的就不再是target的值了,所以我们不能对l = mid + 1 操作。
此时怎么办?既然l不能等于mid + 1, 那么r = mid - 1,总可以了吧,此时我们也要对mid的计算进行一些改变,之前我们直接对小数部分进行了舍弃,然而,在这里如果继续沿用舍去小数部分的做法,是会出错的,比如区间[1,2],所以这里采取四舍五入的做法,能够正确的变更边界。
for l < r { mid := (l + r + 1) / 2 if x[mid] > target { r = mid - 1 } else { l = mid } }
查找插入位置
-
插入位置
有时,我们还会碰到一些数组中可能不存在的target,然后我们需要找到正确的位置并插入,插入后能够保证原数组的有序性。
正确的位置也就是数组中最后一个存储的数值是小于或等于target的位置的下一个位置,虽然采取直接查找第一个大于target的值这个策略也行,但是某些情况下并不适合使用这种策略。
在遇到数组中有多个相同元素的时候,某种意义上我们也要寻找这个元素的左边界,但是我们有需要查找“最后一个”位置,某种意义上我们还需要找到“右边界”,所以我们要将以上两者相结合。
由于在遇到相等的情况时,我们需要查找左边界,所以当x[mid] == target时,移动右边界。
同时在x[mid] < target时,我们需要移动我们的左边界,基本思路定好了,剩下便是一些细节的+1还是-1还是不加不减的问题,所以此时,我们最好分析一些边界情况:
[1,6],插入的数值为6,此时,由于我们还没有选取mid应该采用的计算方式,所以我们先看if语句,倘若mid舍去小数,那么我们的r = mid, l = mid + 1,此时,找到的便是我们的位置1,是错误的,所以我们采取四舍五入的方法,此时, r = mid - 1, l = mid,找到下标0,结果正确。
for l < r { mid := (l + r + 1) / 2 if x[mid] < target { l = mid } else { r = mid - 1 } }
这部分去想还是挺困难的,比较好的记忆办法就是自己在脑海里面想一遍,然后理解着敲出来。