二分查找
前言
打算重新学习一下算法,目标是理解透彻。 先从最简单的二分法开始。一、思想
二分查找的思想很简单,现在有1-100这样一百个提前排好序的数字,想要找到其中一个数,例如7(很显然它在第七个位置,但现在我们需要把这个结论暂且忘掉),那么可以从头到尾依次遍历,不难找到这个数字,但这样很慢(时间复杂度为O(n)),所以我们换一种思路,先找到一百个数字里最中间的那个数——50(向下取整),很显然,7比50小,这时我们可以将50之后的所有数抹去不看,继续取1-50中最中间的数字——25,很显然7比25小,那么继续,7比13小,继续抹去13后面的数字,此时再取1-13中最中间的数字,正好就是7,我们找到它了。这种算法比遍历要快得多(当然运气不好的情况下也可能和遍历算法一样慢),但我们取它的时间复杂度就是O(logn)。
值得一提,二分查找和单调这一条件是密不可分的,有了单调我们可以考虑使用二分法,而使用二分法时必须要想办法满足单调的条件。
二、代码
1.经典二分查找
在包含size个元素,并且从小到大排列的int数组a里查找元素p,如果找到,则返回元素下标,如果找不到,则返回-1。
int BinarySearch(int a[], int size, int p)
{
int L = 0; //查找区间的左端点
int R = size - 1; //查找区间的右端点
while (L <= R)
{
int mid = L + (R - L)/2; //查找区间正中元素的下标
if (p == a[mid])
return mid;
else if (p > a[mid])
L = mid + 1;
else
R = mid - 1;
}
return -1;
}
注意:计算mid时用L+(R-L)/2而不用(L+R)/2,是为了防止(L+R)过大溢出。
2.二分查找变式
在包含size个元素,并且从小到大排列的int数组a里查找比元素p小,下标最大的元素。如果找到,则返回元素下标,如果找不到,则返回-1。
int LowerBound(int a[], int size, int p)
{
int L = 0;
int R = size - 1;
int lastPos = -1; //到目前为止找到的最优解的下标
while (L <= R)
{
int mid = L + (R - L)/2;
if (a[mid] >= p)
R = mid - 1;
else //a[mid] < p
{
lastPos = mid;
L = mid + 1;
}
}
return lastPos;
}
一、例题
1.找一对数
输入n(n<=100000)个整数,找出其中的两个数,它们之和等于整数m(假定肯定有解)。
①解题思路
- 将数组排序,快速排序复杂度为O(nlogn)
- 对数组中每个元素a[i],在数组中用二分法查找m-a[i],看能否找到。复杂度为O(logn),最坏要查找n-2次,所以查找部分的复杂度也为O(nlogn)
②解题代码
#include<iostream>
#include<algorithm>
using namespace std;
#define MAXSIZE 100000
int n; //一共n个数
int m; //一对数要等于的目标数
int main()
{
cin >> n;
cin >> m;
int a[MAXSIZE];
for (int i = 0; i < n; i++)
{
cin >> a[i];
}
sort(a, a+n); //对数组进行快速排序
for (int i = 0; i < n; i++)
{
int t = m - a[i]; //想要查找的目标
int L = i + 1; //查找区间的左端点
int R =n - 1; //查找区间的右端点
while (L <= R)
{
int mid = L + (R - L) / 2; //查找区间正中元素的下标
if (t == a[mid])
{
cout << a[i] << " " << t << endl; //找到了这一对数,并输出
return 0;
}
else if (t > a[mid])
L = mid + 1;
else
R = mid - 1;
}
}
return -1; //找不到
}
③代码分析
排序部分 复杂度为O(nlogn)
查找部分 每次二分查找复杂度为O(logn),最坏要查找n-2次,所以查找部分的复杂度也为O(nlogn)
所以总的复杂度为O(nlogn)
2.农夫和奶牛
农夫建造了一个很长的畜栏,它包括N(2<=N<=100000)个隔间,这些小隔间的位置X0,…,Xn-1(0<=Xi<=1000000000,均为整数,各不相同)。农夫的C(2<=C<=N)头牛,每头牛分到一个隔间。怎样才能让两头牛之间的最小距离尽可能大,这个最大的最小距离是多少呢?
①解题思路
- 先将隔间坐标进行排序
- 在区间[L,R]中用二分法尝试当前最大最近距离D,D=(L+R)/2,[L,R]的初值为[1,1000000000/C]
- 若当前的D可行,则记住该D,并在[D+1,R]中继续尝试新的D
- 若当前的D不可行,则在[L,D-1]中继续尝试新的D
②解题代码
#include<iostream>
#include<algorithm>
using namespace std;
int n;
int c;
int a[100005];
bool judge(int dis)
{
int num = 1, pre = a[0];
for (int i = 1; i < n; i++)
{
if (a[i] - pre >= dis)
{
num++;
pre = a[i];
if (num == c)
{
return true;
}
}
}
return false;
}
int main(void)
{
cin >> n >> c;
for (int i = 0; i < n; i++)
{
cin >> a[i];
}
sort(a, a + n);
int l = 0, r = 1000000000 / c;
int ans;
while (l <= r)
{
int mid = (l + r) / 2;
if (judge(mid))
{
ans = mid;
l = mid + 1;
}
else
{
r = mid - 1;
}
}
cout << ans << endl;
return 0;
}
③代码分析
查找区间的规模是1000000000/C,所以二分查找的复杂度为O(log1000000000/C),而每次二分查找中需要进行的尝试操作的复杂度为O(n),所以总的复杂度为O(nlog1000000000/C)