猜数字大小 图解题解
这道题到底在问什么
- 输入
- n=10, pick=6
- 输出
- 6
最优解:为什么这么做
一句话答案:LeetCode 374 猜数字大小:1 到 n 里猜系统选的 pick,每问一次 guess 回偏大、偏小或命中。二分按反馈把候选区间减半,最少次数锁定 pick,时间 O(log n)、空间 O(1)。
只靠偏大偏小的回复,怎么锁定 pick
系统在 1 到 n 里偷偷选了一个数 pick。你能做的只有一件事:挑一个数 m 去问 guess(m),它回 1 表示 m 比 pick 小(猜小了)、回 −1 表示 m 比 pick 大(猜大了)、回 0 表示命中。看不到答案、只能靠每次回复一点点逼近,这种方式叫交互反馈。题目要的不是随便猜中,而是用尽量少的次数问出 pick。
从 1 开始一个个问,为什么会被卡死
笨办法是从 1 挨个问:guess(1)、guess(2)、guess(3)……直到返回 0。一定能中,可 pick 若靠近 n 就得问将近 n 次,n 到十亿这条线性扫(从头一个个试)就慢得没法接受。更亏的是,guess 明明告诉了你「偏大还是偏小」,挨个问却只用「中没中」,方向信息全浪费。
偏大偏小这条回复,凭什么能砍掉一半
关键在于 1 到 n 本身从小到大排好。挑正中间的数 m 去问,guess 的回复直接圈定 pick 在哪半边:回 1 说明 pick 比 m 大,m 连同左边整段排除;回 −1 说明 pick 比 m 小,m 连同右边全排除。一问就砍掉一半候选,这正是二分查找(每问一次,候选就减半的找法)能用的前提。
区间怎么收、mid 怎么取才不会漏掉 pick
用 lo、hi 两头框住还没排除的候选,起初 lo=1、hi=n,闭区间 [lo, hi](两端都算数)里自始至终含着 pick。每轮取中点 mid = lo + (hi − lo) // 2,问 guess(mid):回 0 就返回 mid;回 1 说明 pick 更大,lo = mid + 1;回 −1 说明 pick 更小,hi = mid − 1。收缩必须 ±1——mid 已问过、不是答案,得跨过它,否则区间缩不动、一直空转。lo 超过 hi 时候选空了,循环停。
拿 n=10、pick=6 逐轮把区间夹到底
拿题面的 n=10、pick=6 走一遍。第一轮 lo=1、hi=10,mid = 1 + (10 − 1) // 2 = 5,guess(5) 返回 1(5 比 6 小),pick 在右半,lo 提到 6。第二轮 lo=6、hi=10,mid = 6 + (10 − 6) // 2 = 8,guess(8) 返回 −1(8 比 6 大),pick 在左半,hi 退到 7。第三轮 lo=6、hi=7,mid = 6 + (7 − 6) // 2 = 6,guess(6) 返回 0,命中返回 6。十个数只问 3 次就锁定,每轮都砍掉一半。
认反 guess 的方向,越问越偏
每问一次候选减半,最多约 log₂n 次收敛,时间 O(log n);只用 lo、hi、mid 三个记号,空间 O(1)。最坑是 guess 方向记反:返回 1 是猜小了、该 lo = mid + 1,反着写越问越偏。收缩漏 ±1、写成 lo = mid,mid 不挪、循环卡死。mid 写 (lo+hi)//2 在 n 大时溢出,换 lo + (hi − lo) // 2 才稳;n=1 一问即中,两端值也照常收敛。
▶ 动画逐步走查(共 13 步)——想跟着动画一帧帧对照就展开
- 3记住「取中点 mid → guess → 偏小 lo=mid+1、偏大 hi=mid−1、命中即停」,下面每帧都在套它。
- 4还没被排除的范围是 [1, 12](灰格已排除)。取中点 mid = 6(用整除 (12−1)//2 向下取整:候选个数为奇数时正好是正中间,偶数时取靠左那个),下一步去问 guess(6)。
- 5guess(6) 返回 1。返回 1 表示猜的数比 pick 小,也就是 pick 比 6 大,答案落在它右边。
- 6既然 pick 比 6 大,1 到 6(含刚问过的 6)全部排除、标灰,lo 跳到 7。左半被砍掉,范围缩成 [7, 12]。
- 7还没被排除的范围是 [7, 12](灰格已排除)。取中点 mid = 9(用整除 (12−7)//2 向下取整:候选个数为奇数时正好是正中间,偶数时取靠左那个),下一步去问 guess(9)。
- 8guess(9) 返回 1。返回 1 表示猜的数比 pick 小,也就是 pick 比 9 大,答案落在它右边。
- 9既然 pick 比 9 大,7 到 9(含刚问过的 9)全部排除、标灰,lo 跳到 10。左半被砍掉,范围缩成 [10, 12]。
- 10还没被排除的范围是 [10, 12](灰格已排除)。取中点 mid = 11(用整除 (12−10)//2 向下取整:候选个数为奇数时正好是正中间,偶数时取靠左那个),下一步去问 guess(11)。
- 11guess(11) 返回 -1。返回 −1 表示猜的数比 pick 大,也就是 pick 比 11 小,答案落在它左边。
- 12既然 pick 比 11 小,11 到 12(含刚问过的 11)全部排除、标灰,hi 退到 10。右半被砍掉,范围缩成 [10, 10]。
- 13还没被排除的范围是 [10, 10](灰格已排除)。取中点 mid = 10(用整除 (10−10)//2 向下取整:候选个数为奇数时正好是正中间,偶数时取靠左那个),下一步去问 guess(10)。
- 14guess(10) 返回 0。正好等于 pick,猜中了!
- 1510 就是 pick,绿色标出,二分结束。回头看,从 12 个数里只问了 4 次就锁定了它,每次都砍掉一半,这就是二分的威力。
⚠️ 容易写错的地方
✗ 错:写 mid = (left+right)/2
✓ 对:mid = left + (right−left)//2(整除)
left+right 可能溢出,这种写法等价又安全;中点用整除向下取整
✗ 错:guess 返回值方向搞反
✓ 对:返回 1=猜小了 pick 更大往右、−1=猜大了 pick 更小往左
方向反了会越猜越偏甚至死循环
✗ 错:闭区间忘了 ±1
✓ 对:lo=mid+1 / hi=mid−1
mid 已经问过、必须排除,否则可能死循环
完整代码(Python / C++ / Java)
Python
PICK = 1
def guess(num: int) -> int:
if num == PICK:
return 0
return 1 if num < PICK else -1
class Solution:
def guessNumber(self, n: int) -> int:
left, right = 1, n
while left <= right:
mid = left + (right - left) // 2
g = guess(mid)
if g == 0:
return mid
if g > 0:
left = mid + 1
else:
right = mid - 1
return -1C++
using namespace std;
int PICK = 1;
int guess(int num) {
if (num == PICK) return 0;
return num < PICK ? 1 : -1;
}
class Solution {
public:
int guessNumber(int n) {
int left = 1, right = n;
while (left <= right) {
int mid = left + (right - left) / 2;
int g = guess(mid);
if (g == 0) return mid;
if (g > 0) left = mid + 1;
else right = mid - 1;
}
return -1;
}
};Java
class GuessGame {
static int pick = 1;
int guess(int num) {
if (num == pick) return 0;
return num < pick ? 1 : -1;
}
}
class Solution extends GuessGame {
public int guessNumber(int n) {
int left = 1, right = n;
while (left <= right) {
int mid = left + (right - left) / 2;
int g = guess(mid);
if (g == 0) return mid;
if (g > 0) left = mid + 1;
else right = mid - 1;
}
return -1;
}
}复杂度
时间
O(log n)
每问一次范围减半,最多约 ceil(log2(n+1)) 次就收敛,即 O(log n) 次
空间
O(1)
只用 left、right、mid 几个变量
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 猜数字大小 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
guess 的返回值方向老是记反,有没有办法锁死?+
记住一步翻译:返回 1 是「你猜的数偏小」,既然 m 偏小,pick 就在 m 右边,该把 lo 提到 mid + 1;返回 −1 是「偏大」,pick 在左边,hi 退到 mid − 1。每次先把返回值念成「猜小了/猜大了」,再据此定往哪半收,就不会把 lo、hi 写反。别死记数字和方向的对应,记那句话。
这题和普通在有序数组里二分查找有什么区别?+
骨架完全一样,都是闭区间 [lo, hi] 每轮取中点、按比较结果收掉一半。区别只在「拿什么做比较」:普通二分是 nums[mid] 和 target 比大小,这题没有数组,改成调 guess(mid) 拿系统反馈。把 guess 看成一个不告诉你具体值、只告诉你大小方向的比较器,闭区间二分原样照搬就行。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 猜数字大小 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。