算法题 猜数字大小

374. 猜数字大小

问题描述

我们正在玩一个猜数字的游戏。游戏规则如下:

  • 我从 1 到 n 之间选择一个数字。
  • 你来猜我选的是哪个数字。
  • 每次你猜错了,我会返回 3 个可能的结果之一:
    • -1:我的数字比较小,即我选的数字比你猜的
    • 1:我的数字比较大,即我选的数字比你猜的
    • 0:恭喜!你猜对了!

设计一个算法来找到我选定的数字,要求调用 guess API 的次数最少。

示例

输入: n = 10, pick = 6
输出: 6
输入: n = 1, pick = 1  
输出: 1

算法思路

二分查找法

  1. 使用二分查找在区间 [1, n] 中搜索目标数字
  2. 每次猜测区间的中点值
  3. 根据 guess 函数的返回值调整搜索范围
  4. 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

  1. 初始left=1, right=10
  2. 第1轮
    • mid = 1 + (10-1)/2 = 5
    • guess(5) → 返回 1(目标更大)
    • left = 6, right = 10
  3. 第2轮
    • mid = 6 + (10-6)/2 = 8
    • guess(8) → 返回 -1(目标更小)
    • left = 6, right = 7
  4. 第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
}

关键点

  1. 避免整数溢出

    • 使用 left + (right - left) / 2 而不是 (left + right) / 2
    • leftright 都很大时,left + right 可能溢出
  2. 循环条件

    • 使用 while (left <= right) 确保所有情况都被覆盖
    • left == right 时仍需检查
  3. 边界更新

    • result == -1 时:right = mid - 1(排除mid)
    • result == 1 时:left = mid + 1(排除mid)
  4. 中点计算

    • 推荐使用 left + (right - left) / 2
    • 或使用位运算 left + ((right - left) >> 1) 提升效率

常见问题

  1. 为什么不能用线性搜索?

    • 时间复杂度 O(n),在 n 很大时效率极低
    • 二分查找 O(log n) 是最优解
  2. 如何处理 guess() 返回值异常?

    • 题目保证返回值只会是 -1, 0, 1
    • 实际编码中可添加异常处理
  3. 数字范围从0开始怎么办?

    • 算法同样适用,只需调整初始边界为 [0, n]
  4. 能否用三分查找?

    • 理论上可以,但二分查找已经是最优
    • 三分查找的期望次数更多
  5. API调用次数有上限吗?

    • LeetCode 通常有调用次数限制
    • 二分查找能保证最少的调用次数

扩展思考

  1. 如果 guess() 函数有时会返回错误结果?

    • 需要设计容错算法,如多次验证
  2. 如果搜索空间不是连续的?

    • 需要先排序或使用其他数据结构
  3. 如何最小化最坏情况下的猜测次数?

    • 二分查找已经是最优策略
  4. 如果允许自定义猜测策略?

    • 可以根据先验知识调整搜索策略
