1.二分查找:基于有序序列(递增/递减)的查找算法
核心优势:每次比较后搜索范围可以有效地减半
时间复杂度:O(log n)--最坏情况,其中 n 是要搜索的数组的长度
先举个例子 :我要在1 2 3 4 5 6 7 8 9 10中查询9所在的位置(下标)
令int left=0 right=9(下图我就简化为l和r了)
下标中点mid=(left+right)/2 这里mid还有另一种表达形式 我们晚点再说
[left,right]=[0,9] mid=9/2=4 我们看到下图下标为4的数小于欲查询数9
说明我们需要在[mid+1,right]范围内继续查找 因此令left=mid+1;
[left,right]=[5,9] 因此下标中点mid=7 下标7对应的元素仍然小于9
说明需要在[mid+1,right]范围内继续查找 因此令left=mid+1;
[left,right]=[8,9] 下标中点mid=8对应的数刚好为9 于是返回下标mid=8
大致情况如下:(二分区间为左闭右闭)
代码如下:
我们给的例子不包含重复元素 那么二分查找能不能在有重复元素的有序序列查找呢
当然可以
现在我们来回答之前的问题mid的另一种形式
为什么要有另一种形式呢?
如果二分上界超过int型数据范围的一般时 那么当欲查询元素在序列较靠后的位置时 语句mid=(left+right)/2中的(left+right)就有可能超过int而导致溢出 此时一般使用另一种形式mid=left+(right-left)/2以避免溢出
相信看到这里 有疑问的小猴就该发出疑问了
- 在代码中的左闭右闭以及传入的初值必须是上述情况吗
我的回答:当然不是
关于二分区间 我们一般使用左闭右闭和左闭右开 当然也有左开右闭...
但我要告诉你的是:当我们的二分区间改变 代码也需要跟着改变
我们讲一下一般使用的左闭右开区间
在二分区间为左闭右开的情况下 我们传入的初值 while内的条件 以及right的表达式都改变了
现在我们一一解释
因为二分区间为左闭右开 所以原先的left<=right无法在使用 否则会与给定的二分区别相悖
由于右区间为开 所以我们传入的初值right=n 不包含下标为n的元素 下标依旧是从0~ n-1
接下来我们讲right表示式的改变
其实也是因为当right=mid的时候 才能包含下标为mid-1的元素(依旧是右开搞得鬼)
总结:当假设的二分区间不一样时 函数中while循环条件 right left的表达式会有改变
我们只要画张简图就可以一探究竟
讲了这些大家对二分查找应该有所了解了 现在我们趁热打铁 探讨更进一步的问题
如果递增序列arr中的元素可能重复 那么如何对给定的欲查询的元素x 求出序列中第一个大于等于x的元素的位置L以及第一个大于x的元素的位置R 这样元素x在序列中的存在区间就是左闭右开区间[L,R)
例如对下标从0开始、有5个元素的序列{1,3,3,3,6}来说 如果要查询3 则应当得到L=1,R=4 如果查询5 则应当得到L=R=4 如果查询6 则应当得到L=4、R=5;而如果查询8 则应当得到L=R=5 显然如果序列中没有x 那么L和R也可以理解为假设序列中存在x 则x应当在的位置
当我们使用二分查找来解决这个问题时,我们通常会初始化一个左闭右闭的搜索区间,然后根据比较结果逐步缩小这个区间。在每一步中,我们根据中间元素与x的比较结果来决定是向左还是向右搜索。一旦我们找到了L和R,我们就可以将它们转换为左闭右开区间的形式来报告结果。
- 我们先来求序列中第一个大于等于x的元素的位置L
假设当前区间为左闭右闭区间[left,right] 那么可以根据mid位置处的元素与欲查询元素x的大小来判断应当往哪个子区间查找
如果arr[mid]>=x 说明第一个大于等于x的元素的位置一定在mid处或在mid的左侧 应往左子区间[left,mid]继续查询 即令right=mid
如果arr[mid]<x 说明第一个大于等于x的元素的位置一定在mid的右侧 应往右子区间[mid+1,right]继续查询 即令left=mid+1
下面给出代码:
这里有几点需要大家注意:
- 传入的初值为[0,n]而不是之前的[0,n-1]
- 循环条件为left<right而非之前的left<=right
现在我们开始解释
对1来说 二分的初始区间应当能覆盖到所有可能返回的结果 首先二分下界是0 这是必然的(本题来说) 但是二分上界是n-1还是n呢? 考虑到欲查询元素有可能比序列中的所有元素都要大,此时应当返回n(即假设它存在 它应该在的位置) 因此二分上界为n
对2来说 循环条件改为left<right 这是由问题本身决定的 在之前的left<=right那题中 需要当元素不存在时返回-1 这样当left>right时 [left,right]就不再是闭区间 可以此作为元素不存在的判定原则 因此left<=right满足时 循环应当一直执行 但是在本题(返回第一个大于等于x的元素的位置)中,就不需要判断元素x本身是否存在 因为就算它不存在 返回的也是“假设它存在 它应该在的位置” 于是当left==right时 left<right的二分区间[left,right]刚好能夹出唯一的位置---就是需要的结果
这里还需要注意的是 当left==right时 while循环停止 所以最后的返回值既可以是left 也可以是right
2.接下来我们求序列中第一个大于x的元素的位置
其实我们只要将if语句中的>=换成>即可
当我们求出第一个大于等于x的元素的位置L和第一个大于x的元素的位置R后 只需要将其代入左闭右开的区间即可