如果面试时面试官让你写一个二分查找你会怎么写呢?不少同学可能都会这样写
int binary_search(const vector<int> &nums, int target) {
int l = 0, r = nums.size();
while (l < r) {
int mid = l + (r - l) / 2;
if (target == nums[mid])
return mid;
else if (target < nums[mid])
r = mid;
else
l = mid + 1;
}
return -1;
}
如果面试的时候这样写了,并且其他方面又没有什么亮点,很不幸,那可能就要回去等通知了。
首先,这段代码确实没有错;但是它有一个缺陷,就是它返回的下标并不是确定的。什么意思呢?如果需要查找的target在有序数组中存在多个,我们没法确定它到底会返回哪一个。
例如:
target: 2
Input: [1, 2, 2, 3, 4]
Output: 2
Input: [1, 2, 2, 2, 2, 2, 2, 3, 4]
Output: 4
对于上面的两个数组一个返回了2,另一个返回了4,结果确实没有错。但如果函数能返回找到的第一个target的下标那肯定就更好了,这样对于两个输入函数都应该会返回1。
那么需要怎样修改才能满足上面的要求呢?首先大体框架不会发生变化,需要变化的是一些细节;当然,有序数组还是需要保证的,这里使用非逆序数组来讲解。并且我们使用左闭右开区间 [l,r),即包含左侧元素而不包含右侧元素。
当 target==nums[mid] 时,不要立刻返回下标。而是令 r=mid,这么做的目的是让搜索区间往左边挪动,挪到最后 l 和 r 相等,此时 l 就是最左侧的 target 下标。
当 target<nums[mid] 时,令 r=mid,因为 nums[mid] 已经比 target 大了,nums[mid] 以及右侧的数都不可能等于 target。注意搜索区间是不包含 nums[r] 的。
当 target>nums[mid] 时,令 l=mid+1,因为 nums[mid] 已经比 target 小了,nums[mid] 左侧的数都不可能等于 target,至于为什么这里需要加1,是因为使用的是左闭右开区间。
int binary_search(const vector<int> &nums, int target) {
int l = 0, r = nums.size();
while (l < r) {
int mid = l + (r - l) / 2;
if (target <= nums[mid])
r = mid;
else
l = mid + 1;
}
if ((l < 0 || nums.size() <= l) || nums[l] != target)
return -1;
return l;
}
根据上面的方法,在退出循环时 l==r。如果数组中存在 target,那么 l 就是最左侧 target 的下标;如果数组中不存在 target,那么 l 就是将 target 插入到数组中并且保持数组有序时 target 的下标。
数组中存在 target 的情形,最终 l 和 r 会在最左侧的 target 相遇 (r=mid 会让区间不断地向最左侧的 target 缩小),然后退出循环。
数组中不存在 target 的情形
- target 小于数组中所有的数,则不断使 r=mid,最终使得 l==r,即退出循环。
- target 大于数组中所以的数,则不断使 l=mid+1,最终使得 l==r。
- target 位于数组的区间范围时,假设对于 a<target<b,且数组 nums 中存在 a,b。如果 nums[mid]<target,l 需要不停地向右移动,并且至多移动到 b 的位置上;同理如果 target<=nums[mid],r 需要向左移动,并且至多移动到 b 的位置上。最终 l 和 r 相遇并退出循环。
综上分析,实际上循环结束时 l 的值就是假设数组中存在 target 时的合理下标。最后我们对于没有找到 target 时做特殊处理,返回-1。
上面我们已经知道了找左侧边界的二分查找怎样实现,那么找右侧边界的二分查找就很简单了,原理是相通的。
int binary_search(const vector<int> &nums, int target) {
int l = 0, r = nums.size();
while (l < r) {
int mid = l + (r - l) / 2;
if (nums[mid] <= target)
l = mid + 1;
else
r = mid;
}
if ((r - 1 < 0 || nums.size() <= r - 1) || nums[r - 1] != target)
return -1;
return r - 1;
}
如果数组中存在 target ,最终 r 也是停在最右侧 target 的右边 (因为 r 向左移动的条件是 target < nums[mid]),所以退出循环后 r 需要减1才是最右侧 target 的下标。这样一来,当数组中不存在 target 时,r-1 就可以定义为“在数组中为target找一个可以使得数组仍然保持有序的位置的前一个下标”。
看到这里,相信各位已经彻底掌握了二分查找,面试的时候不会再因为写不出让面试官满意的二分查找而喜提感谢信。