Facebook Pixel

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 because 4 × 4 = 16
  • 25 is a perfect square because 5 × 5 = 25
  • 14 is not a perfect square because there's no integer that when multiplied by itself equals 14

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.

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

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 that 16 fits at position 3 (0-indexed) in the sequence [1, 4, 9, 16, 25, ...]. Adding 1 gives us l = 4. Since 4 * 4 = 16, the function returns true.
  • For num = 14: The binary search finds that 14 would fit between 9 and 16. It returns position 3, giving us l = 4. Since 4 * 4 = 16 ≠ 14, the function returns false.

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 Evaluator

Example 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 to x * x, so we're effectively searching where 20 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]
  • New range [1, 9], middle = 5
    • Check: 5 * 5 = 25 > 20, so search left half [1, 4]
  • New range [1, 4], middle = 2
    • Check: 2 * 2 = 4 < 20, so search right half [3, 4]
  • New range [3, 4], middle = 3
    • Check: 3 * 3 = 9 < 20, so search right half [4, 4]
  • Final range [4, 4], middle = 4
    • Check: 4 * 4 = 16 < 20, need to go higher

Step 3: Result from bisect_left

  • bisect_left returns index 4 (0-based) as the position where 20 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, return false

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), and mid = 46341, calculating mid * 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 for num = 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
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the minimum element can be found in:


Recommended Readings

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

Load More