367. Valid Perfect Square
Problem Description
You are given a positive integer num
. Your task is to determine whether this number is a perfect square or not. Return true
if it is a perfect square, otherwise return false
.
A perfect square is an integer that can be expressed as the product of some integer with itself. For example:
16
is a perfect square because4 × 4 = 16
25
is a perfect square because5 × 5 = 25
14
is not a perfect square because there's no integer that when multiplied by itself equals14
The constraint is that you cannot use any built-in library functions like sqrt
to solve this problem. You need to implement your own logic to check if the given number is a perfect square.
The solution uses binary search to find if there exists an integer x
such that x × x = num
. It searches for the smallest integer whose square is greater than or equal to num
. If that integer's square exactly equals num
, then num
is a perfect square.
Intuition
Since we need to find if there exists an integer x
where x * x = num
, we essentially need to search for this value x
in the range from 1
to num
.
The key insight is that if num
is a perfect square, then its square root must be an integer between 1
and num
. Instead of checking every single number from 1
to num
(which would be inefficient for large numbers), we can use binary search to quickly narrow down our search space.
Why does binary search work here? Because the function f(x) = x * x
is monotonically increasing - as x
increases, x * x
also increases. This means if we pick a middle value and compute its square:
- If the square is less than
num
, we know we need to search in the higher half - If the square is greater than
num
, we need to search in the lower half - If the square equals
num
, we've found our answer
The solution cleverly uses bisect_left
with a key function lambda x: x * x
to find the position where num
would fit in the sequence of perfect squares [1, 4, 9, 16, 25, ...]
. This gives us the smallest integer l
such that l * l >= num
. Finally, we just need to check if l * l
exactly equals num
- if it does, then num
is a perfect square; otherwise, it's not.
This approach reduces the time complexity from O(num)
for a linear search to O(log num)
using binary search.
Solution Approach
The solution implements a binary search approach to find if a number is a perfect square. Here's how the implementation works:
Binary Search Setup:
The code uses Python's bisect_left
function to perform the binary search. The search range is set from 1
to num
, represented as range(1, num + 1)
.
Key Function:
The crucial part is the key=lambda x: x * x
parameter. This transforms our search space - instead of searching through the numbers [1, 2, 3, ..., num]
, we're effectively searching through their squares [1, 4, 9, ..., num²]
.
Finding the Position:
bisect_left
finds the leftmost position where num
would fit in the sequence of perfect squares. Since bisect_left
returns a 0-based index and our range starts at 1, we add 1 to get the actual value: l = bisect_left(...) + 1
.
Verification:
After finding l
, we check if l * l == num
. If this condition is true, it means num
is exactly the square of l
, making it a perfect square.
Example Walkthrough:
- For
num = 16
: The binary search finds that16
fits at position 3 (0-indexed) in the sequence[1, 4, 9, 16, 25, ...]
. Adding 1 gives usl = 4
. Since4 * 4 = 16
, the function returnstrue
. - For
num = 14
: The binary search finds that14
would fit between9
and16
. It returns position 3, giving usl = 4
. Since4 * 4 = 16 ≠ 14
, the function returnsfalse
.
The time complexity is O(log num)
due to the binary search, and the space complexity is O(1)
as we only use a constant amount of extra space.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with num = 20
to see how the binary search approach works:
Step 1: Initialize Binary Search
- We're searching in the range
[1, 2, 3, 4, 5, ..., 20]
- The key function transforms each value
x
tox * x
, so we're effectively searching where20
fits in the sequence[1, 4, 9, 16, 25, 36, 49, ...]
Step 2: Binary Search Process
- Start with search range
[1, 20]
, middle =10
- Check:
10 * 10 = 100 > 20
, so search left half[1, 9]
- Check:
- New range
[1, 9]
, middle =5
- Check:
5 * 5 = 25 > 20
, so search left half[1, 4]
- Check:
- New range
[1, 4]
, middle =2
- Check:
2 * 2 = 4 < 20
, so search right half[3, 4]
- Check:
- New range
[3, 4]
, middle =3
- Check:
3 * 3 = 9 < 20
, so search right half[4, 4]
- Check:
- Final range
[4, 4]
, middle =4
- Check:
4 * 4 = 16 < 20
, need to go higher
- Check:
Step 3: Result from bisect_left
bisect_left
returns index4
(0-based) as the position where20
would be inserted- After adding 1 to convert from 0-based index:
l = 5
Step 4: Verification
- Check if
l * l == num
:5 * 5 = 25 ≠ 20
- Since
25 ≠ 20
, returnfalse
Therefore, 20
is not a perfect square. The closest perfect squares are 16
(4²) and 25
(5²), with 20
falling between them.
Solution Implementation
1class Solution:
2 def isPerfectSquare(self, num: int) -> bool:
3 """
4 Determines if a given number is a perfect square.
5
6 Uses binary search to find if there exists an integer whose square equals num.
7
8 Args:
9 num: A positive integer to check
10
11 Returns:
12 True if num is a perfect square, False otherwise
13 """
14 # Handle edge case
15 if num < 1:
16 return False
17
18 # Binary search for the square root
19 # Search range: [1, num]
20 left, right = 1, num
21
22 while left <= right:
23 # Calculate middle point
24 mid = left + (right - left) // 2
25
26 # Calculate square of mid
27 square = mid * mid
28
29 if square == num:
30 # Found exact square root
31 return True
32 elif square < num:
33 # Square is too small, search in upper half
34 left = mid + 1
35 else:
36 # Square is too large, search in lower half
37 right = mid - 1
38
39 # No integer square root found
40 return False
41
1class Solution {
2 public boolean isPerfectSquare(int num) {
3 // Initialize binary search boundaries
4 // left starts at 1 (smallest possible square root)
5 // right starts at num (largest possible square root)
6 int left = 1;
7 int right = num;
8
9 // Binary search to find the square root
10 while (left < right) {
11 // Calculate middle point using unsigned right shift to avoid overflow
12 // This is equivalent to (left + right) / 2 but prevents integer overflow
13 int mid = (left + right) >>> 1;
14
15 // Check if mid squared is greater than or equal to num
16 // Use long multiplication to prevent integer overflow
17 if (1L * mid * mid >= num) {
18 // If mid squared is >= num, the answer is in the left half (including mid)
19 right = mid;
20 } else {
21 // If mid squared is < num, the answer is in the right half (excluding mid)
22 left = mid + 1;
23 }
24 }
25
26 // After the loop, left == right and points to the potential square root
27 // Check if the found value squared equals num
28 return left * left == num;
29 }
30}
31
1class Solution {
2public:
3 bool isPerfectSquare(int num) {
4 // Binary search to find the square root
5 // Search range: [left, right]
6 int left = 1;
7 int right = num;
8
9 // Binary search for the smallest number whose square is >= num
10 while (left < right) {
11 // Calculate middle point to avoid overflow
12 int mid = left + (right - left) / 2;
13
14 // Use long long to prevent overflow when calculating mid * mid
15 if (static_cast<long long>(mid) * mid >= num) {
16 // If mid^2 >= num, the answer is in [left, mid]
17 right = mid;
18 } else {
19 // If mid^2 < num, the answer is in [mid + 1, right]
20 left = mid + 1;
21 }
22 }
23
24 // After the loop, left == right
25 // Check if the found number's square equals num
26 return static_cast<long long>(left) * left == num;
27 }
28};
29
1/**
2 * Determines if a given number is a perfect square
3 * Uses binary search to find the square root efficiently
4 * Time Complexity: O(log n)
5 * Space Complexity: O(1)
6 *
7 * @param num - The number to check
8 * @returns true if num is a perfect square, false otherwise
9 */
10function isPerfectSquare(num: number): boolean {
11 // Initialize binary search boundaries
12 // left starts at 1, right starts at num
13 let left: number = 1;
14 let right: number = num;
15
16 // Binary search for the square root
17 while (left < right) {
18 // Calculate middle point using bit shift for efficiency
19 // Right shift by 1 is equivalent to dividing by 2
20 const mid: number = (left + right) >> 1;
21
22 // Check if mid could be the square root or larger
23 // Using division to avoid overflow: mid >= num/mid is equivalent to mid*mid >= num
24 if (mid >= num / mid) {
25 // If mid squared is greater than or equal to num,
26 // the answer is in the left half (including mid)
27 right = mid;
28 } else {
29 // If mid squared is less than num,
30 // the answer is in the right half (excluding mid)
31 left = mid + 1;
32 }
33 }
34
35 // After binary search, left equals right and points to the potential square root
36 // Check if the found value squared equals the original number
37 return left * left === num;
38}
39
Time and Space Complexity
The time complexity is O(log n)
, where n
is the given number. The bisect_left
function performs a binary search on the range from 1 to num
, which contains n
elements. Binary search divides the search space in half at each step, resulting in logarithmic time complexity.
The space complexity is O(1)
. Although range(1, num + 1)
is used as an argument to bisect_left
, in Python 3, range
objects are lazy iterators that don't create the entire list in memory. The bisect_left
function only evaluates the key
function for specific indices during the binary search process, using constant extra space for variables like search boundaries and the mid-point calculation.
Common Pitfalls
1. Integer Overflow Issue
The most critical pitfall in this solution is the potential for integer overflow when calculating mid * mid
. For very large values of num
, the multiplication mid * mid
can exceed the maximum integer value, causing overflow issues in languages with fixed integer sizes (though Python handles arbitrary precision integers automatically).
Problem Example:
- When
num = 2147483647
(maximum 32-bit integer), andmid = 46341
, calculatingmid * mid = 2147488281
would overflow in a 32-bit system.
Solution:
Instead of comparing mid * mid
with num
, use division to avoid overflow:
if mid == num // mid and num % mid == 0: return True elif mid < num // mid: left = mid + 1 else: right = mid - 1
2. Inefficient Search Range
Setting the initial right boundary to num
is inefficient for large numbers. The square root of any number greater than 1 is always less than or equal to num / 2
.
Problem Example:
- For
num = 100
, searching from 1 to 100 is unnecessary when the square root can't be larger than 50.
Solution: Optimize the initial search range:
left, right = 1, min(num, num // 2 + 1)
3. Not Handling Edge Cases Properly
The code might not handle num = 1
correctly if the search range is optimized incorrectly.
Problem Example:
- If you set
right = num // 2
, then fornum = 1
,right = 0
, making the search range invalid.
Solution: Add special handling for small numbers:
if num < 2: return True # Both 0 and 1 are perfect squares left, right = 2, num // 2
4. Using Wrong Middle Point Calculation
Using (left + right) // 2
instead of left + (right - left) // 2
can cause overflow in languages with fixed integer sizes.
Improved Complete Solution:
class Solution:
def isPerfectSquare(self, num: int) -> bool:
if num < 2:
return True
left, right = 2, num // 2
while left <= right:
mid = left + (right - left) // 2
# Use division to avoid overflow
if mid == num // mid and num % mid == 0:
return True
elif mid < num // mid:
left = mid + 1
else:
right = mid - 1
return False
In a binary min heap, the minimum element can be found in:
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
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
Want a Structured Path to Master System Design Too? Don’t Miss This!