Facebook Pixel

374. Guess Number Higher or Lower

Problem Description

This is a classic number guessing game where you need to find a secret number between 1 and n.

The game works as follows:

  • A number between 1 and n (inclusive) has been picked beforehand
  • You need to find this number by making guesses
  • For each guess you make, you'll receive feedback through a pre-defined API function guess(num)

The guess(num) function returns:

  • -1 if your guess is too high (your guess > the picked number)
  • 1 if your guess is too low (your guess < the picked number)
  • 0 if your guess is correct (your guess = the picked number)

Your task is to implement a function that finds and returns the picked number using the guess API.

The solution uses binary search to efficiently find the target number. By leveraging Python's bisect module with a custom key function, it searches for the first position where -guess(x) returns 0 or greater. The key function -guess(x) transforms the problem into finding the insertion point in a sorted sequence, where the negation converts the guess API's return values to work with bisect's expectations.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we need to find a specific number in a sorted range from 1 to n, and we can only check if our guess is too high, too low, or correct, binary search naturally comes to mind. This is because we can eliminate half of the remaining possibilities with each guess.

The traditional binary search approach would involve maintaining left and right pointers, calculating the middle point, and adjusting the search range based on the feedback from guess(). However, Python's bisect module provides a clever alternative.

The key insight is recognizing that bisect is designed to find insertion points in sorted sequences. If we can transform our guessing problem into finding an insertion point, we can leverage this built-in functionality.

Here's how the transformation works:

  • bisect expects a sorted sequence where it needs to find where to insert a value
  • We want to find where guess(x) transitions from returning 1 (too low) to -1 (too high), with 0 (correct) being our target
  • By negating the guess function with -guess(x), we transform the return values:
    • When guess is too low (returns 1), -guess(x) returns -1
    • When guess is correct (returns 0), -guess(x) returns 0
    • When guess is too high (returns -1), -guess(x) returns 1

This creates a sequence that goes from negative to positive values, and bisect searching for 0 will find exactly where this transition happens - which is our answer. The beauty of this approach is that it reduces the entire binary search logic to a single line using Python's optimized built-in function.

Solution Approach

The solution implements binary search using Python's bisect.bisect() function with a custom key function.

Implementation Details:

  1. Range Setup: We create a range from 1 to n+1 using range(1, n + 1), which represents all possible numbers we need to search through.

  2. Using bisect.bisect(): The bisect.bisect() function performs binary search on the range. It takes three main parameters:

    • The sequence to search: range(1, n + 1)
    • The value to search for: 0
    • A key function: lambda x: -guess(x)
  3. Key Function Transformation: The key function lambda x: -guess(x) is crucial. For each number x in the range:

    • If x is less than the picked number, guess(x) returns 1, so -guess(x) returns -1
    • If x equals the picked number, guess(x) returns 0, so -guess(x) returns 0
    • If x is greater than the picked number, guess(x) returns -1, so -guess(x) returns 1
  4. How bisect Works: The bisect.bisect() function searches for the insertion point of value 0 in the transformed sequence. Since the negated guess values create a sequence that goes from -1 (for numbers below target) to 0 (at target) to 1 (above target), bisect will find the exact position where 0 appears, which is our answer.

  5. Return Value: The function directly returns the result of bisect.bisect(), which is the index (1-based in this case due to our range starting at 1) where the picked number is located.

Time Complexity: O(log n) - Binary search divides the search space in half with each iteration.

Space Complexity: O(1) - The range object is a generator that doesn't store all values in memory, and we only use constant extra space for the binary search operation.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example where n = 10 and the picked number is 6.

