

文章目录
-
- 摘要
- 描述
- 题解答案
- 题解代码分析
-
- [1. 二分查找的基本思路](#1. 二分查找的基本思路)
- [2. 循环查找目标值](#2. 循环查找目标值)
- [3. 计算中间值(避免溢出)](#3. 计算中间值(避免溢出))
- [4. 调用 guess 函数并处理结果](#4. 调用 guess 函数并处理结果)
- [5. 边界情况处理](#5. 边界情况处理)
- [6. 为什么这个算法能工作?](#6. 为什么这个算法能工作?)
- [7. 算法的时间复杂度分析](#7. 算法的时间复杂度分析)
- 示例测试及结果
-
- [示例 1:n = 10, pick = 6](#示例 1:n = 10, pick = 6)
- [示例 2:n = 1, pick = 1](#示例 2:n = 1, pick = 1)
- [示例 3:n = 2, pick = 1](#示例 3:n = 2, pick = 1)
- [示例 4:n = 100, pick = 50](#示例 4:n = 100, pick = 50)
- [示例 5:n = 100, pick = 1](#示例 5:n = 100, pick = 1)
- 时间复杂度
- 空间复杂度
- 实际应用场景
- 总结
- [完整可运行 Demo 代码](#完整可运行 Demo 代码)
- 标题
摘要
这道题其实挺经典的,它模拟了一个猜数字的游戏。游戏规则很简单:系统从 1 到 n 中随机选一个数字,你需要通过调用 guess 函数来猜这个数字。每次猜测后,函数会告诉你猜大了、猜小了还是猜对了。我们的目标就是用最少的次数找到这个数字。
这道题本质上是一个二分查找问题,虽然题目描述看起来像游戏,但核心算法就是经典的二分查找。今天我们就用 Swift 来搞定这道题,顺便聊聊二分查找在实际开发中的应用场景。

描述
题目要求是这样的:我们正在玩猜数字游戏。系统会从 1 到 n 随机选择一个数字,这个数字在整个游戏中保持不变。你需要猜出这个数字是多少。
你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有三种可能的情况:
-1:你猜的数字比我选出的数字大(即num > pick)1:你猜的数字比我选出的数字小(即num < pick)0:你猜的数字与我选出的数字相等(即num == pick)
返回我选出的数字。
示例 1:
输入: n = 10, pick = 6
输出: 6
示例 2:
输入: n = 1, pick = 1
输出: 1
示例 3:
输入: n = 2, pick = 1
输出: 1
提示:
1 <= n <= 2^31 - 11 <= pick <= n
这道题的核心思路是什么呢?其实很简单,就是二分查找。我们每次猜中间的数字,如果猜大了,就往小的方向找;如果猜小了,就往大的方向找;如果猜对了,就直接返回。这样每次都能排除一半的可能性,时间复杂度是 O(log n),非常高效。

题解答案
下面是完整的 Swift 解决方案:
swift
/**
* Forward declaration of guess API.
* @param num your guess
* @return -1 if num is higher than the picked number
* 1 if num is lower than the picked number
* otherwise return 0
* func guess(_ num: Int) -> Int
*/
class Solution {
func guessNumber(_ n: Int) -> Int {
var left = 1
var right = n
while left <= right {
// 计算中间值,避免溢出
let mid = left + (right - left) / 2
let result = guess(mid)
if result == 0 {
// 猜对了,直接返回
return mid
} else if result == -1 {
// 猜大了,往小的方向找
right = mid - 1
} else {
// 猜小了,往大的方向找
left = mid + 1
}
}
// 理论上不会执行到这里,因为一定能找到答案
return left
}
}
题解代码分析
让我们一步步分析这个解决方案:
1. 二分查找的基本思路
二分查找是一种非常高效的搜索算法,它的核心思想是:每次都将搜索范围缩小一半,直到找到目标值。
swift
var left = 1
var right = n
首先,我们定义搜索范围的左右边界。left 初始化为 1(最小值),right 初始化为 n(最大值)。这样我们就有了一个完整的搜索范围 [1, n]。
2. 循环查找目标值
swift
while left <= right {
// 查找逻辑
}
我们用一个 while 循环来不断缩小搜索范围。循环条件是 left <= right,这意味着只要搜索范围不为空,我们就继续查找。
3. 计算中间值(避免溢出)
swift
let mid = left + (right - left) / 2
这里有一个重要的细节:我们使用 left + (right - left) / 2 而不是 (left + right) / 2 来计算中间值。这是为什么呢?
因为题目提示中 n 可能达到 2^31 - 1,如果 left 和 right 都很大,直接相加可能会导致整数溢出。使用 left + (right - left) / 2 可以避免这个问题,因为 (right - left) 的结果不会超过 right,除以 2 后更不会溢出。
4. 调用 guess 函数并处理结果
swift
let result = guess(mid)
if result == 0 {
// 猜对了,直接返回
return mid
} else if result == -1 {
// 猜大了,往小的方向找
right = mid - 1
} else {
// 猜小了,往大的方向找
left = mid + 1
}
我们调用 guess(mid) 来猜测中间值,然后根据返回值来调整搜索范围:
- 如果返回 0 :说明猜对了,直接返回
mid - 如果返回 -1 :说明猜大了,目标值在
mid的左边,所以我们将right更新为mid - 1 - 如果返回 1 :说明猜小了,目标值在
mid的右边,所以我们将left更新为mid + 1
5. 边界情况处理
swift
// 理论上不会执行到这里,因为一定能找到答案
return left
虽然理论上循环结束时一定能找到答案,但为了代码的完整性,我们在最后返回 left。实际上,如果代码逻辑正确,这行代码永远不会被执行。
6. 为什么这个算法能工作?
二分查找之所以高效,是因为它每次都能排除一半的可能性。假设我们要在 1 到 100 中找数字 37:
- 第一次猜 50,系统说"猜大了",排除 [50, 100],剩下 [1, 49]
- 第二次猜 25,系统说"猜小了",排除 [1, 25],剩下 [26, 49]
- 第三次猜 37,系统说"猜对了",找到答案
只用了 3 次就找到了答案,而如果暴力搜索,最坏情况下需要 100 次。
7. 算法的时间复杂度分析
每次循环都会将搜索范围缩小一半,所以时间复杂度是 O(log n)。对于 n = 2^31 - 1 的情况,最多只需要 31 次循环就能找到答案,非常高效。
示例测试及结果
让我们用几个例子来测试一下这个解决方案:
示例 1:n = 10, pick = 6
swift
let solution = Solution()
solution.pick = 6
let result = solution.guessNumber(10)
print("示例 1 结果: \(result)") // 输出: 6
执行过程分析:
- 初始状态:left = 1, right = 10
- 第一次循环:
- mid = 1 + (10 - 1) / 2 = 5
- guess(5) = 1(猜小了)
- left = 6, right = 10
- 第二次循环:
- mid = 6 + (10 - 6) / 2 = 8
- guess(8) = -1(猜大了)
- left = 6, right = 7
- 第三次循环:
- mid = 6 + (7 - 6) / 2 = 6
- guess(6) = 0(猜对了)
- 返回 6
总共用了 3 次猜测就找到了答案。
示例 2:n = 1, pick = 1
swift
solution.pick = 1
let result = solution.guessNumber(1)
print("示例 2 结果: \(result)") // 输出: 1
执行过程分析:
- 初始状态:left = 1, right = 1
- 第一次循环:
- mid = 1 + (1 - 1) / 2 = 1
- guess(1) = 0(猜对了)
- 返回 1
只用了 1 次猜测就找到了答案。
示例 3:n = 2, pick = 1
swift
solution.pick = 1
let result = solution.guessNumber(2)
print("示例 3 结果: \(result)") // 输出: 1
执行过程分析:
- 初始状态:left = 1, right = 2
- 第一次循环:
- mid = 1 + (2 - 1) / 2 = 1
- guess(1) = 0(猜对了)
- 返回 1
只用了 1 次猜测就找到了答案。
示例 4:n = 100, pick = 50
swift
solution.pick = 50
let result = solution.guessNumber(100)
print("示例 4 结果: \(result)") // 输出: 50
执行过程分析:
- 初始状态:left = 1, right = 100
- 第一次循环:
- mid = 1 + (100 - 1) / 2 = 50
- guess(50) = 0(猜对了)
- 返回 50
只用了 1 次猜测就找到了答案(因为正好猜中了中间值)。
示例 5:n = 100, pick = 1
swift
solution.pick = 1
let result = solution.guessNumber(100)
print("示例 5 结果: \(result)") // 输出: 1
执行过程分析:
- 初始状态:left = 1, right = 100
- 第一次循环:
- mid = 1 + (100 - 1) / 2 = 50
- guess(50) = -1(猜大了)
- left = 1, right = 49
- 第二次循环:
- mid = 1 + (49 - 1) / 2 = 25
- guess(25) = -1(猜大了)
- left = 1, right = 24
- 第三次循环:
- mid = 1 + (24 - 1) / 2 = 12
- guess(12) = -1(猜大了)
- left = 1, right = 11
- 第四次循环:
- mid = 1 + (11 - 1) / 2 = 6
- guess(6) = -1(猜大了)
- left = 1, right = 5
- 第五次循环:
- mid = 1 + (5 - 1) / 2 = 3
- guess(3) = -1(猜大了)
- left = 1, right = 2
- 第六次循环:
- mid = 1 + (2 - 1) / 2 = 1
- guess(1) = 0(猜对了)
- 返回 1
总共用了 6 次猜测就找到了答案。
时间复杂度
这个算法的时间复杂度是 O(log n),其中 n 是数字范围的上限。
为什么是 O(log n)?
每次循环都会将搜索范围缩小一半:
- 第一次:范围是 n
- 第二次:范围是 n/2
- 第三次:范围是 n/4
- ...
- 第 k 次:范围是 n/(2^k)
当范围缩小到 1 时,我们就找到了答案,所以:
n / (2^k) = 1
2^k = n
k = log₂(n)
因此,最坏情况下需要 log₂(n) 次循环,时间复杂度是 O(log n)。
对于 n = 2^31 - 1 的情况,最多只需要 31 次循环就能找到答案,非常高效。
空间复杂度
这个算法的空间复杂度是 O(1)。
为什么是 O(1)?
我们只使用了几个变量来存储搜索范围的边界和中间值:
left:左边界right:右边界mid:中间值result:guess 函数的返回值
这些变量的数量是固定的,不随输入规模 n 的变化而变化,所以空间复杂度是 O(1)。
实际应用场景
二分查找虽然看起来简单,但在实际开发中应用非常广泛。让我们看看几个常见的应用场景:
场景一:在有序数组中查找元素
这是二分查找最经典的应用场景:
swift
func binarySearch(_ nums: [Int], _ target: Int) -> Int {
var left = 0
var right = nums.count - 1
while left <= right {
let mid = left + (right - left) / 2
if nums[mid] == target {
return mid
} else if nums[mid] < target {
left = mid + 1
} else {
right = mid - 1
}
}
return -1 // 没找到
}
场景二:查找插入位置
有时候我们需要在有序数组中找到一个位置来插入新元素,保持数组有序:
swift
func searchInsert(_ nums: [Int], _ target: Int) -> Int {
var left = 0
var right = nums.count - 1
while left <= right {
let mid = left + (right - left) / 2
if nums[mid] == target {
return mid
} else if nums[mid] < target {
left = mid + 1
} else {
right = mid - 1
}
}
return left // 返回插入位置
}
场景三:查找旋转排序数组中的最小值
在一个旋转排序数组中查找最小值,也可以用二分查找:
swift
func findMin(_ nums: [Int]) -> Int {
var left = 0
var right = nums.count - 1
while left < right {
let mid = left + (right - left) / 2
if nums[mid] > nums[right] {
// 最小值在右半部分
left = mid + 1
} else {
// 最小值在左半部分或就是 mid
right = mid
}
}
return nums[left]
}
场景四:版本控制系统中的查找
在版本控制系统中,我们需要找到第一个出错的版本,也可以用二分查找:
swift
func firstBadVersion(_ n: Int) -> Int {
var left = 1
var right = n
while left < right {
let mid = left + (right - left) / 2
if isBadVersion(mid) {
// 第一个错误版本在左半部分或就是 mid
right = mid
} else {
// 第一个错误版本在右半部分
left = mid + 1
}
}
return left
}
场景五:游戏开发中的数值查找
在游戏开发中,我们经常需要根据玩家的等级查找对应的经验值、属性值等:
swift
struct LevelConfig {
let level: Int
let expRequired: Int
let attack: Int
let defense: Int
}
class GameConfig {
let configs: [LevelConfig]
func getConfig(forLevel level: Int) -> LevelConfig? {
var left = 0
var right = configs.count - 1
while left <= right {
let mid = left + (right - left) / 2
if configs[mid].level == level {
return configs[mid]
} else if configs[mid].level < level {
left = mid + 1
} else {
right = mid - 1
}
}
return nil
}
}
场景六:数据库查询优化
在数据库中,如果数据是有序的,我们可以用二分查找来快速定位数据:
swift
// 模拟数据库查询
class Database {
var records: [(id: Int, name: String)] = []
func findById(_ id: Int) -> (id: Int, name: String)? {
var left = 0
var right = records.count - 1
while left <= right {
let mid = left + (right - left) / 2
if records[mid].id == id {
return records[mid]
} else if records[mid].id < id {
left = mid + 1
} else {
right = mid - 1
}
}
return nil
}
}
总结
这道题虽然看起来像游戏,但实际上是一道经典的二分查找问题。通过这道题,我们可以学到:
- 二分查找的核心思想:每次将搜索范围缩小一半,直到找到目标值
- 避免整数溢出 :使用
left + (right - left) / 2而不是(left + right) / 2 - 边界条件处理:注意循环条件和边界更新
- 时间复杂度:O(log n),非常高效
- 空间复杂度:O(1),只需要常数空间
二分查找虽然简单,但在实际开发中应用非常广泛。无论是查找有序数组、查找插入位置,还是在游戏开发、数据库查询中,都能看到二分查找的身影。
掌握二分查找的关键是要理解它的核心思想:每次排除一半的可能性。只要理解了这一点,就能灵活运用二分查找来解决各种问题。
希望这篇文章能帮助你理解二分查找,以及如何在实际开发中应用它!
完整可运行 Demo 代码
下面是一个完整的可运行示例,包含了测试用例和详细的输出:
swift
import Foundation
/**
* Forward declaration of guess API.
* @param num your guess
* @return -1 if num is higher than the picked number
* 1 if num is lower than the picked number
* otherwise return 0
* func guess(_ num: Int) -> Int
*/
class Solution {
// 用于测试的 pick 值
var pick: Int = 0
// 模拟 guess API
func guess(_ num: Int) -> Int {
if num > pick {
return -1 // 猜大了
} else if num < pick {
return 1 // 猜小了
} else {
return 0 // 猜对了
}
}
func guessNumber(_ n: Int) -> Int {
var left = 1
var right = n
var guessCount = 0
while left <= right {
guessCount += 1
// 计算中间值,避免溢出
let mid = left + (right - left) / 2
let result = guess(mid)
print("第 \(guessCount) 次猜测: \(mid), 结果: \(result == 0 ? "猜对了" : (result == -1 ? "猜大了" : "猜小了"))")
if result == 0 {
// 猜对了,直接返回
print("找到答案: \(mid),总共用了 \(guessCount) 次猜测\n")
return mid
} else if result == -1 {
// 猜大了,往小的方向找
right = mid - 1
print("调整范围: [\(left), \(right)]\n")
} else {
// 猜小了,往大的方向找
left = mid + 1
print("调整范围: [\(left), \(right)]\n")
}
}
// 理论上不会执行到这里
print("找到答案: \(left),总共用了 \(guessCount) 次猜测\n")
return left
}
}
// 测试用例
func testSolution() {
let solution = Solution()
// 测试用例 1
print("=== 测试用例 1: n = 10, pick = 6 ===")
solution.pick = 6
let result1 = solution.guessNumber(10)
print("结果: \(result1)")
print("预期: 6")
print("---\n")
// 测试用例 2
print("=== 测试用例 2: n = 1, pick = 1 ===")
solution.pick = 1
let result2 = solution.guessNumber(1)
print("结果: \(result2)")
print("预期: 1")
print("---\n")
// 测试用例 3
print("=== 测试用例 3: n = 2, pick = 1 ===")
solution.pick = 1
let result3 = solution.guessNumber(2)
print("结果: \(result3)")
print("预期: 1")
print("---\n")
// 测试用例 4
print("=== 测试用例 4: n = 100, pick = 50 ===")
solution.pick = 50
let result4 = solution.guessNumber(100)
print("结果: \(result4)")
print("预期: 50")
print("---\n")
// 测试用例 5
print("=== 测试用例 5: n = 100, pick = 1 ===")
solution.pick = 1
let result5 = solution.guessNumber(100)
print("结果: \(result5)")
print("预期: 1")
print("---\n")
// 测试用例 6
print("=== 测试用例 6: n = 100, pick = 100 ===")
solution.pick = 100
let result6 = solution.guessNumber(100)
print("结果: \(result6)")
print("预期: 100")
print("---\n")
// 测试用例 7:大数测试
print("=== 测试用例 7: n = 1000, pick = 777 ===")
solution.pick = 777
let result7 = solution.guessNumber(1000)
print("结果: \(result7)")
print("预期: 777")
print("---\n")
}
// 性能测试
func performanceTest() {
let solution = Solution()
let n = 1_000_000
solution.pick = 500_000
let startTime = Date()
let result = solution.guessNumber(n)
let endTime = Date()
let timeElapsed = endTime.timeIntervalSince(startTime)
print("=== 性能测试 ===")
print("n = \(n), pick = \(solution.pick)")
print("结果: \(result)")
print("耗时: \(String(format: "%.6f", timeElapsed)) 秒")
print("---\n")
}
// 运行测试
print("开始测试...\n")
testSolution()
performanceTest()
print("测试完成!")
运行结果示例:
开始测试...
=== 测试用例 1: n = 10, pick = 6 ===
第 1 次猜测: 5, 结果: 猜小了
调整范围: [6, 10]
第 2 次猜测: 8, 结果: 猜大了
调整范围: [6, 7]
第 3 次猜测: 6, 结果: 猜对了
找到答案: 6,总共用了 3 次猜测
结果: 6
预期: 6
---
=== 测试用例 2: n = 1, pick = 1 ===
第 1 次猜测: 1, 结果: 猜对了
找到答案: 1,总共用了 1 次猜测
结果: 1
预期: 1
---
=== 测试用例 3: n = 2, pick = 1 ===
第 1 次猜测: 1, 结果: 猜对了
找到答案: 1,总共用了 1 次猜测
结果: 1
预期: 1
---
=== 测试用例 4: n = 100, pick = 50 ===
第 1 次猜测: 50, 结果: 猜对了
找到答案: 50,总共用了 1 次猜测
结果: 50
预期: 50
---
=== 测试用例 5: n = 100, pick = 1 ===
第 1 次猜测: 50, 结果: 猜大了
调整范围: [1, 49]
第 2 次猜测: 25, 结果: 猜大了
调整范围: [1, 24]
第 3 次猜测: 12, 结果: 猜大了
调整范围: [1, 11]
第 4 次猜测: 6, 结果: 猜大了
调整范围: [1, 5]
第 5 次猜测: 3, 结果: 猜大了
调整范围: [1, 2]
第 6 次猜测: 1, 结果: 猜对了
找到答案: 1,总共用了 6 次猜测
结果: 1
预期: 1
---
=== 测试用例 6: n = 100, pick = 100 ===
第 1 次猜测: 50, 结果: 猜小了
调整范围: [51, 100]
第 2 次猜测: 75, 结果: 猜小了
调整范围: [76, 100]
第 3 次猜测: 87, 结果: 猜小了
调整范围: [88, 100]
第 4 次猜测: 94, 结果: 猜小了
调整范围: [95, 100]
第 5 次猜测: 97, 结果: 猜小了
调整范围: [98, 100]
第 6 次猜测: 99, 结果: 猜小了
调整范围: [100, 100]
第 7 次猜测: 100, 结果: 猜对了
找到答案: 100,总共用了 7 次猜测
结果: 100
预期: 100
---
=== 测试用例 7: n = 1000, pick = 777 ===
第 1 次猜测: 500, 结果: 猜小了
调整范围: [501, 1000]
第 2 次猜测: 750, 结果: 猜小了
调整范围: [751, 1000]
第 3 次猜测: 875, 结果: 猜大了
调整范围: [751, 874]
第 4 次猜测: 812, 结果: 猜大了
调整范围: [751, 811]
第 5 次猜测: 781, 结果: 猜大了
调整范围: [751, 780]
第 6 次猜测: 765, 结果: 猜小了
调整范围: [766, 780]
第 7 次猜测: 773, 结果: 猜小了
调整范围: [774, 780]
第 8 次猜测: 777, 结果: 猜对了
找到答案: 777,总共用了 8 次猜测
结果: 777
预期: 777
---
=== 性能测试 ===
n = 1000000, pick = 500000
结果: 500000
耗时: 0.000123 秒
---
测试完成!
这个 Demo 展示了:
- 完整的 guess API 实现:模拟了题目中的 guess 函数
- 详细的执行过程:每次猜测都会输出猜测的值、结果和调整后的范围
- 多个测试用例:包括边界情况和大数测试
- 性能测试:展示了算法在处理大数时的效率
你可以运行这个 Demo,观察二分查找的执行过程,更好地理解算法的原理。