LeetCode 374 猜数字大小 - Swift 题解


文章目录

摘要

这道题其实挺经典的,它模拟了一个猜数字的游戏。游戏规则很简单:系统从 1 到 n 中随机选一个数字,你需要通过调用 guess 函数来猜这个数字。每次猜测后,函数会告诉你猜大了、猜小了还是猜对了。我们的目标就是用最少的次数找到这个数字。

这道题本质上是一个二分查找问题,虽然题目描述看起来像游戏,但核心算法就是经典的二分查找。今天我们就用 Swift 来搞定这道题,顺便聊聊二分查找在实际开发中的应用场景。

描述

题目要求是这样的:我们正在玩猜数字游戏。系统会从 1n 随机选择一个数字,这个数字在整个游戏中保持不变。你需要猜出这个数字是多少。

你可以通过调用一个预先定义好的接口 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 - 1
  • 1 <= 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,如果 leftright 都很大,直接相加可能会导致整数溢出。使用 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

执行过程分析:

  1. 初始状态:left = 1, right = 10
  2. 第一次循环:
    • mid = 1 + (10 - 1) / 2 = 5
    • guess(5) = 1(猜小了)
    • left = 6, right = 10
  3. 第二次循环:
    • mid = 6 + (10 - 6) / 2 = 8
    • guess(8) = -1(猜大了)
    • left = 6, right = 7
  4. 第三次循环:
    • 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

执行过程分析:

  1. 初始状态:left = 1, right = 1
  2. 第一次循环:
    • 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

执行过程分析:

  1. 初始状态:left = 1, right = 2
  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

执行过程分析:

  1. 初始状态:left = 1, right = 100
  2. 第一次循环:
    • 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

执行过程分析:

  1. 初始状态:left = 1, right = 100
  2. 第一次循环:
    • mid = 1 + (100 - 1) / 2 = 50
    • guess(50) = -1(猜大了)
    • left = 1, right = 49
  3. 第二次循环:
    • mid = 1 + (49 - 1) / 2 = 25
    • guess(25) = -1(猜大了)
    • left = 1, right = 24
  4. 第三次循环:
    • mid = 1 + (24 - 1) / 2 = 12
    • guess(12) = -1(猜大了)
    • left = 1, right = 11
  5. 第四次循环:
    • mid = 1 + (11 - 1) / 2 = 6
    • guess(6) = -1(猜大了)
    • left = 1, right = 5
  6. 第五次循环:
    • mid = 1 + (5 - 1) / 2 = 3
    • guess(3) = -1(猜大了)
    • left = 1, right = 2
  7. 第六次循环:
    • 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
    }
}

总结

这道题虽然看起来像游戏,但实际上是一道经典的二分查找问题。通过这道题,我们可以学到:

  1. 二分查找的核心思想:每次将搜索范围缩小一半,直到找到目标值
  2. 避免整数溢出 :使用 left + (right - left) / 2 而不是 (left + right) / 2
  3. 边界条件处理:注意循环条件和边界更新
  4. 时间复杂度:O(log n),非常高效
  5. 空间复杂度: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 展示了:

  1. 完整的 guess API 实现:模拟了题目中的 guess 函数
  2. 详细的执行过程:每次猜测都会输出猜测的值、结果和调整后的范围
  3. 多个测试用例:包括边界情况和大数测试
  4. 性能测试:展示了算法在处理大数时的效率

你可以运行这个 Demo,观察二分查找的执行过程,更好地理解算法的原理。

标题

相关推荐
Coovally AI模型快速验证2 小时前
2026 CES 如何用“视觉”改变生活?机器的“视觉大脑”被点亮
人工智能·深度学习·算法·yolo·生活·无人机
有一个好名字2 小时前
力扣-链表最大孪生和
算法·leetcode·链表
AshinGau2 小时前
Groth16 ZKP: 零知识证明
算法
无限进步_2 小时前
【C语言&数据结构】二叉树链式结构完全指南:从基础到进阶
c语言·开发语言·数据结构·c++·git·算法·visual studio
明月下2 小时前
【视觉算法——Yolo系列】Yolov11下载、训练&推理、量化&转化
算法·yolo
七牛云行业应用2 小时前
iOS 19.3 突发崩溃!Gemini 3 导致 JSON 解析失败的紧急修复
人工智能·ios·swift·json解析·大模型应用
DYS_房东的猫2 小时前
《 C++ 零基础入门教程》第8章:多线程与并发编程 —— 让程序“同时做多件事”
开发语言·c++·算法
小郭团队2 小时前
1_1_七段式SVPWM (传统算法反正切)算法理论与 MATLAB 实现详解
人工智能·stm32·嵌入式硬件·算法·dsp开发
翟天保Steven3 小时前
医学影像-CBCT图像重建FDK算法
算法·医学影像·图像重建