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.
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
- When guess is too low (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:
-
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. -
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)
- The sequence to search:
-
Key Function Transformation: The key function
lambda x: -guess(x)
is crucial. For each numberx
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
- If
-
How bisect Works: The
bisect.bisect()
function searches for the insertion point of value0
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. -
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 EvaluatorExample 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)
: returns1
(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)
: returns0
(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 insertedbisect.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.
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
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!