二分查找

二分查找
GuoTianyu二分查找(Binary Search)
简介
二分查找(Binary Search)是一种在有序数组中查找特定元素的搜索算法。它通过反复将数组分成两半,然后在其中一半中继续查找,直到找到目标元素或者确定目标元素不在数组中。
满足条件
- 数组必须有序。(a n-1 ≤ a n 或 a n-1 ≥ a n)
- 数组中每个元素必须可以随机访问。
- 数组的上限和下限必须是已知的。
时间复杂度
二分查找的时间复杂度是O(log n),其中n是数组的长度。这比线性查找的O(n)要快得多,特别是在处理大型数组时。
二分查找步骤
- 确认数组的下限l与上限r。
- 确认二分终止条件,while(l+1<r)。
- 开始二分 ,int mid = r+l >> 1。(’+‘优先级比’>>‘高,通过右移运算符,可以实现二分,也可以写这个 int mid = (r+l)/2)
- 编写bool check(int mid) 函数,check(mid)用于区分数组的边界。(重点)
- 更新l和r的值,缩小查找的范围。(重点)
- 重复上面3~5步骤,直到不满足条件while(l<r)。
- 输出最终结果。
二分查找应用
查找无重复单调有序数组
下面开始解题:
先判断是否满足二分的条件
- 元素单调递增,满足有序性。
- 使用数组存储,满足随机访问。
- 上限与下限已知。
满足上面三点,可以使用二分法。
先创建一个数组接收数据。
1
int arr[] ={1,3,4,6,8,9,10,16,20,25,44,90,100};
确定左边界和右边界,即元素中的下标最小值-1和大于最大值+1。
1
2int l=-1; //左边界 最小元素下标-1
int r=sizeof(arr)/sizeof(arr[0]); //右边界,要大于数组的最大下标,该数组最大下标是12,长度是13。
- 这里的左边界和右边界的范围一定要是[答案左边界-1,答案右边界+1],否则在查找边界元素时,如上面的1或者100,会查找失败(非边界元素不用考虑边界)。
下面开始二分查找,先把模板写上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using namespace std;
int arr[] ={1,3,4,6,8,9,10,16,20,25,44,90,100};
int n;
bool check(int mid) //编写check(mid)函数
{
}
int main()
{
cin>>n;// 接收要查找的元素n
int l=-1; //左边界
int r=sizeof(arr)/sizeof(arr[0]); //右边界,即数组的最大下标
while(l+1<r)//二分终止条件
{
int mid = l + r >> 1;//也可以写 int mid = (l+r)/2;
if(check(mid))// 边界更新条件,根据题意写
{
}else
{
}
}
}基本所有的二分查找都适用上面这套模板,多理解理解,背一背。
编写check(mid)函数和更新l与r的边界(重点)。
假如我们要查找元素‘4’。
我们先看一下,当前l和r所指向的位置:
第一次进入while(l+1<r),第一次执行 int mid = l + r >> 1 ,此时mid = -1+ 13 >> 1 = 6
此时,发现要查找的元素‘4’在mid指针的左侧,现在元素‘4’在一个新的区间内(l,mid),arr[l]<4<arr[mid],所以我们要移动r指针,指向mid,即r=mid。
再次更新mid的值:mid = l+r>>1=-1+6>>1=2,arr[mid]=4,我们就找到了元素‘4’,并且元素‘4’的下标就是2。
已经找到了元素‘4’的下标,但是while(l+1<r),循序没有退出,所以还需要更新l和r的位置,此时更新左边界l和更新右边界r都是可以的。
- 如果更新左边界,使目标元素最终在左边界上,我们称为“答案在左边界上” 。
- 如果更新右边界,使目标元素最终在右边界上,我们称为“答案在右边界上” 。
这是两种不同的写法,不同的写法会导致下面更新l和r不同,哪一种写法都可以解决问题。
答案在左边界上
答案都在左边界上了,那么左边界在更新的时候,就要考虑答案是否在左边界上了。所以给l赋值的时候,范围要包含目标元素。
1
2
3
4
5
6
7
8
9
10bool check(int mid) //编写check(mid)函数。
{
if(arr[mid]<=n)//只要能分成上面两部分就行,if(arr[mid]>n)也可以。
{
return true;
}else
{
return false;
}
}根据上面check(mid)函数,就可以更新l和r了。
1
2
3
4
5
6
7
8
9
10
11
12while(l+1<r)
{
int mid = l + r >> 1;
if(check(mid))
{
l=mid;
}
else
{
r=mid;
}
}答案在右边界上
答案都在右边界上了,那么右边界在更新的时候,就要考虑答案是否在右边界上了。所以给r赋值的时候,范围要包含目标元素。
1
2
3
4
5
6
7
8
9
10bool check(int mid) //编写check(mid)函数。
{
if(arr[mid]>=n)//只要能分成上面两部分就行,if(arr[mid]>n)也可以。
{
return true;
}else
{
return false;
}
}根据上面check(mid)函数,就可以更新l和r了。
1
2
3
4
5
6
7
8
9
10
11
12while(l+1<r)
{
int mid = l + r >> 1;
if(check(mid))
{
r=mid;
}
else
{
l=mid;
}
}是否找到了目标元素,并输出结果。
“答案在左边界上”完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41//答案在左边界上
using namespace std;
int arr[] ={1,3,4,6,8,9,10,16,20,25,44,90,100};
int n;
bool check(int mid) //编写check(mid)函数
{
if(arr[mid]<=n)
{
return true;
}else
{
return false;
}
}
int main()
{
cin>>n;// 接收要查找的元素n
int l=-1; //左边界
int r=sizeof(arr)/sizeof(arr[0]); //右边界,即数组的最大下标
while(l+1<r)
{
int mid = l + r >> 1;
if(check(mid)) //技巧:答案在哪个边界上,为true,就更新哪个边界
{
l=mid;
}
else
{
r=mid;
}
}
if(arr[l]==n)//判断左边界上,是否为要查的元素
{
cout<<l;
}
else
{
cout<<-1;
}
}“答案在右边界上”完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41//答案在右边界上
using namespace std;
int arr[] ={1,3,4,6,8,9,10,16,20,25,44,90,100};
int n;
bool check(int mid) //编写check(mid)函数
{
if(arr[mid]>=n)
{
return true;
}else
{
return false;
}
}
int main()
{
cin>>n;// 接收要查找的元素n
int l=-1; //左边界
int r=sizeof(arr)/sizeof(arr[0]); //右边界,即数组的最大下标
while(l+1<r)
{
int mid = l + r >> 1;
if(check(mid))//技巧:答案在哪个边界上,为true,就更新哪个边界
{
r=mid;
}
else
{
l=mid;
}
}
if(arr[r]==n)//判断右边界上,是否为要查的元素
{
cout<<r;
}
else
{
cout<<-1;
}
}
二分查找拓展
查找单调不递增或递减有序数组
下面开始解题:
先判断是否满足二分的条件
- 元素单调递增,满足有序性。
- 使用数组存储,满足随机访问。
- 上限与下限已知。
满足上面三点,可以使用二分法。
先创建一个数组接收数据。
1
int arr[] ={2,4,7,7,7,7,8,8,9,9,9,10,16,16,32,50};
确定左边界和右边界,即元素中的小于最小值和大于最大值。
1
2int l=-1; //左边界 要小于0
int r=sizeof(arr)/sizeof(arr[0]); //右边界,要大于数组的最大下标先把模板写上,之后再补全。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using namespace std;
int arr[] ={2,4,7,7,7,7,8,8,9,9,9,10,16,16,32,50};
int n;
bool check(int mid) //编写check(mid)函数
{
}
int main()
{
cin>>n;// 接收要查找的元素n
int l=-1; //左边界
int r=sizeof(arr)/sizeof(arr[0]); //右边界,即数组的最大下标
while(l+1<r)
{
int mid = l + r >> 1;
if(check(mid))
{
}
else
{
}
}
}编写check(mid)函数和更新l与r的边界(重点)。
假如我们要查找元素‘9’。
我们先看一下,当前l和r所指向的位置:
check(mid)函数必须能够,区分目标元素的左右边界,所以可以这样分。
- 第一种分法:答案在右边界上
1
2
3
4
5
6
7
8
9
10bool check(int mid)
{
if(arr[mid]>=n)
{
return true;
}else
{
return false;
}
}第二种分法:答案在左边界上
1
2
3
4
5
6
7
8
9
10bool check(int mid)
{
if(arr[mid]<=n)
{
return true;
}else
{
return false;
}
}- 在查找单调不递增或递减有序数组 中:
- 查找目标元素的最小下标使用:答案在右边界上法 。
- 查找目标元素的最大下标使用:答案在左边界上法 。
找到目标元素,输出结果。
完整代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
using namespace std;
int arr[] ={2,4,7,7,7,7,8,8,9,9,9,10,16,16,32,50};
int n;
int save_l,save_r;//保存左边界和右边界
bool checkOnRight(int mid) //答案在右边界上
{
if(arr[mid]>=n)
{
return true;
}
else
{
return false;
}
}
bool checkOnLeft(int mid) //答案在左边界上
{
if(arr[mid]<=n)
{
return true;
}
else
{
return false;
}
}
int main()
{
cin>>n;// 接收要查找的元素n
int l=-1; //左边界
int r=sizeof(arr)/sizeof(arr[0]); //右边界,即数组的最大下标
//先找左边界 ,采用“答案在右边界法”
while(l+1<r)
{
int mid = l + r >> 1;
if(checkOnRight(mid)) //答案在右边法
{
r=mid;
}
else
{
l=mid;
}
}
save_l=r; //保存左边界
if(arr[save_l]==n) //找到了目标元素的最小下标
{
//之后找右边界 ,采用“答案在左边界法”
l=-1; //其实这里可以为l=save_l-1,因为上面的save_l就是n的左边界,这样可以缩小范围
r=sizeof(arr)/sizeof(arr[0]);//右边界
while(l+1<r)
{
int mid = l + r >> 1;
if(checkOnLeft(mid)) //答案在左边法
{
l=mid;
}
else
{
r=mid;
}
}
save_r=l;//保存右边界
if(arr[save_r]==n)//找到了目标元素的最大下标 ,上面已经验证最小下标了,故不需要验证了。
{
cout<<save_l<<" "<<save_r;
}
}
else // 没有找到
{
cout<<"-1";
}
return 0;
}上面代码的封装版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51//封装版(可改变参数,可读性高)
using namespace std;
int getMinPos(int l,int r,int arr[],int n)//获取目标元素的最小下标
{
while(l+1<r)
{
int mid = l+r>>1;
if(arr[mid]>=n)
{
r=mid;
}else
{
l=mid;
}
}
return arr[r]==n?r:-1;
}
int getMaxPos(int l,int r,int arr[],int n)//获取目标元素的最大下标
{
while(l+1<r)
{
int mid = l+r>>1;
if(arr[mid]<=n)
{
l=mid;
}else
{
r=mid;
}
}
return arr[l]==n?l:-1;
}
int main()
{
int arr[] ={2,4,7,7,7,7,8,8,9,9,9,10,16,16,32,50};
int n;
cin>>n;// 接收要查找的元素n
int l=-1; //左边界
int r=sizeof(arr)/sizeof(arr[0]); //右边界,即数组的最大下标
int save_l=getMinPos(l,r,arr,n);
int save_r=getMaxPos(save_l-1,r,arr,n);//这么写是为了缩小范围,也可以写getMaxPos(l,r,arr,n);
if(save_l!=-1)//只要save_l和save_r其中一个不是-1,另一个肯定不是-1,只要能找到最小下标,也就存在最大下标。
{
cout<<save_l<<" "<<save_r;
}else
{
cout<<-1;
}
return 0;
}如果给一个单调不递增有序数组,还能使用这个方法吗?
本质上获取目标元素最小下标还是使用答案在右边界上法,最大下标是答案在左边界上法。
举例:给定一个单调不递增有序数组{10,9,8,8,8,8,5,2,1,0},请输出指定元素n的最小下标和最大下标,否则输出-1。
为什么还能使用“答案在右边界法”和“答案在左边界法”呢?请看下图:
虽然数组元素的顺序改变了,但是仍然是有序的,具有边界性质的,所以只需改变check(mid)里面的条件就好了,改的地方我会标注一下,仔细对比一下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
using namespace std;
int arr[] ={10,9,8,8,8,8,5,2,1,0};
int n;
int save_l,save_r;//保存左边界和右边界
bool checkOnRight(int mid) //答案在右边界上
{
if(arr[mid]<=n) //这里改了
{
return true;
}
else
{
return false;
}
}
bool checkOnLeft(int mid) //答案在左边界上
{
if(arr[mid]>=n) //这里也改了
{
return true;
}
else
{
return false;
}
}
int main()
{
cin>>n;
int l=-1; //左边界
int r=sizeof(arr)/sizeof(arr[0]);
while(l+1<r)
{
int mid = l + r >> 1;
if(checkOnRight(mid))
{
r=mid;
}
else
{
l=mid;
}
}
save_l=r;
if(arr[save_l]==n)
{
l=-1;
r=sizeof(arr)/sizeof(arr[0]);
while(l+1<r)
{
int mid = l + r >> 1;
if(checkOnLeft(mid))
{
l=mid;
}
else
{
r=mid;
}
}
save_r=l;
if(arr[save_r]==n)
{
cout<<save_l<<" "<<save_r;
}
}
else
{
cout<<"-1";
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51//封装版
using namespace std;
int getMinPos(int l,int r,int arr[],int n)//获取目标元素的最小下标
{
while(l+1<r)
{
int mid = l+r>>1;
if(arr[mid]<=n) //这里改了
{
r=mid;
}else
{
l=mid;
}
}
return arr[r]==n?r:-1;
}
int getMaxPos(int l,int r,int arr[],int n)//获取目标元素的最大下标
{
while(l+1<r)
{
int mid = l+r>>1;
if(arr[mid]>=n) //这里也改了
{
l=mid;
}else
{
r=mid;
}
}
return arr[l]==n?l:-1;
}
int main()
{
int arr[] ={10,9,8,8,8,8,5,2,1,0};
int n;
cin>>n;
int l=-1;
int r=sizeof(arr)/sizeof(arr[0]);
int save_l=getMinPos(l,r,arr,n);
int save_r=getMaxPos(save_l-1,r,arr,n);
if(save_l!=-1)
{
cout<<save_l<<" "<<save_r;
}else
{
cout<<-1;
}
return 0;
}