二分查找
介绍
二分查找,也叫折半搜索、对数搜索。是用来在一个
有序
数组中查找一个数的算法。
例题
题目描述
给定一个
n
个元素有序的
升序
整型数组
nums
和一个目标值
target
,写一个函数
搜索 nums 中的 target
,
如果目标值存在返回下标,否则返回 -1
。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
提示:
-
你可以假设
nu
m
s
nums
n
u
m
s
中的所有元素是
不重复
的。 -
nn
n
将在
[1
,
10000
]
[1, 10000]
[
1
,
1
0
0
0
0
]
之间。 -
nu
m
s
nums
n
u
m
s
的每个元素都将在
[−
9999
,
9999
]
[-9999, 9999]
[
−
9
9
9
9
,
9
9
9
9
]
之间。
解题思路
思路1:暴力
(
O
(
n
)
)
(O(n))
(
O
(
n
)
)
遍历数组,如果出现了
target
值则
返回其下标
,如果遍历完还没有出现
target
值则
返回-1
,因为整个数组需要遍历一遍,因此时间复杂度为
O
(
n
)
O(n)
O
(
n
)
。
代码
class Solution {
public:
int search(vector<int>& nums, int target) {
for(int i = 0; i < nums.size(); i ++) // 遍历数组
if(nums[i] == target) return i; // 如果出现了target值,返回下标
return -1; // 遍历完还没出现target值,返回-1
}
};
思路2:二分
(
O
(
l
o
g
n
)
)
(O(logn))
(
O
(
l
o
g
n
)
)
从题目中我们发现数组是
有序
的,符合二分查找的使用条件。假设数组长度为
n
,我们可以先看
nums[n / 2]
,假如
nums[n / 2] > target
,那么就意味着
nums[n / 2]
之后的所有数都大于
target
,我们只需要从
0
∼
n
/
2
0 \sim n / 2
0
∼
n
/
2
中去找
target
即可。否则我们就从
n
/
2
∼
n
−
1
n / 2 \sim n – 1
n
/
2
∼
n
−
1
中去找,当只剩下最后一个元素时,我们
判断它与target是否相等
,如果
相等就返回下标,否则返回-1
.因为我们每次都只会用到前一半或者后一半的数组,最后数组长度从
n
变成
1
,因此总共操作次数
k
符合
2
k
=
n
2^k = n
2
k
=
n
,因此
k = logn
,时间复杂度为
O
(
l
o
g
n
)
O(logn)
O
(
l
o
g
n
)
。
代码
class Solution {
public:
int search(vector<int>& nums, int target) {
int l = 0,r = nums.size() - 1; // 设l为左边界,r为右边界,从nums[]l~r]中查找target
while(l < r){ // 如果还剩下不止一个元素,则继续查找
int mid = l + r >> 1; // 求出l~r的中点
if(nums[mid] < target) l = mid + 1; // 假如中点值小于target,,就从nums[mid + 1,r]中查找target
else r = mid; // 否则从nums[l,mid]中查找target
}
if(nums[l] == target) return l; // 如果剩下的数 == target,返回下标
return -1;// 否则返回-1
}
};
二分思想
前面的例题应该已经让你对二分有个初步的概念。就是将一个
有序数组
分成两半,其中一半的元素都
<
=
m
i
d
<=mid
<
=
m
i
d
值,另外一半反之,因此只有一半是我们可能需要的,另一半我们可以直接舍弃。从广义上看,不仅是排序好的数组可以用二分,只要是左右两边具有不同的性质都可以用二分,排序好的数组中,
mid左边的数都<=mid
,
mid右边的数都>=mid
,这便是左右两边的不同性质,可以让我们对数组进行折半拆分。,例如
搜索旋转排序数组
,这个题目的数组虽然没有严格排序,但它依然可以用二分解答。
边界问题
整数二分有一个边界问题,那就是在
if(nums[mid] < target) l = mid + 1;
中能不能改成
l = mid
,答案是不可以,假如我们有一个从
1
∼
100
1 \sim 100
1
∼
1
0
0
内猜数字
target = 7
的程序代码如下:
#include <iostream>
using namespace std;
int main(){
int n =100;
int target = 7;
int l = 1,r = 100;
while(l < r){
cout << l << ' ' << r << endl; // 输出l 和 r
int mid = l + r >> 1;
if(mid < target) l = mid;
else r = mid;
}
cout << l;
return 0;
}
我们很容易发现程序陷入了死循环,因为当
l = 6,r = 7
时,,
mid = l + r >> 1 = 6
,
6 < 7
,因此
l = mid = 6
,因此陷入了死循环,归根结底是因为
mid = l + r >> 1
是
向下取整
的,因此当还剩2个元素的时候
l和mid有可能相等
,因此令
l = mid
会引发死循环,所以
l = mid + 1
在这份代码中是无法修改为
l = mid
的,其它情况也可以自由探索。