二分搜索也叫折半搜索。它是一种高效的算法,应用广泛。下面通过一个例子来介绍二分搜索。
有序数列中查找某数
给出一个升序的整数数列
a0,a1,...,an−1
。问小于等于某个整数
k
的最大数的位置。为了简化问题我们假设这个数组中元素是两两不同的,并且数组中最小的数小于等于
当然我们可以使用朴素算法。
int search(int a[], int n, int k)
{
for (int i = 0; i < n; i++)
if (a[i] > k)
return i - 1;
return n - 1; // 所有数都小于等于 k
}
这个函数返回所求位置。时间复杂度是
O(n)
。
为了提高效率,我们可以考虑折半查找:我们假设要求的位置是
p
。先看看数组中间的元素(
1. 如果
2. 如果
am≤k
,那么所求位置肯定不在
m
之前。这个是因为
我们仔细归纳一下发生了什么。一开始我们可以肯定
p∈[0,n)
。然后进行了上面的判断之后,如果 1 发生了,就可以肯定
p∈[0,m)
;如果 2 发生了,就可以肯定
p∈[m,n)
。总之,通过这次判断,我们把
p
可能在的位置区间缩小了一半。
那么我们对于这剩下的一半区间自然可以故技重施,使得区间再缩减一半。一直重复下去,我们可以肯定很快就可以使得‘有嫌疑’是答案的区间只剩下一个位置。那这个位置自然就是正确结果。
int binary_search(int a[], int n, int k)
{
// [l, r) 是有嫌疑是答案的区间
int l = 0;
int r = n;
for (; (r - l) > 1; ) { // 区间有多个元素时,就要继续循环
// 这个循环体将嫌疑区间缩减至一半
int m = (l + r) / 2; // 区间的中间位置
if (a[m] > k)
r = m; // 区间变为 [l, m)
else l = m; // 区间变为 [m, r)
}
// 现在区间长度是 1,[l, r) 中只有一个位置 l
return l;
}
容易发现这个算法的复杂度是
二分搜索的本质
二分搜索是由上面这个问题引发的算法,但是勿思维僵化,认为二分搜索只是在一列数中查找数的算法。
这个问题的实质有两部分:
1. 答案可以确定在某个区间中
2. 可以通过某种判断将上述的区间缩减成一半。
在引入部分的题目里面,第二步是对中间的数与
k
比较。二分搜索的关键就是确定判断,这个良好的判断排除了一半的答案。
下面通过另一道题目介绍二分搜索更广泛的用法。
最小化最大值
将一个正整数序列
我们设命题
我们考虑如果
C(x)
真,也就是说,
M
还有点可能是更小的,比
反之,也就是
M>x
,说明
x
太小了,答案一定比这个大。
我们首先可以估计一个答案所在的范围,不难发现答案肯定比 0 大,不大于所有数的和小。如果我们可以判断
要解决
C(x)
,应该认识到,
x
越大,越难划分成较多的份数,同时,如果划分出了少于
上述的最小段数其实不难解决。我只要从数列头开始划分,直到这一段不能再多(再多的话和就会超过
x
),则划分出一段;在剩下的部分继续这样划分,就可以求出最小段数。
算法的总复杂度是
这个问题是二分搜索的经典题目。下面这道题跟我的描述几乎一样,请同学们通过这道题。
http://poj.org/problem?id=3273
下面给出另一个经典例题供大家思考,下次课的开始我们会讲这个题目。
最大化性价比
现有
n
种商品,每种商品有其价格
注:设选择的商品价格是
P1,P2,...,Pk
,价值分别是
V1,V2,...,Vk
。性价比指的是
∑Vi∑Pi
。
另外,由于性价比不见得是整数,本题只需要你输出保留小数点后三位输出最高性价比。
本文作者
张静之