Files
codetutor/backend/data/questions/two-sum.yaml

180 lines
7.9 KiB
YAML

title: Two Sum
slug: two-sum
difficulty: easy
leetcode_id: 1
leetcode_url: https://leetcode.com/problems/two-sum/
categories:
- arrays
- hash-tables
patterns:
- two-pointers
function_signature: "def two_sum(nums: list[int], target: int) -> list[int]:"
test_cases:
visible:
- input: { nums: [2, 7, 11, 15], target: 9 }
expected: [0, 1]
- input: { nums: [3, 2, 4], target: 6 }
expected: [1, 2]
- input: { nums: [3, 3], target: 6 }
expected: [0, 1]
hidden:
- input: { nums: [1, 5, 3, 7, 2], target: 10 }
expected: [2, 3]
- input: { nums: [-1, -2, -3, -4, -5], target: -8 }
expected: [2, 4]
- input: { nums: [0, 4, 3, 0], target: 0 }
expected: [0, 3]
description: |
Given an array of integers `nums` and an integer `target`, return *indices of the two numbers such that they add up to `target`*.
You may assume that each input would have **exactly one solution**, and you may not use the same element twice.
You can return the answer in any order.
constraints: |
- `2 <= nums.length <= 10^4`
- `-10^9 <= nums[i] <= 10^9`
- `-10^9 <= target <= 10^9`
- Only one valid answer exists
examples:
- input: "nums = [2,7,11,15], target = 9"
output: "[0,1]"
explanation: "Because nums[0] + nums[1] == 9, we return [0, 1]."
- input: "nums = [3,2,4], target = 6"
output: "[1,2]"
explanation: "Because nums[1] + nums[2] == 6, we return [1, 2]."
- input: "nums = [3,3], target = 6"
output: "[0,1]"
explanation: "Because nums[0] + nums[1] == 6, we return [0, 1]."
explanation:
intuition: |
Imagine you're at a party with numbered name tags, and you need to find two people whose numbers add up to a target sum. The brute force approach would be to ask every person to compare their number with every other person — exhausting and slow!
Think of it like this: as you meet each person, you write down their number and where you met them in a notebook (hash map). When you meet someone new, you quickly calculate what number you *need* to find (`target - current number`) and check your notebook. If it's there, you've found your pair instantly!
The key insight is that for any number `x`, its **complement** is `target - x`. Instead of scanning the entire array each time looking for the complement, we store seen numbers in a hash map for O(1) lookup. This transforms our search from O(n) per element to O(1) per element.
Building the hash map as we iterate also elegantly handles the constraint of not using the same element twice — when we check for a complement, it can only be a *previously seen* element, never the current one.
approach: |
We solve this using a **One-Pass Hash Map Approach**:
**Step 1: Initialise a hash map**
- Create an empty dictionary `seen` to store numbers and their indices
- This will let us look up any previously encountered number in O(1) time
&nbsp;
**Step 2: Iterate through the array**
- For each number at index `i`, calculate its complement: `complement = target - num`
- Check if `complement` exists in `seen`
- If found, we have our answer: return `[seen[complement], i]`
- If not found, store the current number and its index: `seen[num] = i`
&nbsp;
**Step 3: Return the result**
- The problem guarantees exactly one solution, so we'll always find a match during iteration
- If somehow no match exists, return an empty list as a fallback
&nbsp;
This approach works because by checking *before* adding to the map, we ensure we never match an element with itself, and we find the earliest valid pair.
common_pitfalls:
- title: The Brute Force Trap
description: |
A common first instinct is to use nested loops to check every pair:
- Outer loop `i` from `0` to `n-1`
- Inner loop `j` from `i+1` to `n-1`
This results in **O(n²) time complexity**. While it works, with `nums.length <= 10^4`, you could have up to 100 million comparisons. The hash map approach reduces this to a single pass.
wrong_approach: "Nested loops comparing all pairs"
correct_approach: "Single pass with hash map for O(1) lookups"
- title: Using the Same Element Twice
description: |
If you add a number to the hash map *before* checking for its complement, you might match an element with itself.
For example, with `nums = [3, 2, 4]` and `target = 6`: if you add `3` to the map first, then check if `6 - 3 = 3` exists, you'd incorrectly find it!
By checking the complement *before* adding the current number, we guarantee we're only matching with previously seen elements.
wrong_approach: "Adding to map before checking complement"
correct_approach: "Check complement first, then add to map"
- title: Returning Values Instead of Indices
description: |
The problem asks for **indices**, not the actual values. A common mistake is returning `[num, complement]` instead of their positions.
Always read the problem carefully — "return indices" means storing and returning index values from the hash map.
wrong_approach: "return [num, complement]"
correct_approach: "return [seen[complement], i]"
key_takeaways:
- "**Hash map for complement lookup**: Trading O(n) space for O(1) lookup time is a foundational optimisation pattern"
- "**Check before insert**: Building data structures incrementally while checking prevents edge cases like self-matching"
- "**The Two Sum pattern**: This exact technique (hash map + complement) appears in many 'find pair' problems like 3Sum, 4Sum, and pair-with-given-sum variants"
- "**Indices vs values**: Always verify what the problem asks you to return — this catches many wrong answers"
time_complexity: "O(n). We traverse the array once, and each hash map lookup/insert is O(1) on average."
space_complexity: "O(n). In the worst case, we store all n elements in the hash map before finding a match (e.g., when the solution is the last two elements)."
solutions:
- approach_name: One-Pass Hash Map
is_optimal: true
code: |
def two_sum(nums: list[int], target: int) -> list[int]:
# Hash map to store number -> index mappings
seen = {}
for i, num in enumerate(nums):
# Calculate what number we need to find
complement = target - num
# Check if we've seen the complement before
if complement in seen:
# Found it! Return both indices
return [seen[complement], i]
# Haven't found a match yet, store current number
seen[num] = i
# Problem guarantees a solution, but return empty as fallback
return []
explanation: |
**Time Complexity:** O(n) — Single pass through the array with O(1) hash map operations.
**Space Complexity:** O(n) — Hash map stores up to n-1 elements before finding the match.
We iterate once, storing each number's index in a hash map. For each number, we check if its complement exists in the map. By checking before inserting, we ensure we never match an element with itself.
- approach_name: Brute Force
is_optimal: false
code: |
def two_sum(nums: list[int], target: int) -> list[int]:
n = len(nums)
# Try every possible pair of indices
for i in range(n):
for j in range(i + 1, n):
# Check if this pair sums to target
if nums[i] + nums[j] == target:
return [i, j]
# No solution found
return []
explanation: |
**Time Complexity:** O(n²) — Nested loops check all possible pairs.
**Space Complexity:** O(1) — No additional data structures needed.
This approach checks every valid pair of indices. While correct and simple to understand, it's inefficient for large inputs. Included here to illustrate the progression from brute force to optimal solution.