一、什么时候使用二分?:
大部分人接触二分应该二分查找算法,二分查找可以在一个有序区间中,找到指定的元素x的下标。
这里有两点:1、区间是有序的,即区间要满足一定条件。2、我们要找的数也有一定条件,二分查找要求找的数一定要等于x。
我们再看两题:
题目给出的是按照升序排列的整数数组,要求一个元素k的起始终止位置,也就是第一个等于k的数和最后一个等于k的数。
题目给出一个山脉数组,即在峰顶元素左边升序排列,右边降序,可以算是半有序的状态。要找的就是峰顶元素。
可以看到,都满足我们提出的两个条件。当我们看到一个有一定条件分布的数组,要我们寻找一个较为特殊的值时,我们就可以考虑使用二分来做。
二、二分模板:
大部分人最早接触二分算法应该都是二分查找算法,但是二分能做的远不止与此。二分算法的核心在于一个check(mid)
函数,只要这个函数能够将给定的序列划分为两个区间,一个满足check(mid),一个不满足。就可以根据模板查找目标值。
二分总体上可以分为两个模板,需要根据情况选择使用。
1、模板一:
int binary_search() {
int l = 0, r = n - 1;
while (l < r) {
int mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
2、模板二:
int binary_search() {
int l = 0, r = n - 1;
while (l < r) {
int mid = (l + r + 1) / 2;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
两个模板的关键都在4~6行,即如何划分区间。模板一会将区间划为([l, mid]和[mid+1, r]),而模板二会将区间划为([l, mid-1]和[mid, r])。这就是模板最重点的区别。
模板上实际也是可以互换的
-
两个模板最后返回时,都有(l ==r),且(l、r)都等于划分的第一个区间的右端点,即mid或mid - 1。
-
模板二求mid时需要+1,这是为了防止死循环。
三、模板使用:
下面画图说明两个模板的区别和应用:
如果只想记住模板直接使用,有一个很好理解的方法。我们设xxx表示不满足check(mid)的序列元素,ooo表示满足,v表示我们需要的数。假设题目要求找到0到n之间的>5的第一个数,我们的序列类似这样。
0 1 2 3 4 5 6 7 8 9 10 ... n
x x x x x x v o o o o ... o
可以看到,序列被划分成左边不满足、右边满足。我们要找的数就是第一个满足check(mid)的值,面对这样的划分区间,就可以套用模板一。
假设要找 <= 5的最后一个数,我们的序列如下:
0 1 2 3 4 5 6 7 8 9 10 ... n
o o o o o v x x x x x ... x
序列左边满足、右边不满足check(mid)。v就是最后一个满足check(mid)的值,这样的划分区间就可以套用模板二。
最后总结一下二分的思路:
- 首先把序列写出来,判断题目要求的是满足某一要求的第一个值而是最后一个值。
- 接着根据要求画出这样的划分情况分析。
- 如果形如x x x x v o o o,选择模板一,形如o o o v x x x,选择模板二。
三、思考:
我们知道模板一模板二最后的l、r值是相同的,在模板一、二中分别等于mid、mid-1。两个模板也是可以相互转换的,第一个不满足check(mid)的值也就是最后一个满足check(mid)的值的下一个值。因此模板二的check(mid)改称与模板一的完全相反的判断函数,最后模板二返回l+1,就可以得到与模板一相同的答案。但是不建议这样使用。
其次check(mid)
函数选择还是有一定技巧的,如果不考虑像上面那样更改模板。那么check(mid)
就决定了我们该使用哪个模板。