374. 猜数字大小
问题描述
我们正在玩一个猜数字的游戏。游戏规则如下:
- 我从 1 到 n 之间选择一个数字。
- 你来猜我选的是哪个数字。
- 每次你猜错了,我会返回 3 个可能的结果之一:
-1
:我的数字比较小,即我选的数字比你猜的小1
:我的数字比较大,即我选的数字比你猜的大0
:恭喜!你猜对了!
设计一个算法来找到我选定的数字,要求调用 guess
API 的次数最少。
示例:
输入: n = 10, pick = 6
输出: 6
输入: n = 1, pick = 1
输出: 1
算法思路
二分查找法:
- 使用二分查找在区间 [1, n] 中搜索目标数字
- 每次猜测区间的中点值
- 根据
guess
函数的返回值调整搜索范围 - 当
guess
返回 0 时,找到目标数字
核心思想:每次猜测都能将搜索空间缩小一半,确保在 O(log n) 时间内找到答案。
代码实现
方法一:标准二分查找(推荐解法)
public class Solution extends GuessGame {
/**
* 使用二分查找猜数字
*
* @param n 数字范围上限(1 到 n)
* @return 猜中的数字
*/
public int guessNumber(int n) {
// 初始化搜索区间:[left, right]
int left = 1; // 左边界(包含)
int right = n; // 右边界(包含)
// 二分查找主循环
while (left <= right) {
// 计算中点,避免整数溢出
// 使用 left + (right - left) / 2 而不是 (left + right) / 2
int mid = left + (right - left) / 2;
// 调用 guess API 进行猜测
int result = guess(mid);
// 根据返回结果调整搜索范围
if (result == 0) {
// 猜对了!返回答案
return mid;
} else if (result == -1) {
// 我的数字比较小,说明目标在左半部分
// 注意:mid 已经被排除,所以右边界为 mid - 1
right = mid - 1;
} else { // result == 1
// 我的数字比较大,说明目标在右半部分
// mid 已经被排除,所以左边界为 mid + 1
left = mid + 1;
}
}
// 理论上不会到达这里,因为一定存在答案
// 但为了语法完整性,返回-1表示未找到
return -1;
}
}
方法二:递归二分查找
public class Solution extends GuessGame {
/**
* 递归实现二分查找猜数字
*
* @param n 数字范围上限
* @return 猜中的数字
*/
public int guessNumber(int n) {
return binarySearch(1, n);
}
/**
* 递归辅助函数:在 [left, right] 区间内二分查找
*
* @param left 搜索区间的左边界(包含)
* @param right 搜索区间的右边界(包含)
* @return 找到的数字
*/
private int binarySearch(int left, int right) {
// 递归终止条件
if (left > right) {
return -1; // 未找到(理论上不会发生)
}
// 计算中点
int mid = left + (right - left) / 2;
int result = guess(mid);
if (result == 0) {
// 猜对了
return mid;
} else if (result == -1) {
// 目标在左半部分
return binarySearch(left, mid - 1);
} else {
// 目标在右半部分
return binarySearch(mid + 1, right);
}
}
}
方法三:优化的二分查找(处理边界情况)
public class Solution extends GuessGame {
/**
* 优化:特别处理边界情况,减少不必要的API调用
*
* @param n 数字范围上限
* @return 猜中的数字
*/
public int guessNumber(int n) {
// 特殊情况:n=1时直接返回
if (n == 1) {
return 1;
}
int left = 1;
int right = n;
while (left < right) {
// 使用位运算优化除法(可选)
int mid = left + ((right - left) >> 1);
int result = guess(mid);
if (result == 0) {
return mid;
} else if (result == -1) {
right = mid - 1;
} else {
left = mid + 1;
}
}
// 当 left == right 时,只剩一个候选数字
// 需要最后验证一次
return guess(left) == 0 ? left : -1;
}
}
算法分析
-
时间复杂度:O(log n)
- 每次都将搜索空间减半,最多需要 log₂n 次猜测
- 例如 n=1000 时,最多只需要 10 次猜测
-
空间复杂度:
- 迭代版本:O(1),只使用常数额外空间
- 递归版本:O(log n),递归调用栈的深度
-
API调用次数:最多 ⌊log₂n⌋ + 1 次
算法过程
n = 10, pick = 6
:
- 初始:
left=1, right=10
- 第1轮:
mid = 1 + (10-1)/2 = 5
guess(5)
→ 返回 1(目标更大)left = 6, right = 10
- 第2轮:
mid = 6 + (10-6)/2 = 8
guess(8)
→ 返回 -1(目标更小)left = 6, right = 7
- 第3轮:
mid = 6 + (7-6)/2 = 6
guess(6)
→ 返回 0(猜对了!)- 返回 6
总共调用 guess
API 3 次。
测试用例
// 注意:以下测试代码需要在实际环境中运行
// 因为 guess() 方法由系统提供
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准情况
int result1 = solution.guessNumber(10);
// 假设系统选择的数字是6
System.out.println("Test 1: " + result1); // 期望输出: 3
// 测试用例2:最小情况
int result2 = solution.guessNumber(1);
System.out.println("Test 2: " + result2); // 期望输出: 1
// 测试用例3:较大范围
int result3 = solution.guessNumber(100);
// 假设系统选择的数字是50
System.out.println("Test 3: " + result3); // 期望输出: 1
// 测试用例4:边界值
int result4 = solution.guessNumber(2);
// 假设系统选择的数字是2
System.out.println("Test 4: " + result4); // 期望输出: 2
}
关键点
-
避免整数溢出:
- 使用
left + (right - left) / 2
而不是(left + right) / 2
- 当
left
和right
都很大时,left + right
可能溢出
- 使用
-
循环条件:
- 使用
while (left <= right)
确保所有情况都被覆盖 - 当
left == right
时仍需检查
- 使用
-
边界更新:
result == -1
时:right = mid - 1
(排除mid)result == 1
时:left = mid + 1
(排除mid)
-
中点计算:
- 推荐使用
left + (right - left) / 2
- 或使用位运算
left + ((right - left) >> 1)
提升效率
- 推荐使用
常见问题
-
为什么不能用线性搜索?
- 时间复杂度 O(n),在 n 很大时效率极低
- 二分查找 O(log n) 是最优解
-
如何处理 guess() 返回值异常?
- 题目保证返回值只会是 -1, 0, 1
- 实际编码中可添加异常处理
-
数字范围从0开始怎么办?
- 算法同样适用,只需调整初始边界为
[0, n]
- 算法同样适用,只需调整初始边界为
-
能否用三分查找?
- 理论上可以,但二分查找已经是最优
- 三分查找的期望次数更多
-
API调用次数有上限吗?
- LeetCode 通常有调用次数限制
- 二分查找能保证最少的调用次数
扩展思考
-
如果 guess() 函数有时会返回错误结果?
- 需要设计容错算法,如多次验证
-
如果搜索空间不是连续的?
- 需要先排序或使用其他数据结构
-
如何最小化最坏情况下的猜测次数?
- 二分查找已经是最优策略
-
如果允许自定义猜测策略?
- 可以根据先验知识调整搜索策略