Initial Setup:

  • Search range: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  • Target: Find the picked number (which is 6, but we don't know this yet)
  • We'll use bisect.bisect(range(1, 11), 0, key=lambda x: -guess(x))

How Binary Search Proceeds:

Step 1: Bisect starts by checking the middle of the range

  • Middle position: x = 5
  • Call guess(5): returns 1 (5 is too low, since picked = 6)
  • Key function: -guess(5) = -1
  • Since -1 < 0, bisect knows the answer is in the upper half

Step 2: Search narrows to [6, 7, 8, 9, 10]

  • Middle position: x = 8
  • Call guess(8): returns -1 (8 is too high, since picked = 6)
  • Key function: -guess(8) = 1
  • Since 1 > 0, bisect knows the answer is in the lower half

Step 3: Search narrows to [6, 7]

  • Check position: x = 6
  • Call guess(6): returns 0 (correct!)
  • Key function: -guess(6) = 0
  • Since we found exactly 0, this is our insertion point

Step 4: Bisect continues to verify the exact insertion position

  • Check position: x = 7
  • Call guess(7): returns -1 (too high)
  • Key function: -guess(7) = 1
  • This confirms that 6 is the last position where the key function returns ≤ 0

Result: bisect.bisect() returns 6, which is the correct answer.

Visualizing the Transformation:

Number x:        1   2   3   4   5   6   7   8   9   10
guess(x):        1   1   1   1   1   0  -1  -1  -1  -1
-guess(x):      -1  -1  -1  -1  -1   0   1   1   1   1
                 ↑                   ↑
                 all negative        target (returns 0)

The bisect function finds where to insert 0 in this transformed sequence, which is exactly at position 6 - our answer!

Solution Implementation

1import bisect
2
3# The guess API is already defined for you.
4# @param num, your guess
5# @return -1 if num is higher than the picked number
6#          1 if num is lower than the picked number
7#          otherwise return 0
8# def guess(num: int) -> int:
9
10
11class Solution:
12    def guessNumber(self, n: int) -> int:
13        """
14        Find the picked number between 1 and n using binary search.
15      
16        Args:
17            n: The upper bound of the range [1, n]
18          
19        Returns:
20            The picked number
21        """
22        # Use bisect to perform binary search on the range [1, n]
23        # The key function transforms guess results:
24        # - guess(x) returns -1 if x > picked (we need to go lower)
25        # - guess(x) returns 1 if x < picked (we need to go higher)  
26        # - guess(x) returns 0 if x == picked (found it)
27        # 
28        # By negating guess(x), we get:
29        # - If x > picked: -(-1) = 1 (positive, so bisect goes left)
30        # - If x < picked: -(1) = -1 (negative, so bisect goes right)
31        # - If x == picked: -(0) = 0 (found the target)
32        #
33        # bisect.bisect searches for where to insert 0 in the transformed sequence
34        # This finds the position where -guess(x) == 0, which is our answer
35        return bisect.bisect(range(1, n + 1), 0, key=lambda x: -guess(x))
36
1/**
2 * Forward declaration of guess API.
3 * @param  num   your guess
4 * @return 	     -1 if num is lower than the guess number
5 *			      1 if num is higher than the guess number
6 *               otherwise return 0
7 * int guess(int num);
8 */
9
10public class Solution extends GuessGame {
11    /**
12     * Finds the target number using binary search.
13     * The search space is from 1 to n (inclusive).
14     * 
15     * @param n The upper bound of the search range
16     * @return The target number that was picked
17     */
18    public int guessNumber(int n) {
19        // Initialize binary search boundaries
20        // left: lower bound (inclusive)
21        // right: upper bound (inclusive)
22        int left = 1;
23        int right = n;
24      
25        // Continue searching while the search space has more than one element
26        while (left < right) {
27            // Calculate the middle point using unsigned right shift to avoid overflow
28            // This is equivalent to (left + right) / 2 but prevents integer overflow
29            int mid = (left + right) >>> 1;
30          
31            // Check if mid is the target or adjust search boundaries
32            int guessResult = guess(mid);
33          
34            if (guessResult <= 0) {
35                // If guess returns -1: mid is lower than target (but this moves right boundary)
36                // If guess returns 0: mid is the target (include mid in search space)
37                // Move right boundary to mid (keep mid in search space)
38                right = mid;
39            } else {
40                // If guess returns 1: mid is higher than target
41                // Target must be in the upper half, exclude mid from search space
42                left = mid + 1;
43            }
44        }
45      
46        // When left == right, we've found the target
47        return left;
48    }
49}
50
1/**
2 * Forward declaration of guess API.
3 * @param  num   your guess
4 * @return 	     -1 if num is lower than the guess number
5 *			      1 if num is higher than the guess number
6 *               otherwise return 0
7 * int guess(int num);
8 */
9
10class Solution {
11public:
12    /**
13     * Binary search to find the target number
14     * @param n The upper bound of the search range [1, n]
15     * @return The target number that was picked
16     */
17    int guessNumber(int n) {
18        // Initialize binary search boundaries
19        // Search range is [left, right] inclusive
20        int left = 1;
21        int right = n;
22      
23        // Continue searching while the range has more than one element
24        while (left < right) {
25            // Calculate middle point avoiding potential overflow
26            // Using bit shift for division by 2
27            int mid = left + ((right - left) >> 1);
28          
29            // Get feedback from guess API
30            int result = guess(mid);
31          
32            // If mid is greater than or equal to target
33            // Target must be in [left, mid]
34            if (result <= 0) {
35                right = mid;  // Include mid as it could be the answer
36            } 
37            // If mid is less than target
38            // Target must be in [mid + 1, right]
39            else {
40                left = mid + 1;  // Exclude mid as it's definitely not the answer
41            }
42        }
43      
44        // When left == right, we've found the target
45        return left;
46    }
47};
48
1/**
2 * Forward declaration of guess API.
3 * @param {number} num   your guess
4 * @return 	            -1 if num is lower than the guess number
5 *			             1 if num is higher than the guess number
6 *                       otherwise return 0
7 * var guess = function(num) {}
8 */
9
10/**
11 * Binary search to find the target number using the guess API
12 * Time Complexity: O(log n)
13 * Space Complexity: O(1)
14 * 
15 * @param n - The upper bound of the search range [1, n]
16 * @returns The target number that was picked
17 */
18function guessNumber(n: number): number {
19    // Initialize binary search boundaries
20    let left: number = 1;
21    let right: number = n;
22  
23    // Continue searching while the range is valid
24    while (left < right) {
25        // Calculate middle point using bit shift to avoid overflow
26        // >>> 1 is equivalent to Math.floor(division by 2)
27        const middle: number = (left + right) >>> 1;
28      
29        // Check the guess result for the middle value
30        const guessResult: number = guess(middle);
31      
32        if (guessResult <= 0) {
33            // If middle is greater than or equal to target, search left half
34            // Include middle in the search range as it could be the answer
35            right = middle;
36        } else {
37            // If middle is less than target, search right half
38            // Exclude middle from the search range
39            left = middle + 1;
40        }
41    }
42  
43    // When left === right, we've found the target number
44    return left;
45}
46

Time and Space Complexity

The time complexity is O(log n), where n is the upper limit given in the problem. This is because bisect.bisect performs a binary search on the range [1, n]. Binary search works by repeatedly dividing the search space in half, requiring at most log₂(n) comparisons to find the target element. Even though the code creates a range object and uses a lambda function as a key, the bisect module intelligently performs binary search without materializing the entire range, making only O(log n) calls to the guess function.

The space complexity is O(1). While the code creates a range(1, n + 1) object, Python's range objects are lazy iterators that don't store all values in memory - they only store the start, stop, and step values. The bisect.bisect function uses constant extra space for its binary search implementation (just a few variables to track indices). The lambda function and its execution also use constant space.

Common Pitfalls

1. Misunderstanding bisect.bisect() vs bisect.bisect_left()

A common mistake is confusing which bisect function to use. While both bisect.bisect() and bisect.bisect_left() work for this problem, they behave differently when multiple equal elements exist:

  • bisect.bisect() returns the rightmost position where the value can be inserted
  • bisect.bisect_left() returns the leftmost position where the value can be inserted

In this specific problem, since there's only one correct answer (one position where -guess(x) == 0), both functions return the same result. However, using bisect.bisect_left() is technically more precise since we want the first (leftmost) position where the condition is met.

Solution:

# More semantically correct version
return bisect.bisect_left(range(1, n + 1), 0, key=lambda x: -guess(x))

2. Incorrect Key Function Logic

Developers might incorrectly implement the key function by forgetting to negate the guess result or misunderstanding the transformation needed:

Wrong approaches:

# Wrong: Using guess(x) directly without negation
return bisect.bisect(range(1, n + 1), 0, key=lambda x: guess(x))

# Wrong: Looking for 1 instead of 0
return bisect.bisect(range(1, n + 1), 1, key=lambda x: -guess(x))

Solution: Always remember that bisect searches for an insertion point in a sorted sequence. The negation -guess(x) creates a monotonic sequence from -1 to 0 to 1, allowing bisect to find where 0 belongs.

3. Off-by-One Errors with Range

A critical mistake is using incorrect range boundaries:

Wrong approaches:

# Wrong: Missing n itself
return bisect.bisect(range(1, n), 0, key=lambda x: -guess(x))

# Wrong: Starting from 0 instead of 1
return bisect.bisect(range(0, n + 1), 0, key=lambda x: -guess(x))

Solution: Always use range(1, n + 1) to include both 1 and n in the search space, as the problem states the number is between 1 and n inclusive.

4. Assuming bisect Returns the Value Instead of Index

Some might think bisect returns the actual value found rather than its position:

# Wrong understanding - bisect returns the index/position, not the value
result = bisect.bisect(range(1, n + 1), 0, key=lambda x: -guess(x))
# No need for: return range(1, n + 1)[result - 1]

Solution: In this case, since our range starts at 1 and increments by 1, the position returned by bisect IS the actual number we're looking for. The index in range(1, n + 1) directly corresponds to the value.

5. Over-complicating with Manual Binary Search

While the bisect solution is elegant, some developers might not realize they can use it and implement manual binary search with potential bugs:

# Unnecessary manual implementation when bisect exists
def guessNumber(self, n: int) -> int:
    left, right = 1, n
    while left <= right:
        mid = left + (right - left) // 2
        result = guess(mid)
        if result == 0:
            return mid
        elif result == -1:
            right = mid - 1
        else:
            left = mid + 1

Solution: Leverage Python's built-in bisect module for cleaner, more maintainable code. The manual approach works but is more prone to implementation errors.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using the following tree as input?

1def serialize(root):
2    res = []
3    def dfs(root):
4        if not root:
5            res.append('x')
6            return
7        res.append(root.val)
8        dfs(root.left)
9        dfs(root.right)
10    dfs(root)
11    return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4    StringJoiner res = new StringJoiner(" ");
5    serializeDFS(root, res);
6    return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10    if (root == null) {
11        result.add("x");
12        return;
13    }
14    result.add(Integer.toString(root.val));
15    serializeDFS(root.left, result);
16    serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2    let res = [];
3    serialize_dfs(root, res);
4    return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8    if (!root) {
9        res.push("x");
10        return;
11    }
12    res.push(root.val);
13    serialize_dfs(root.left, res);
14    serialize_dfs(root.right, res);
15}
16

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More