二分查找适用于有序数组
(拓展理解二分查找)特殊题目下,数组是否有序并不重要,比如我们把数组分为:左半边数组,mid,右半边数组。我们先判断,mid是否满足题目条件,是就直接返回;如果不是,然后我们再根据题目条件判断正确答案在左半边数组还是右半边数组,可能有一半边数组比另外一半边数组好判断正确答案位置(根据题目条件),比如左半边数组比右半边数组好判断,根据题目条件,那我们就先判断正确答案在不在左半边数组,如果在左半边数组就继续搜索左半边数组,如果不在左半边数组,那么一定在右半边数组,根据排除法,就在右半边数组搜索正确答案,重复上诉过程,直到找到正确答案
一些特殊题目甚至需要自己构建二分查找的数组,就需要观察力了
题目
比如说我要在数组v里找个target,打印target的数组下标
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> v = { 1,2,3,4,5,6,8 };
int left = 0;
int right = v.size() - 1; // 如果写的是 v.size(), 搜索范围就是[0,v.size],小心越界
// 根据实际情况选择范围,特殊题目会用到 right=v.size()
int target = 7;
int res = -1; // 假设-1不会出现在数组里
while (left <= right) { // 见注解 ①
int mid = left + (right - left) / 2; // 见注解 ②
if (v[mid] < target) { //说明 target 偏大,要往右边搜索
// 接下来搜索区间范围 [mid + 1, right]
left = mid + 1;
}
else if (v[mid] > target) { // 说明 target 偏小,要往左边搜索
// 接下来搜索区间范围 [left, mid - 1]
right = mid - 1;
}
else {
// v[mid] == target
// 找到了
res = mid;
break;
}
}
if (res != -1) {
cout << res;
}
}
注解
① 首先 left <= right 还是 left < right,取决于你当 left == right 的时候,你要不要在 while 循环里面做判断。如果使用 left < right,可能会退出循环,我们就判断不到 target 位置正好在 left 上(或者说 right 上),当然我们也可以在退出 while 循环时,使用 v[left] == target 做判断,防止漏网之鱼
② 分为两点:
1、left + (right - left) / 2 和 (right + left) / 2 的区别,其实没有太大区别。就是使用 (right + left) 的时候,如果 left 和 right 都约等于整型最大值,(right + left) 这个时候容易超过整型长度
2、left + (right - left) / 2 和 left + (right - left + 1) / 2 的区别,取决于你想最后是向上取整还是向下取整,向上取整比如 + 1 小心一点,就是小心数组越界,比如说 mid 算出来可能大于数组长度,所以初学者最好有个好习惯就是写出搜索范围
对于初学者的建议
⑴ 要把所有的 if 条件的可能都写出来,有些题解(见下面)可能是把 v[mid] > target 和 v[mid] == target 写在一起,或者直接写个 else,后面不写 if 条件,这种写起来就非常不可控
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> v = { 1,2,3,4,5,6,8 };
int left = 0;
int right = v.size() - 1;
int target = 7;
int res = -1; //假设-1不会出现在数组里
while (left < right) {
int mid = left + (right - left) / 2;
if (v[mid] < target) { //说明 target 偏大,要往右边搜索
// 接下来搜索区间范围 [mid + 1, right]
left = mid + 1;
}
else if (v[mid] >= target) { // 说明 target 偏小,要往左边搜索
// 接下来搜索区间范围 [left, mid]
right = mid;
}
/*
* else { // 说明 target 偏小,要往左边搜索
* // 接下来搜索区间范围 [left, mid]
* right = mid;
*}
*/
}
if (v[left] == target) {
cout << left;
}
}
⑵ 要把所有的 if 语句中的搜索区间范围写出来,作用有下面两点:
一是范围可控,如上面这段代码,就需要考虑万一 right 始终等于 mid,可能会导致死循环,比如:我 while条件写的 left <= right,然后 left等于right等于mid,mid 的值就是 target,就会死循环
上面代码没有出现死循环是因为 while 条件是 left < right(见注解①),而且我们是向下取整(见注解②)
left 等于 right 会退出 while 循环,或者 left 和 right 差值为 1时,由于我们向下取整,left + (right - left) / 2 肯定会等于 left,mid 赋值给 right,left 就等于 right,接着退出循环
二是容易判断数组越界
特别提示
mid=(right + left) / 2 如果和 left=mid 搭配可能会出问题,如果 left 与 right非常接近(比如差一位),(right + left) / 2 的结果始终为 left,然后 if 条件始终走向 left=mid ,就可能导致死循环
所以,建议 (right + left+1) / 2 和 left=mid 搭配,或者是 (right + left) / 2 和 right=mid 搭配(上面代码例子中,我用的后者)
有人可能会说,那我不用 left=mid,我用 left=mid+1 不就行了,其实不行,有些题目就是要 left=mid,mid 可能是一个备选答案,但是 mid+1却不是正确答案,举个例子
找到下面升序数组最右边的3
[1,3,3,3,3,4,5]
如果 mid 的位置是正中间的3,那么执行 left=mid+1 没问题,但是 mid 如果正好是最右边的 3,然后我们执行 left=mid+1,就会错失正确答案