二分法的难点在于琐碎,但它的目标也不过是把顺序元素划成两份。有一些模板可以用于应对大多数情况:
假设数组A可以被分为两部分,前半部分满足条件cond,后半部分不满足,那么下面的代码可用于求解数组中第一个不满足条件cond的元素下标。如果所有元素都满足cond,那么会返回最大索引加一。
int find(vector<int>& A) {
int n = A.size();
int l = 0, r = n - 1;
while (l <= r) {
int m = (l + r) / 2;
if (cond) l = m + 1;
else r = m - 1;
}
return l;
}
算法正确性说明:l和r分别代表待查闭区间的左右界,n代表数组中元素数目。算法开始时,A[l]满足cond,A[r]不满足;由于算法中l保持严格递增,r保持严格递减,l或r总会越过cond的分界线。不妨设l先越过分界线,即A[l]不满足cond;可以想见由于m>=l,故A[m]也将不满足cond,接下来l不再变化,只有r会严格递减,直至l>r,循环结束。
然而巧合的是,A[l]必定是第一个不满足cond的元素。由于l被赋值为m+1时,A[m]一定满足cond,当m成为最后一个满足cond的元素的下标时,m+1,即l,便是第一个不满足cond的元素的下标。
接下来r也越过分界线,更巧的是r刚好比l小1,即是最后一个满足cond的元素。这是必然的,否则最后一步时,A[m]满足cond,说明算法最后一步选择的不是r=m-1,产生矛盾。
若r先越过分界线,同理可证。
二分法的目标不过是把顺序元素划成两份。综上所述,算法结束时,A[l]是右半边元素的第一个,A[r]是左半边元素的最后一个。
例子:
- 剑指 Offer 53 – I. 在排序数组中查找数字 I:查找一个顺序数组nums中数字target出现的次数。这道题可以用两次二分解决,第一次令cond为nums[i]<=target,这样就求得了第一个大于target的数的索引;第二次令cond为nums[i]<=target-1,求得第一个大于target-1的数的索引。由于nums中都是整数,如果target存在,求得的就是第一个target的索引,否则就和第一次二分得到的结果一样。最后两次二分的结果相减,就得到答案了。
- 剑指 Offer 53 – II. 0~n-1中缺失的数字:查找顺序数组中一个缺失的数字,cond其实就是nums[i]==i,这样就可以求得第一个不等于自己索引的数字,它就是答案。