Problem 0001 · Array · Hash Table · Amortized Analysis
1. Two Sum
From a quadratic Cartesian search to a linear algebraic complement lookup using an inverse hash table in C++.
Problem Statement
You are 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.
Example 1
nums[0] + nums[1] is 2 + 7 = 9, returning [0, 1].
Example 2
nums[1] + nums[2] is 2 + 4 = 6, returning [1, 2].
Example 3
nums[0] + nums[1] is 3 + 3 = 6, returning [0, 1].
Constraints & Follow-up
- 2 ≤ nums.length ≤ 104
- -109 ≤ nums[i] ≤ 109
- -109 ≤ target ≤ 109
- Only one valid answer exists.
Starting from the initial C++ template:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
}
};Mathematical Representation & Index Mapping
Let the input array be modeled as a finite sequence of integers indexed over the discrete domain . We define a function where , and let denote target.
Visual · Sequence Mapping A : I → ℤ (Example 1: T = 9)
Combinatorial Search Space & The O(n²) Baseline
Because addition is commutative () and self-pairing () is forbidden, the naive search space consists of all strictly ordered index pairs . This corresponds to the strict upper triangle of the Cartesian product .
For : pair evaluations.
Visual · Upper-Triangular Search Matrix (I × I for nums = [2, 7, 11, 15], T = 9)
| i \ j | j = 0 (2) | j = 1 (7) | j = 2 (11) | j = 3 (15) |
|---|---|---|---|---|
| i = 0 (2) | i = j (skip) | 2 + 7 = 9 ✓ | 2 + 11 = 13 | 2 + 15 = 17 |
| i = 1 (7) | symmetric | i = j (skip) | 7 + 11 = 18 | 7 + 15 = 22 |
| i = 2 (11) | symmetric | symmetric | i = j (skip) | 11 + 15 = 26 |
| i = 3 (15) | symmetric | symmetric | symmetric | i = j (skip) |
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int n = nums.size();
// Traverse the upper-triangular index space: 0 <= i < j < n
// Evaluates C(n, 2) = n(n - 1) / 2 pairs -> O(n^2) time
for (int i = 0; i < n - 1; ++i) {
for (int j = i + 1; j < n; ++j) {
if (nums[i] + nums[j] == target) {
return {i, j};
}
}
}
return {};
}
};Algebraic Inversion & The Complement Operator
To break the barrier, we eliminate the inner loop by algebraically isolating . Instead of varying both and simultaneously, we fix the right endpoint and ask whether the unique additive complement has already appeared in the prefix .
Decision Predicate at step : Does there exist such that ?
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int n = nums.size();
for (int j = 0; j < n; ++j) {
// Algebraic reduction: a_i + a_j = T <=> a_i = T - a_j
int complement = target - nums[j];
// Instead of a second loop scanning i in {0, ..., j - 1},
// we need an O(1) way to check if `complement` was already seen.
}
return {};
}
};Inverse Mapping via Hash Table (O(1) Lookup)
An array gives forward evaluation , but finding an index from a value requires inverting the function: . By maintaining a running hash table (std::unordered_map<int, int>) that maps each previously seen value to its index , each membership and index query takes amortized time.
Visual Trace · Example 2: nums = [3, 2, 4], T = 6
| Step j | a_j | Complement C(a_j) = 6 - a_j | Hash Map M_j (before insert) | Action |
|---|---|---|---|---|
| j = 0 | 3 | 6 - 3 = 3 | { } | 3 ∉ M0 → insert 3 ↦ 0 |
| j = 1 | 2 | 6 - 2 = 4 | { 3: 0 } | 4 ∉ M1 → insert 2 ↦ 1 |
| j = 2 | 4 | 6 - 4 = 2 | { 3: 0, 2: 1 } | 2 ∈ M2(index 1) → Return [1, 2] ✓ |
Why Query-Before-Insert Solves Duplicate Elements (Example 3: nums = [3, 3], T = 6)
At , is empty, so is inserted. At , we query before inserting nums[1], immediately finding and returning [0, 1] without ever colliding or using the same index twice.
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
// Partial inverse map M_j : value (a_i) -> index (i)
unordered_map<int, int> seen;
for (int j = 0; j < nums.size(); ++j) {
int complement = target - nums[j];
// 1. O(1) membership query: is C(a_j) in dom(M_j)?
if (seen.find(complement) != seen.end()) {
return {seen[complement], j};
}
// 2. State transition: M_{j+1} = M_j U { a_j |-> j }
seen[nums[j]] = j;
}
return {};
}
};Final Optimal C++ Solution — O(n) Time
Combining the single-pass prefix invariant with bucket pre-allocation (seen.reserve(nums.size())) eliminates rehash overhead and achieves optimal time complexity:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> seen;
seen.reserve(nums.size());
for (int j = 0; j < nums.size(); ++j) {
int complement = target - nums[j];
if (auto it = seen.find(complement); it != seen.end()) {
return {it->second, j};
}
seen[nums[j]] = j;
}
return {};
}
};| Approach | Time Complexity | Space Complexity | Ops at n = 10⁴ |
|---|---|---|---|
| Brute Force (Nested Loops) | O(n²) | O(1) | ~5.0 × 10⁷ |
| One-Pass Hash Table (Optimal) | O(n) amortized | O(n) | ≤ 1.0 × 10⁴ |