二分查找 图解题解
升序数组找目标值,从头挨个扫等于白浪费「有序」这张牌——每次取中间一比,范围对折,O(log n) 就够了。
像猜数字游戏里的高手:每次都猜正中间那个数,对方说「大了」就扔掉右半边,说「小了」就扔掉左半边——每问一次,范围砍一半。100 万个数最多猜 20 次就能锁定,而不是傻乎乎从头一个个试。有序是先决条件,正中间就是每次的「分水岭」,比目标小则答案在右,比目标大则在左,不可能跑到另一边。
这道题到底在问什么
- 输入
- nums=[-1,0,3,5,9,12], target=9
- 输出
- 4 (nums[4]=9)
- 输入
- 同上, target=2
- 输出
- -1 (不存在)
最优解:为什么这么做
一句话答案:LeetCode 704 二分查找:升序数组里找 target,靠有序每次砍掉一半——比中点后大了收右、小了收左,范围收空就返回 -1,时间 O(log n)、空间 O(1)。
有序数组里找一个 target,返回什么
给一个升序(从小到大排好)、无重复数字的数组 nums 和目标值 target:在数组里就返回下标,不在返回 -1。题面例子 nums=[-1,0,3,5,9,12],找 9 返回下标 4,找 2 返回 -1。题目还卡死 O(log n),不许一个个挨着找。
一个个往后比,为什么撑不到 O(log n)
笨办法是从头扫到尾,一个个和 target 比。数组有 n 个数,最坏(target 排末尾或不存在)要比满 n 次,这就是 O(n)(大 O 记号,衡量数据规模增大时操作量的放大倍数)。n 到百万就得比百万次,O(log n) 把这条路堵死了。
凭什么敢一次丢掉一半数不看
钥匙全在『有序』二字上。看正中间的 nums[mid](mid 是中点下标)和 target 比:nums[mid] 小于 target,左边只会更小、够不着,左半连 mid 一起扔掉;大于 target 则右半作废。一比砍掉一半,剩下的才是 target 藏身处,这就是二分查找——每次砍掉一半候选的找法。乱序里左右没规律,砍哪半都可能误伤 target。
看中点该往哪半收,闭区间怎么不越界
用两个指针 l、r 圈住没排除的范围,起初 l=0、r=n-1,两端都算数,这叫闭区间 [l,r]。每轮取中点 mid=l+(r-l)//2 和 target 比:相等直接返回 mid;小于 target,l 跳到 mid+1;大于 target,r 退到 mid-1。
这里藏着一条循环不变量(每一轮都成立的保证):target 若存在,就一定还在 [l,r] 内。收缩必须跨过 mid——比过的 mid 已排除,l 或 r 落到 mid 下一格,范围才真变小。循环条件写 while l<=r,因为 l==r 时闭区间里还剩一格没查。
[-1,0,3,5,9,12] 里找 9、找 2 各几轮
先找 9。l=0、r=5,mid=0+(5-0)//2=2,nums[2]=3 比 9 小,l 跳到 3。第二轮 l=3、r=5,mid=3+(5-3)//2=4,nums[4]=9 相等,返回下标 4,两轮命中。
再找 2。l=0、r=5,mid=2,nums[2]=3 比 2 大,r 退到 1。第二轮 l=0、r=1,mid=0,nums[0]=-1 比 2 小,l 跳到 1。第三轮 l=1、r=1,mid=1,nums[1]=0 比 2 小,l 跳到 2,此时 l 超过 r,范围空了,返回 -1。
缩不动的死循环,卡在 mid 没跨过去
收缩那步最阴:把 l=mid+1 写成 l=mid,比过的 mid 没跨过去,l 卡着不动就成了缩不动的死循环。循环条件也别写成 while l<r,闭区间里 l==r 那一格会被漏掉。mid 写成 (l+r)//2 看着没差,可数组一大 l+r 会超整数上限溢出,得写 l+(r-l)//2 绕开。
复杂度上,每比一次范围减半,n 个数最多 log₂n 次收敛,时间 O(log n);只用 l、r、mid 三个变量,空间 O(1)。百万个数约 20 次比较,比 O(n) 快几个数量级。
▶ 动画逐步走查(共 29 步)——想跟着动画一帧帧对照就展开
- 3记住这条「看中间:小了丢左半、大了丢右半、范围每次减半」,下面每一帧都在套它。
- 4范围用 l 和 r 圈定,灰掉的格子是已经被排除、再也不用看的部分。
- 5看正中间 nums[5]=12,比 9 大。有序数组里它右边只会更大,所以 9 只可能在它左边。
- 6把右半(含中点 5)整段灰掉排除,r 跳到 4。一次砍掉一半,范围缩到 [0, 4]。
- 7看正中间 nums[2]=3,比 9 小。有序数组里它左边只会更小,所以 9 只可能在它右边。
- 8把左半(含中点 2)整段灰掉排除,l 跳到 3。一次砍掉一半,范围缩到 [3, 4]。
- 9看正中间 nums[3]=5,比 9 小。有序数组里它左边只会更小,所以 9 只可能在它右边。
- 10把左半(含中点 3)整段灰掉排除,l 跳到 4。一次砍掉一半,范围缩到 [4, 4]。
- 11范围正中间 nums[4]=9 正好等于 9,命中!返回下标 4。
- 12命中的格子绿色高亮。11 个数最多比 4 次(log₂11≈3.5),这就是 O(log n) 的威力。
- 13找不到的情形同样靠砍半收敛:当 l 超过 r、范围空了,就返回 -1。
- 14中间 nums[5]=12 比 2 大,目标若存在只能在左半。
- 15灰掉右半,r → 4,范围缩到 [0, 4]。
- 16中间 nums[2]=3 比 2 大,目标若存在只能在左半。
- 17灰掉右半,r → 1,范围缩到 [0, 1]。
- 18中间 nums[0]=-1 比 2 小,目标若存在只能在右半。
- 19灰掉左半,l → 1,范围缩到 [1, 1]。
- 20中间 nums[1]=0 比 2 小,目标若存在只能在右半。
- 21收缩后 l=2 超过了 r=1,范围空了——说明 2 根本不存在,返回 -1。
- 22所有格子都被排除、范围空掉,确认没有这个数。找不到也只用 O(log n) 次比较。
- 23目标偏大时,每次都丢掉左半,范围像滚雪球一样向右收紧。
- 24看正中间 nums[5]=12,比 33 小。有序数组里它左边只会更小,所以 33 只可能在它右边。
- 25把左半(含中点 5)整段灰掉排除,l 跳到 6。一次砍掉一半,范围缩到 [6, 10]。
- 26看正中间 nums[8]=23,比 33 小。有序数组里它左边只会更小,所以 33 只可能在它右边。
- 27把左半(含中点 8)整段灰掉排除,l 跳到 9。一次砍掉一半,范围缩到 [9, 10]。
- 28看正中间 nums[9]=28,比 33 小。有序数组里它左边只会更小,所以 33 只可能在它右边。
- 29把左半(含中点 9)整段灰掉排除,l 跳到 10。一次砍掉一半,范围缩到 [10, 10]。
- 30范围正中间 nums[10]=33 正好等于 33,命中!返回下标 10。
- 31命中的格子绿色高亮。11 个数最多比 4 次(log₂11≈3.5),这就是 O(log n) 的威力。
⚠️ 容易写错的地方
✗ 错:mid = (l + r) / 2
✓ 对:mid = l + (r - l) / 2
l+r 在超大数组里可能整数溢出,这种写法避免溢出
✗ 错:while (l < r)
✓ 对:while (l <= r)
闭区间 [l,r] 下漏判 l==r 那一格,会错过最后一个候选
✗ 错:收缩写成 l = mid / r = mid
✓ 对:应是 l = mid + 1 / r = mid - 1
mid 已比较过、不是答案,不+1/-1 会死循环
完整代码(Python / C++ / Java)
Python
def search(nums, target):
l, r = 0, len(nums) - 1
while l <= r:
mid = l + (r - l) // 2 # 防溢出
if nums[mid] == target:
return mid
elif nums[mid] < target:
l = mid + 1 # 丢左半
else:
r = mid - 1 # 丢右半
return -1C++
int search(vector<int>& nums, int target){
int l = 0, r = nums.size() - 1;
while(l <= r){
int mid = l + (r - l) / 2; // 防溢出
if(nums[mid] == target) return mid;
else if(nums[mid] < target) l = mid + 1; // 丢左半
else r = mid - 1; // 丢右半
}
return -1;
}Java
public int search(int[] nums, int target) {
int l = 0, r = nums.length - 1;
while (l <= r) {
int mid = l + (r - l) / 2; // 防溢出
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
l = mid + 1; // 目标在右半,丢左半
} else {
r = mid - 1; // 目标在左半,丢右半
}
}
return -1;
}复杂度
时间
O(log n)
每比一次范围减半,n 个数最多 log₂n 次
空间
O(1)
只用 l、r、mid 三个变量
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 二分查找 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么必须是有序数组,乱序能用二分吗?+
不能。二分每一步都靠『nums[mid] 比 target 小,那 mid 左边只会更小、可以整片丢掉』这个推断来砍半,而这个推断只在升序(或降序)时成立。数组一乱,一个数左右两边大小没有规律,被砍掉的那半里完全可能就藏着 target,结果会漏找。若真遇到乱序又要反复查找,通常先花 O(n log n) 排好序,再用二分。
mid 为什么写成 l+(r-l)//2,直接 (l+r)//2 不是更省事?+
两种写法数学上相等,但 (l+r)//2 在 l、r 都很大时,l+r 可能超出整数能表示的上限(溢出),算出负数或错误下标。l+(r-l)//2 先求区间长度 r-l 再折半加回 l,中间量始终不超过原来的 r,天然避开溢出。Python 整数没有上限,感受不到;C++、Java 里这就是必写的防溢出姿势。
循环条件到底用 while l<=r 还是 l+
差在只剩一个数时查不查。这份代码用闭区间 [l,r],l 和 r 指的格子都还没排除,当 l==r 时那一格仍需比对,所以必须 l<=r 才进循环;写成 l
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 二分查找 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。