<think>我们正在讨论猜数字游戏的算法实现和逻辑分析。根据用户提供的引用,猜数字游戏有多种变体,但核心逻辑通常包括:1.生成一个秘密数字(通常是随机生成的,长度固定,如4位数)2.玩家输入猜测的数字3.系统比较猜测数字和秘密数字,给出提示:通常包括位置和数字都正确的个数(A)和数字正确但位置不对的个数(B)4.重复步骤2和3直到玩家猜中或达到最大次数根据引用[1]和[2],我们主要关注的是经典猜数字游戏(如Mastermind)的提示生成算法,即如何计算A和B。算法步骤:1.首先,遍历两个字符串(秘密数字和猜测数字),统计位置和数字都相同的个数(A)。同时,为了避免重复计数,我们可以将已经匹配的位置标记或跳过。2.然后,统计数字正确但位置不对的个数(B)。这里需要注意的是,同一个数字如果在秘密数字中出现多次,那么在猜测数字中出现的次数不能超过秘密数字中出现的次数。具体实现:我们可以使用两个数组(或哈希表)分别统计秘密数字和猜测数字中每个数字出现的次数(不包括已经匹配的位置)。然后,对于每个数字0-9(假设数字范围是0-9),取秘密数字中该数字出现的次数和猜测数字中该数字出现的次数的较小值,这个值就是该数字在两个序列中都出现的总次数(包括位置正确和位置不正确的)。因此,数字正确(包括位置正确)的总次数= min(秘密数字中数字i的出现次数,猜测数字中数字i的出现次数)对所有i求和。那么,数字正确但位置不对的个数(B)=总次数 -A(位置正确的个数)。注意:在引用[4]中,这个游戏被称为Bulls andCows,其中Bulls就是A,Cows就是B。下面我们用一个例子来说明:秘密数字:1122猜测数字:1234第一步:计算A(位置和数字都正确)第0位:1和1 ->正确,A=1第1位:1和2 ->不同第2位:2和3 ->不同第3位:2和4 ->不同所以A=1第二步:统计每个数字的出现次数(跳过已经匹配的位置?但注意,我们统计的是整个序列中每个数字出现的次数,但需要排除位置正确的数字吗?实际上,在计算总共同出现的数字时,我们不需要排除位置正确的,因为位置正确的也是共同出现的。但是,在计算B时,我们需要减去A,因为B要求位置不正确。秘密数字:1出现2次,2出现2次猜测数字:1出现1次,2出现1次,3出现1次,4出现1次对于数字1:min(2,1)=1对于数字2:min(2,1)=1对于数字3:min(0,1)=0对于数字4:min(0,1)=0总共同出现的数字次数=1+1 =2因此,B =总共同出现次数 -A =2-1 =1但是,在引用[1]中的例子:猜1234,计算机告诉玩家猜对了2个数字,其中一个在正确的位置。这里猜对了2个数字(即总共同出现2次),其中1个位置正确(A=1),另一个位置不正确(B=1)。所以我们的计算符合。再看另一个例子:猜测1111,秘密1122A:第0位1相同,第1位1相同(秘密第1位是1,猜测第1位是1),所以A=2?但是秘密数字第0位和第1位都是1,而猜测数字前两位都是1,所以:第0位:1==1-> A=1第1位:1==1 ->A=2第2位:2!=1->不匹配第3位:2!=1->不匹配所以A=2然后统计共同出现数字:秘密数字:1出现2次,2出现2次猜测数字:1出现4次对于1:min(2,4)=2对于2:min(2,0)=0总共同出现次数=2因此B=总共同出现次数-A=2-2=0所以提示为:猜对2个数字,有2个在正确的位置。符合引用[1]描述。因此,算法可以总结为:1.初始化A=02.遍历每个位置i,如果secret[i]==guess[i],则A++,并记录这个位置已经被匹配(或者我们可以不记录,因为后面统计次数时,位置正确的已经被计算在内,而B需要排除位置正确的,所以我们在统计总共同出现时包括了位置正确的,然后减去A即可)3.使用两个计数器数组,分别统计secret和guess中每个数字出现的次数(注意:这里统计整个字符串,包括位置正确的部分)4.计算每个数字在secret和guess中出现次数的较小值,将这些值累加得到total5. B= total- A注意:在统计数字出现次数时,我们不需要区分位置,所以步骤3和4可以合并。代码实现(以字符串形式输入秘密数字和猜测数字,假设长度相同):伪代码:A=0count_secret=长度为10的数组(初始0)#假设数字0-9count_guess=长度为10的数组(初始0)fori inrange(len(secret)):if secret[i]== guess[i]:A+=1#注意:这里我们分别统计每个数字的出现次数,不管位置count_secret[secret[i]- '0']+=1#如果是字符,则减去'0'得到数字值count_guess[guess[i] -'0'] +=1total =0for digitin range(10):total +=min(count_secret[digit], count_guess[digit])B =total -A然后提示:A个数字位置正确,B个数字存在但位置不正确。另外,根据引用[3],还有一种猜数字游戏是猜一个100以内的正整数,然后根据猜测次数给出不同提示。这种游戏不需要计算A和B,而是比较大小。但用户的问题主要集中在引用[1][2][4]所描述的类型。因此,我们将重点实现这种提示算法(Bulls andCows)。下面给出一个完整的Java实现示例(类似LeetCode299题):注意:LeetCode299题中,秘密数字和猜测数字都是字符串,且只包含数字,长度相同。代码实现:```javapublic StringgetHint(String secret, Stringguess){int bulls=0;intcows =0;int[]countSecret= newint[10];int[] countGuess =new int[10];for (int i=0;i <secret.length(); i++) {chars =secret.charAt(i);charg =guess.charAt(i);if(s== g) {bulls++;}countSecret[s -'0']++;countGuess[g -'0']++;}inttotal =0;for (int i=0;i <10; i++) {total+= Math.min(countSecret[i], countGuess[i]);}cows =total -bulls;returnbulls +"A" +cows +"B"; }```但是,注意:上面的代码在统计数字出现次数时,对于同一个位置,我们既在计数数组secret中加了1,也在guess中加了1。然后计算总共同出现次数(包括位置正确的)。然后减去位置正确的个数,得到位置不正确的个数。这是正确的。然而,有一个潜在的问题:在位置正确的情况下,我们在两个计数数组中都加了1,这不会影响结果吗?不会,因为位置正确的情况也是共同出现的数字,所以应该被计算在总共同出现次数中。然后我们减去位置正确的个数,剩下的就是位置不正确的个数。因此,这个算法是合理的。另外,引用[2]中要求处理多组数据,直到n=0。我们可以使用上述函数来计算每一组数据中的每个猜测的提示。逻辑分析总结:1.生成秘密数字(随机生成,且不重复?根据游戏规则,有些游戏允许重复,有些不允许。在引用[1]中,秘密数字是1122,说明允许重复。所以我们的算法也要支持重复数字)2.对于每个猜测,计算bulls(A)和cows(B)并输出提示。3.直到玩家猜中(bulls等于数字长度)或者达到最大猜测次数。关于生成秘密数字:如果允许重复,则直接随机生成即可。如果不允许重复,则需要生成不重复的数字序列。根据具体游戏规则而定。用户问题:猜数字游戏的算法实现或逻辑分析我们已经给出了核心算法(提示生成)的实现和逻辑分析。下面,我们生成相关问题:§§相关问题§§1.猜数字游戏中如何处理重复数字的情况? 2.如何生成一个不重复的4位随机数字作为秘密数字?3.猜数字游戏的时间复杂度如何分析?4.猜数字游戏有哪些优化策略(如缩小猜测范围)?</think>### 猜数字游戏算法实现与逻辑分析 猜数字游戏的核心算法在于**比较秘密数字和玩家猜测**,生成包含两个关键指标的提示: 1. **位置和数字都正确**(Bulls/A) 2. **数字正确但位置错误**(Cows/B) #### 核心算法实现(Java示例) ```java public String getHint(String secret, String guess) { int bulls = 0; int cows = 0; int[] secretCount = new int[10]; // 数字0-9的计数数组 int[] guessCount = new int[10]; // 数字0-9的计数数组 // 第一轮遍历:统计公牛数 for (int i = 0; i < secret.length(); i++) { char s = secret.charAt(i); char g = guess.charAt(i); if (s == g) { bulls++; // 位置和数字均匹配 } else { // 记录未匹配位置的数字出现次数 secretCount[s - '0']++; guessCount[g - '0']++; } } // 统计奶牛数(数字正确但位置错误) for (int i = 0; i < 10; i++) { cows += Math.min(secretCount[i], guessCount[i]); } return bulls + "A" + cows + "B"; } ``` #### 逻辑分析 1. **公牛(Bulls)计算** - 遍历秘密数字和猜测数字的每一位 - 当相同位置上的数字相等时,公牛计数增加 $$ \text{bulls} = \sum_{i=0}^{n-1} [\text{secret}[i] == \text{guess}[i]] $$ - 时间复杂度:$O(n)$,其中$n$为数字长度 2. **奶牛(Cows)计算** - 关键原则:**数字在秘密数字中出现次数的最小值**(排除公牛位置) - 创建两个长度为10的计数数组(对应数字0-9) - 统计未匹配位置中每个数字的出现次数 - 奶牛数 = 所有数字的$\min(\text{secretCount}[d], \text{guessCount}[d])$之和 $$ \text{cows} = \sum_{d=0}^{9} \min(\text{secretCount}_d, \text{guessCount}_d) $$ - 时间复杂度:$O(1)$(固定10次循环) 3. **处理重复数字** - 示例:秘密数字"1122",猜测"1222" - Bulls: 位置0的'1'匹配 → 1A - Cows: - 秘密未匹配数字计数: [1:1, 2:2] → 实际[1,2,2] - 猜测未匹配数字计数: [2:3] → 实际[2,2,2] - min(1,0)+min(2,3)=2 → 2B - 最终提示:1A2B #### 游戏流程控制 ```python # 简化版游戏流程(Python伪代码) import random def start_game(n=4, max_attempts=10): secret = ''.join(str(random.randint(0,9)) for _ in range(n)) for attempt in range(max_attempts): guess = input(f"Attempt {attempt+1}: ") hint = get_hint(secret, guess) # 调用上述提示算法 if hint == f"{n}A0B": print("You win!") return print(f"Hint: {hint}") print(f"Game Over! Secret: {secret}") ``` #### 关键特性 1. **数字重复处理** 通过计数数组自动处理重复数字(如1122),无需特殊逻辑[^1][^4]。 2. **效率优化** - 双计数数组法避免$O(n^2)$暴力比较 - 空间复杂度$O(1)$(固定长度计数数组) 3. **扩展性** 调整数字长度(如6位密码)只需修改$n$值,核心算法不变[^2]。 #### 应用场景 1. 算法教学(数组操作/计数统计) 2. 人机交互基础框架 3. 密码强度检测的简化模型 4. 数据匹配算法原型(如生物特征识别)[^4] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值