219 lines
9.4 KiB
YAML
219 lines
9.4 KiB
YAML
title: Sqrt(x)
|
|
slug: sqrtx
|
|
difficulty: easy
|
|
leetcode_id: 69
|
|
leetcode_url: https://leetcode.com/problems/sqrtx/
|
|
categories:
|
|
- binary-search
|
|
- math
|
|
patterns:
|
|
- binary-search
|
|
|
|
function_signature: "def my_sqrt(x: int) -> int:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { x: 4 }
|
|
expected: 2
|
|
- input: { x: 8 }
|
|
expected: 2
|
|
- input: { x: 1 }
|
|
expected: 1
|
|
hidden:
|
|
- input: { x: 0 }
|
|
expected: 0
|
|
- input: { x: 16 }
|
|
expected: 4
|
|
- input: { x: 15 }
|
|
expected: 3
|
|
|
|
description: |
|
|
Given a non-negative integer `x`, return *the square root of* `x` *rounded down to the nearest integer*. The returned integer should be **non-negative** as well.
|
|
|
|
You **must not use** any built-in exponent function or operator.
|
|
|
|
- For example, do not use `pow(x, 0.5)` in C++ or `x ** 0.5` in Python.
|
|
|
|
constraints: |
|
|
- `0 <= x <= 2^31 - 1`
|
|
|
|
examples:
|
|
- input: "x = 4"
|
|
output: "2"
|
|
explanation: "The square root of 4 is 2, so we return 2."
|
|
- input: "x = 8"
|
|
output: "2"
|
|
explanation: "The square root of 8 is 2.82842..., and since we round it down to the nearest integer, 2 is returned."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine you're trying to guess a number between 1 and `x`. Someone will tell you if your guess squared is too high, too low, or exactly right. How would you find the answer efficiently?
|
|
|
|
The key insight is that the square root function is **monotonically increasing** — if `a < b`, then `sqrt(a) < sqrt(b)`. This means the integers `1, 2, 3, ..., x` form a sorted sequence when we consider their squares. When you have a sorted search space and need to find a specific value, **binary search** is the perfect tool.
|
|
|
|
Think of it like this: we're searching for the largest integer `k` such that `k * k <= x`. Instead of checking every number from 1 to x (which could be up to 2 billion!), we repeatedly halve our search space. Start in the middle — if `mid * mid` is too big, search the left half; if it's too small or just right, search the right half while remembering this valid candidate.
|
|
|
|
The "rounded down" requirement means we want the **floor** of the square root. So even if the exact square root is 2.83, we return 2. This translates to finding the largest integer whose square doesn't exceed `x`.
|
|
|
|
approach: |
|
|
We solve this using **Binary Search** on the answer space:
|
|
|
|
**Step 1: Handle the edge case**
|
|
|
|
- If `x` is `0` or `1`, return `x` directly (the square root of 0 is 0, and the square root of 1 is 1)
|
|
|
|
|
|
|
|
**Step 2: Initialise binary search bounds**
|
|
|
|
- `left`: Set to `1` (minimum possible answer for x >= 1)
|
|
- `right`: Set to `x // 2` (for x >= 2, the square root is always <= x/2)
|
|
- `result`: Set to `0` to store our answer
|
|
|
|
|
|
|
|
**Step 3: Perform binary search**
|
|
|
|
- While `left <= right`:
|
|
- Calculate `mid = left + (right - left) // 2` to avoid integer overflow
|
|
- Calculate `square = mid * mid`
|
|
- If `square == x`: we found an exact match, return `mid`
|
|
- If `square < x`: `mid` is a valid candidate (could be our answer), store it in `result` and search right half (`left = mid + 1`)
|
|
- If `square > x`: `mid` is too large, search left half (`right = mid - 1`)
|
|
|
|
|
|
|
|
**Step 4: Return the result**
|
|
|
|
- Return `result`, which holds the largest integer whose square is <= x
|
|
|
|
|
|
|
|
The binary search efficiently narrows down from potentially billions of candidates to the exact answer in at most 31 iterations (log base 2 of 2^31).
|
|
|
|
common_pitfalls:
|
|
- title: Integer Overflow When Squaring
|
|
description: |
|
|
When computing `mid * mid`, the result can overflow if `mid` is large. For example, if `mid = 50000` and we're using 32-bit integers, `mid * mid = 2,500,000,000` exceeds the 32-bit signed integer maximum of ~2.1 billion.
|
|
|
|
In Python, integers have arbitrary precision, so this isn't an issue. But in languages like C++ or Java, you need to either:
|
|
- Use `long long` / `long` for the square calculation
|
|
- Compare using division: `mid <= x / mid` instead of `mid * mid <= x`
|
|
wrong_approach: "Using 32-bit integers for mid * mid"
|
|
correct_approach: "Use 64-bit integers or division-based comparison"
|
|
|
|
- title: Linear Search is Too Slow
|
|
description: |
|
|
A naive approach might iterate from 1 to x, checking each number:
|
|
|
|
```python
|
|
for i in range(1, x + 1):
|
|
if i * i > x:
|
|
return i - 1
|
|
```
|
|
|
|
With `x` up to 2^31 - 1, this means up to 46,340 iterations in the worst case (since sqrt(2^31) ~ 46340). While this might pass, it's inefficient. Binary search solves it in ~15-16 iterations.
|
|
wrong_approach: "Linear scan from 1 to sqrt(x)"
|
|
correct_approach: "Binary search reducing search space by half each iteration"
|
|
|
|
- title: Off-by-One Errors in Binary Search
|
|
description: |
|
|
Binary search is notoriously tricky with boundary conditions. Common mistakes include:
|
|
- Using `left < right` instead of `left <= right`, missing the case where they're equal
|
|
- Not storing valid candidates when `mid * mid < x`
|
|
- Returning `mid` instead of `result` at the end
|
|
|
|
The key is understanding we want the **largest** valid answer. When `mid * mid < x`, `mid` is valid but there might be a larger valid number, so we store it and keep searching right.
|
|
wrong_approach: "Incorrect loop condition or forgetting to track valid candidates"
|
|
correct_approach: "Use left <= right, store valid candidates, return the stored result"
|
|
|
|
key_takeaways:
|
|
- "**Binary search on answer space**: When the answer lies in a sorted range and you can verify if a candidate is valid, binary search works beautifully"
|
|
- "**Monotonic property**: The square function is monotonically increasing, which is the prerequisite for binary search"
|
|
- "**Floor vs exact match**: This problem asks for the floor, so track the largest valid candidate rather than only returning exact matches"
|
|
- "**Foundation for harder problems**: This technique extends to problems like finding cube roots, nth roots, or any monotonic function inversion"
|
|
|
|
time_complexity: "O(log x). Binary search halves the search space each iteration, giving us at most log2(x) iterations."
|
|
space_complexity: "O(1). We only use a constant number of variables (`left`, `right`, `mid`, `result`) regardless of input size."
|
|
|
|
solutions:
|
|
- approach_name: Binary Search
|
|
is_optimal: true
|
|
code: |
|
|
def my_sqrt(x: int) -> int:
|
|
# Edge cases: sqrt(0) = 0, sqrt(1) = 1
|
|
if x < 2:
|
|
return x
|
|
|
|
# Search space: answer is between 1 and x//2
|
|
left, right = 1, x // 2
|
|
result = 0
|
|
|
|
while left <= right:
|
|
# Calculate mid avoiding overflow (not needed in Python, but good habit)
|
|
mid = left + (right - left) // 2
|
|
square = mid * mid
|
|
|
|
if square == x:
|
|
# Found exact square root
|
|
return mid
|
|
elif square < x:
|
|
# mid is valid candidate, but there might be a larger one
|
|
result = mid
|
|
left = mid + 1
|
|
else:
|
|
# mid is too large, search smaller numbers
|
|
right = mid - 1
|
|
|
|
return result
|
|
explanation: |
|
|
**Time Complexity:** O(log x) — Binary search halves the search space each iteration.
|
|
|
|
**Space Complexity:** O(1) — Only uses a few integer variables.
|
|
|
|
This solution uses binary search to find the largest integer whose square is at most `x`. By tracking valid candidates in `result` and continuing to search for potentially larger valid answers, we correctly handle the "round down" requirement.
|
|
|
|
- approach_name: Newton's Method
|
|
is_optimal: false
|
|
code: |
|
|
def my_sqrt(x: int) -> int:
|
|
# Newton's method for finding roots
|
|
# For f(r) = r^2 - x, we iterate: r = r - f(r)/f'(r) = (r + x/r) / 2
|
|
if x < 2:
|
|
return x
|
|
|
|
# Start with initial guess
|
|
r = x
|
|
while r * r > x:
|
|
# Newton's iteration: average of r and x/r
|
|
r = (r + x // r) // 2
|
|
|
|
return r
|
|
explanation: |
|
|
**Time Complexity:** O(log x) — Newton's method converges quadratically.
|
|
|
|
**Space Complexity:** O(1) — Only uses a single variable for the approximation.
|
|
|
|
Newton's method (also known as Heron's method for square roots) uses calculus-based iteration. Starting from an initial guess, each iteration gets closer to the true square root. While mathematically elegant, the binary search approach is often preferred in interviews as it's more straightforward to reason about and debug.
|
|
|
|
- approach_name: Linear Search
|
|
is_optimal: false
|
|
code: |
|
|
def my_sqrt(x: int) -> int:
|
|
# Brute force: check each number from 0 upward
|
|
if x < 2:
|
|
return x
|
|
|
|
i = 1
|
|
while i * i <= x:
|
|
i += 1
|
|
|
|
# i * i > x, so answer is i - 1
|
|
return i - 1
|
|
explanation: |
|
|
**Time Complexity:** O(sqrt(x)) — We iterate up to the square root of x.
|
|
|
|
**Space Complexity:** O(1) — Only uses a counter variable.
|
|
|
|
This brute force approach checks each integer starting from 1 until we find one whose square exceeds x. While correct and simple to understand, it's less efficient than binary search. For x = 2^31 - 1, this takes ~46,340 iterations versus ~31 for binary search.
|