196 lines
8.2 KiB
YAML
196 lines
8.2 KiB
YAML
title: Happy Number
|
|
slug: happy-number
|
|
difficulty: easy
|
|
leetcode_id: 202
|
|
leetcode_url: https://leetcode.com/problems/happy-number/
|
|
categories:
|
|
- math
|
|
- hash-tables
|
|
patterns:
|
|
- fast-slow-pointers
|
|
|
|
function_signature: "def is_happy(n: int) -> bool:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { n: 19 }
|
|
expected: true
|
|
- input: { n: 2 }
|
|
expected: false
|
|
- input: { n: 1 }
|
|
expected: true
|
|
hidden:
|
|
- input: { n: 7 }
|
|
expected: true
|
|
- input: { n: 4 }
|
|
expected: false
|
|
- input: { n: 100 }
|
|
expected: true
|
|
|
|
description: |
|
|
Write an algorithm to determine if a number `n` is happy.
|
|
|
|
A **happy number** is a number defined by the following process:
|
|
|
|
- Starting with any positive integer, replace the number by the sum of the squares of its digits.
|
|
- Repeat the process until the number equals `1` (where it will stay), or it **loops endlessly in a cycle** which does not include `1`.
|
|
- Those numbers for which this process **ends in 1** are happy.
|
|
|
|
Return `true` if `n` is a happy number, and `false` if not.
|
|
|
|
constraints: |
|
|
- `1 <= n <= 2^31 - 1`
|
|
|
|
examples:
|
|
- input: "n = 19"
|
|
output: "true"
|
|
explanation: "1^2 + 9^2 = 82 -> 8^2 + 2^2 = 68 -> 6^2 + 8^2 = 100 -> 1^2 + 0^2 + 0^2 = 1"
|
|
- input: "n = 2"
|
|
output: "false"
|
|
explanation: "The sequence 2 -> 4 -> 16 -> 37 -> 58 -> 89 -> 145 -> 42 -> 20 -> 4 enters a cycle that never reaches 1."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Think of this problem as following a path through a maze of numbers. Starting from `n`, you compute the sum of squared digits to get the next number, then repeat. The key insight is that this sequence must eventually do one of two things: either reach `1` (happy!) or enter a cycle (unhappy).
|
|
|
|
Why must it cycle? Because the sum of squared digits for any number has an upper bound. For a number with `d` digits, the maximum sum is `d * 81` (when all digits are `9`). For the largest input (`2^31 - 1`, which has 10 digits), the maximum possible sum is 810. So after at most one step, you're working with numbers in a bounded range, and a bounded sequence that never terminates must eventually repeat.
|
|
|
|
This cycle-detection insight opens up two elegant solutions:
|
|
|
|
1. **Hash Set**: Track every number you've seen. If you see a repeat before reaching `1`, there's a cycle.
|
|
2. **Floyd's Cycle Detection (Fast-Slow Pointers)**: Use two "runners" through the sequence at different speeds. If there's a cycle, the fast runner will eventually lap the slow runner.
|
|
|
|
The fast-slow pointer approach is particularly elegant because it uses O(1) space instead of O(n) for storing visited numbers.
|
|
|
|
approach: |
|
|
We solve this using **Floyd's Cycle Detection** (also known as the tortoise and hare algorithm):
|
|
|
|
**Step 1: Define a helper function**
|
|
|
|
- `get_next(n)`: Computes the sum of squares of digits
|
|
- Extract each digit using modulo and integer division
|
|
- Square each digit and accumulate the sum
|
|
|
|
|
|
|
|
**Step 2: Initialise two pointers**
|
|
|
|
- `slow`: Starts at `n`, moves one step at a time
|
|
- `fast`: Starts at `get_next(n)`, moves two steps at a time
|
|
|
|
|
|
|
|
**Step 3: Run the cycle detection loop**
|
|
|
|
- While `fast != 1` and `fast != slow`:
|
|
- Move `slow` one step: `slow = get_next(slow)`
|
|
- Move `fast` two steps: `fast = get_next(get_next(fast))`
|
|
- If they meet before reaching `1`, there's a cycle (unhappy)
|
|
- If `fast` reaches `1`, the number is happy
|
|
|
|
|
|
|
|
**Step 4: Return the result**
|
|
|
|
- Return `fast == 1`
|
|
- If `fast` is `1`, we found happiness; otherwise we detected a cycle
|
|
|
|
common_pitfalls:
|
|
- title: Infinite Loop Without Cycle Detection
|
|
description: |
|
|
A naive approach might just keep computing the next number forever:
|
|
|
|
```python
|
|
while n != 1:
|
|
n = sum_of_squares(n)
|
|
return True
|
|
```
|
|
|
|
This will never terminate for unhappy numbers like `2`, which cycle endlessly through `2 -> 4 -> 16 -> 37 -> 58 -> 89 -> 145 -> 42 -> 20 -> 4 -> ...`
|
|
|
|
You **must** detect cycles, either with a hash set or Floyd's algorithm.
|
|
wrong_approach: "Loop until n equals 1"
|
|
correct_approach: "Track visited numbers or use Floyd's cycle detection"
|
|
|
|
- title: Forgetting Edge Cases
|
|
description: |
|
|
The number `1` is already happy (sum of squares of `1` is `1`). Single-digit numbers like `7` are also happy (`7 -> 49 -> 97 -> 130 -> 10 -> 1`).
|
|
|
|
Make sure your initial setup handles these correctly. With Floyd's algorithm, initialising `slow = n` and `fast = get_next(n)` naturally handles `n = 1` because `fast` immediately becomes `1`.
|
|
|
|
- title: Integer Overflow in get_next
|
|
description: |
|
|
When extracting digits, some implementations might use string conversion which is slower. The mathematical approach using `n % 10` and `n // 10` is both faster and avoids any potential issues with very large numbers during intermediate steps.
|
|
|
|
However, since the sum of squared digits is bounded (maximum ~810 for 10-digit numbers), overflow is not a concern for the result.
|
|
|
|
key_takeaways:
|
|
- "**Cycle detection pattern**: Floyd's algorithm (fast-slow pointers) is useful whenever you need to detect cycles in a sequence with O(1) space"
|
|
- "**Bounded sequences**: Recognising that the sequence values are bounded (max ~810) proves that cycles must occur for non-happy numbers"
|
|
- "**Math vs Hash Table tradeoff**: The hash set approach is simpler to understand but uses O(k) space where k is the cycle length; Floyd's uses O(1)"
|
|
- "**Related problems**: This pattern applies to Linked List Cycle, Find the Duplicate Number, and other sequence-based cycle problems"
|
|
|
|
time_complexity: "O(log n). The number of digits in n is O(log n), and we process each number in the sequence. The sequence length is bounded by a constant for any starting value."
|
|
space_complexity: "O(1) for Floyd's algorithm, or O(log n) for the hash set approach (storing visited numbers)."
|
|
|
|
solutions:
|
|
- approach_name: Floyd's Cycle Detection
|
|
is_optimal: true
|
|
code: |
|
|
def is_happy(n: int) -> bool:
|
|
def get_next(num: int) -> int:
|
|
"""Calculate sum of squares of digits."""
|
|
total = 0
|
|
while num > 0:
|
|
digit = num % 10 # Extract last digit
|
|
total += digit * digit # Add its square
|
|
num //= 10 # Remove last digit
|
|
return total
|
|
|
|
# Floyd's algorithm: slow moves 1 step, fast moves 2 steps
|
|
slow = n
|
|
fast = get_next(n)
|
|
|
|
# Continue until fast reaches 1 or they meet (cycle detected)
|
|
while fast != 1 and slow != fast:
|
|
slow = get_next(slow) # One step
|
|
fast = get_next(get_next(fast)) # Two steps
|
|
|
|
# Happy if we reached 1, unhappy if cycle detected
|
|
return fast == 1
|
|
explanation: |
|
|
**Time Complexity:** O(log n) — Each number has O(log n) digits to process, and the sequence is bounded.
|
|
|
|
**Space Complexity:** O(1) — Only uses two pointer variables regardless of input size.
|
|
|
|
Floyd's cycle detection elegantly solves the problem: if a cycle exists, the fast pointer will eventually catch up to the slow pointer. If no cycle exists (happy number), fast reaches 1 first.
|
|
|
|
- approach_name: Hash Set
|
|
is_optimal: false
|
|
code: |
|
|
def is_happy(n: int) -> bool:
|
|
def get_next(num: int) -> int:
|
|
"""Calculate sum of squares of digits."""
|
|
total = 0
|
|
while num > 0:
|
|
digit = num % 10
|
|
total += digit * digit
|
|
num //= 10
|
|
return total
|
|
|
|
# Track all numbers we've seen
|
|
seen = set()
|
|
|
|
while n != 1 and n not in seen:
|
|
seen.add(n) # Mark current number as visited
|
|
n = get_next(n) # Move to next in sequence
|
|
|
|
# Happy if we reached 1, unhappy if we saw a repeat
|
|
return n == 1
|
|
explanation: |
|
|
**Time Complexity:** O(log n) — Same as Floyd's approach.
|
|
|
|
**Space Complexity:** O(log n) — Stores visited numbers in the set.
|
|
|
|
This approach is more intuitive: just remember what you've seen. If you see a number twice before reaching 1, you're in a cycle. The tradeoff is using extra memory for the set.
|