一、二分查找基础:别再从头一个个数了
1.1 二分查找的概念与原理:像个心机的“猜数字”玩家
在计算机科学的江湖里,二分查找就像是一个“心机Boy”。它绝不干那种从头开始傻乎乎挨个数的体力活,而是专门在有序数组这个规矩的圈子里,用最少的步骤精准KO目标。
它还有个名字叫“折半查找”,听着挺高大上,其实就是“对半砍”。它的核心哲学是:既然队伍是排好序的,那我就直接跳到队伍中间,问一句:“喂,目标在你们那边还是这边?”
举个栗子,假设我们有一个有序数组 [1, 3, 5, 7, 9, 11, 13],我们要找 15。
普通人的做法是:1?不是。3?不是。5?不是……(累死)。
二分查找的做法是:直接站在中间(9的位置),大喊一声:“15在哪?”
因为 9 < 15,数组这头太小了,目标肯定在右边那群“高个子”里。于是,它直接把左边一半人“咔嚓”划掉,不管了。
接着在右半场 [11, 13] 中间站,再喊:“15在哪?”
13 还是小于 15,继续把 13 左边的(虽然没剩几个了)划掉。
最后发现没人了,得出结论:15 这家伙根本不在这个数组混。
这招数叫“剥洋葱”有点太温柔了,我觉得叫“切西瓜”更贴切。每次一刀下去,不要的那一半直接扔进垃圾桶,效率极高。相比于普通人 O(n) 的线性查找(数据多十倍,时间多十倍),二分查找是 O(logn) 的(数据多十倍,时间只多一点点),简直是算法界的“流氓”。
1.2 二分查找的实现步骤:别把边界写崩了
写二分查找代码,就像在走钢丝,一不小心就会“摔死”(死循环或数组越界)。
首先,我们要划定地盘:左边界 left = 0,右边界 right = n-1。
然后进入循环,只要 left <= right,就说明还有地盘可搜。
中间位置 mid 怎么算?你可能会写 (left + right) / 2。停!别这么干!如果 left 和 right 都是接近内存上限的大数,它们一加直接溢出变成负数,程序就崩了。我们要用点黑科技:mid = left + (right - left) / 2。这样先减后加,稳如老狗。
算出 mid 后,就拿 arr[mid] 和目标值比:
- 如果相等,恭喜你,抓到了,直接 return。
- 如果 arr[mid] < target,说明目标在右边,左边界收缩:left = mid + 1。
- 如果 arr[mid] > target,说明目标在左边,右边界收缩:right = mid - 1。
很多新手在这里容易犯“手抖病”,写成 left = mid 或 right = mid。千万别!因为 mid 位置已经比过了,肯定不是目标,再包含它就是浪费时间,甚至导致死循环。一定要把比过的点“剔除”出去。
二、二分查找的应用场景:不仅仅是找数
2.1 在有序数组中查找元素:精准打击
还是那个数组 [1, 3, 5, 7, 9, 11, 13],找 7。
开局:left=0, right=6。
第一刀:mid=3, 值是 5。5 < 7,所以把 5 及其左边全扔了,left = 4。
第二刀:mid=5, 值是 11。11 > 7,把 11 及其右边全扔了,right = 4。
第三刀:mid=4, 值是 7。Bingo!
你看,7个数只用了3次就找到了。如果数组有一亿个数,二分查找最多也只需要找大约27次(因为 2^27 约等于一亿多)。这种效率,让暴力查找只能跪下唱《征服》。
2.2 寻找插入位置:给新来的找个座
有时候我们不需要找存在的数,而是想知道如果要把一个数插进去,应该插在哪,才能不破坏数组的有序性。
比如在 [1, 3, 5, 7, 9, 11, 13] 中插入 8。
按照二分逻辑:
mid=3, 值是 5。5 < 8,说明 8 应该在右边,left = 4。
mid=5, 值是 11。11 > 8,说明 8 应该在左边(但包含11的位置),right = 4。
此时 left=4, right=4。
mid=4, 值是 7。7 < 8,所以 left = mid + 1 = 5。
现在 left=5, right=4,循环结束。
注意!此时 left 的值就是插入点。因为当循环结束时,left 指向的是第一个比目标值大的位置。所以 8 应该插在下标 5 的位置(也就是 9 的前面),完美!
三、二分答案入门:当“猜数字”变成了“猜答案”
3.1 二分答案的定义与思想:暴力穷举的“作弊”版
如果说二分查找是在“有序的数组”里找数,那二分答案就是在“有序的答案范围”里找解。
它就像是一个考试只会在选择题里蒙答案的老油条。题目很难,不会做?没关系,只要知道答案肯定在 0 到 100 之间,而且具有单调性(比如猜大了就不行,猜小了就行),那就可以二分答案。
经典例子:切绳子。
有一堆绳子,长度分别是 [5, 8, 10],我要把它们切成 6 段,问每段最长能有多长?
普通人可能会从 1cm 开始试:切 1cm 能切很多段,行;切 2cm 也能行……一直试到切不动为止。这叫线性查找,太慢。
二分答案玩家会这样想:每段长度肯定在 0 到最长绳子(10)之间。
我先猜中间值 5cm。
- 切 5cm:5/5=1段,8/5=1段,10/5=2段,总共 4 段。不够 6 段!说明切大了,得切小点。
于是把右边界缩小到 5。
再猜中间值 2cm。 - 切 2cm:5/2=2段,8/2=4段,10/2=5段,总共 11 段。大于 6 段!说明切小了,可以试着切大点。
于是把左边界扩大到 2。
如此反复,很快就能逼近那个“刚刚好能切出6段”的最大长度。
核心逻辑是:如果 x 可行,那么所有比 x 小的都可行;如果 x 不可行,所有比 x 大的都不可行。利用这种单调性,我们就能二分。
3.2 二分答案适用的问题类型:当“暴力”太丑陋时
当你遇到以下类型的问题,且数据量很大(n 到 10^5 或更大)时,二分答案往往是救星:
- “求最大值最小”或“求最小值最大”。比如“让最短的那段尽量长”。
- “判断是否存在一个方案满足条件”,且方案具有单调性。
- 传统的暴力枚举(O(n) 或 O(n^2))会超时,但验证一个猜测的复杂度很低(比如 O(n))。
四、二分答案的经典案例:从理论到实战
4.1 数的范围问题:寻找边界
在数组 [1, 2, 2, 3, 4, 5, 5, 5, 6, 7, 8, 9] 中找 5 的起始和结束位置。
找起始位置(左边界):
我们二分查找,当 arr[mid] == 5 时,不要急着返回,因为可能左边还有。我们要把搜索范围强行拉到左边:right = mid - 1。
当循环结束时,left 通常就是第一个 5 的位置。
找结束位置(右边界):
同样二分,当 arr[mid] == 5 时,去右边找:left = mid + 1。
循环结束时,right 通常就是最后一个 5 的位置。
这叫“二分下界的技巧”,核心在于等于的时候不要停,要继续往左(或右)逼近,直到把边界“挤”出来。
4.2 木材加工问题:实数二分的尴尬
有一堆木材 [5, 8, 10, 12, 15, 18, 20],要切出 10 段等长的木头,求最长长度。
这和切绳子一样。但注意,长度可能是小数(比如 8.333米)。
这时候整数二分的 left <= right 就不好用了,因为实数是连续的。
我们需要设定一个精度,比如 1e-6(0.000001)。
循环条件变成:while (right - left > 1e-6)
中间值 x = (left + right) / 2。
验证 x 是否可行,调整边界。
最后输出 left 或 right 或它们的平均值即可。实数二分的关键在于控制好精度,别让计算机去死磕两个无限接近的数。
五、二分查找与二分答案的对比:表兄弟的异同
5.1 相同点与联系:换汤不换药
这俩货本质上是一回事,核心都是“分治”和“单调性”。
代码长得几乎一模一样:都有 left、right、mid,都有 while 循环,都要根据条件收缩边界。
时间复杂度都是 O(logn),都是效率极高的算法。
二分查找可以说是二分答案的一个特例:二分查找是在数组下标这个“有序序列”里二分;二分答案是在“解空间”里二分。
5.2 不同点与区别:找的东西不一样
二分查找找的是“存在的东西”。数组里必须有这个数,你才能找到它。
二分答案找的是“最优的东西”。这个答案可能在原数组里根本不存在,它是通过逻辑推导出来的。
二分查找的逻辑通常是:比较大小,相等就赢了。
二分答案的逻辑通常是:模拟验证,可行就往大了猜,不可行就往小了猜。
六、二分查找与二分答案的优化技巧:别让细节坑了你
6.1 边界条件的处理:死循环的元凶
写二分最怕死循环。比如在找左边界时,如果写成了 left = mid,当 left 和 right 相邻时,mid 算出来等于 left,赋值后 left 还是等于 mid,这就死循环了。
记住口诀:查找时,不包含 mid(+1/-1);收缩时,保留 mid(因为可能就是解)。
对于整数溢出,坚决使用 left + (right - left) / 2。
对于实数二分,不要用 == 判断,要用精度控制。
6.2 时间复杂度的优化:常数优化
虽然复杂度都是 O(logn),但能快一点是一点。
计算 mid 时,位运算比除法快:mid = left + ((right - left) >> 1)。虽然现代编译器通常会自动优化,但写出来显得你懂汇编。
如果验证一个猜测的代价很高(比如 O(n)),可以尝试优化验证函数的逻辑,或者利用数据结构(如前缀和)把验证复杂度降下来。
七、实际应用案例:这玩意儿真能赚钱
7.1 在数据库查询中的应用:索引的底层逻辑
为什么数据库给字段加了索引后查询飞快?因为索引(如B+树)底层就是利用了二分的思想。
没有索引,查100万条数据可能要扫全表。
有索引,查100万条数据大概只需要 20 次磁盘IO(log2(1000000) ≈ 20)。
这就是二分查找改变世界的地方。下次老板问你为什么要加索引,你就说这是“二分法的降维打击”。
7.2 在算法竞赛中的应用:上分神器
在ACM/ICPC或者LeetCode周赛中,二分答案是中等题和困难题的常客。
遇到“最大化最小值”这种绕口令一样的题目,别犹豫,直接套二分答案模板。
设定 low 和 high,写个 check 函数,然后二分。
只要 check 函数逻辑正确,这道题基本就稳了。这招被称为“二分答案大法好”,是竞赛选手从暴力选手进化成优雅选手的标志。
总之,二分查找和二分答案虽然初看有点绕,但一旦掌握了“单调性”和“边界处理”这两个核心,你会发现它们是算法世界里最简单、最暴力、最高效的工具。别再暴力 for 循环了,学会二分,你就是整条街最靓的仔!