Problem 0002 · Linked List · Math · Arbitrary Precision Arithmetic
2. Add Two Numbers
Rigorous mathematical modeling of reverse-order positional digit streams, formal inductive carry propagation in base-10 arithmetic, and optimal pointer construction in C++.
Problem Statement
You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each of their nodes contains a single digit. Add the two numbers and return the sum as a linked list.
You may assume the two numbers do not contain any leading zero, except the number 0 itself.
Example 1
Reverse representations correspond to:342 + 465 = 807.
Example 2
Single zero identity:0 + 0 = 0.
Example 3
Ripple carry expands length:9999999 + 9999 = 10009998.
Constraints & Invariants
- The number of nodes in each linked list is in the range .
- for all nodes.
- Guaranteed that the list represents a number that does not have leading zeros (except the number 0 itself).
The canonical LeetCode C++ signature:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
}
};Mathematical Representation · Little-Endian Positional Numeral System
In standard radix- notation (where ), a non-negative integer is represented as a polynomial evaluated at base . Because the singly linked list stores the digits in reverse order, the head node contains the least significant digit (LSD, corresponding to ), and each subsequent pointer step increment corresponds to the next ascending power of 10.
Let a singly-linked list of length be modeled as an ordered tuple of digits:
The bijective numerical evaluation function is:
Visual · Positional Rank Correspondence (Example 1: 342 + 465 = 807)
The reverse orientation is an immense computational gift: in elementary addition, human arithmetic and digital ALUs proceed from least significant digit to most significant digit because the carry propagates strictly from rank to rank . Hence, the singly linked list can be processed in a single forward pass without needing stack reversal or double-ended queues!
// Anti-Pattern: Attempting to reconstruct native 64-bit integers
// Fails when list length m, n > 19 digits (max uint64_t ≈ 1.84 * 10^19)
class SolutionNaiveOverflow {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
unsigned long long num1 = 0, num2 = 0;
unsigned long long base = 1;
// Traverse and multiply by 10^k
while (l1) {
num1 += l1->val * base; // OVERFLOW when length > 19 (n can be 100!)
base *= 10;
l1 = l1->next;
}
// Direct arithmetic evaluation in CPU registers is impossible for 100 digits!
// We must stream and process digit-by-digit like an arbitrary-precision ALU.
return nullptr;
}
};Formal Carry Recurrence & Invariant Proof
To handle arbitrary lengths without allocating variable-length buffers or bignum objects, we formalize the addition as a deterministic finite-state transducer acting on the discrete sequence index .
Let the input sequences be zero-extended for all indices :
Define the carry sequence and digit sequence inductively:
Claim: for all .
Proof by Induction: Base case: . Assume induction hypothesis . Since and :
Applying the floor function gives:
Since , . Therefore for all . Q.E.D.
Execution State Trace · Cascading Ripple Carry
The most demanding structural edge case occurs when the input lengths are asymmetric and a continuous series of nines creates a long carry chain that propagates beyond the original length of both lists (Example 3).
Trace Table · l1 = [9,9,9,9,9,9,9] (m = 7), l2 = [9,9,9,9] (n = 4)
| Rank k | a_k (l1) | b_k (l2) | c_k (in) | σ_k = a_k + b_k + c_k | s_k = σ_k % 10 | c_{k+1} (out) | Appended Node |
|---|---|---|---|---|---|---|---|
| k = 0 | 9 | 9 | 0 | 9 + 9 + 0 = 18 | 8 | 1 | Node(8) |
| k = 1 | 9 | 9 | 1 | 9 + 9 + 1 = 19 | 9 | 1 | Node(9) |
| k = 2 | 9 | 9 | 1 | 9 + 9 + 1 = 19 | 9 | 1 | Node(9) |
| k = 3 | 9 | 9 | 1 | 9 + 9 + 1 = 19 | 9 | 1 | Node(9) |
| k = 4 | 9 | 0 (null) | 1 | 9 + 0 + 1 = 10 | 0 | 1 | Node(0) |
| k = 5 | 9 | 0 (null) | 1 | 9 + 0 + 1 = 10 | 0 | 1 | Node(0) |
| k = 6 | 9 | 0 (null) | 1 | 9 + 0 + 1 = 10 | 0 | 1 | Node(0) |
| k = 7 | 0 (null) | 0 (null) | 1 | 0 + 0 + 1 = 1 | 1 | 0 | Node(1) |
Sentinel (Dummy Head) Pattern & Memory Semantics
When constructing a dynamically allocated linked list, a common implementation source of bugs is distinguishing the initialization of the list head from the subsequent append operations:
Without a dummy node, every iteration requires a conditional branch:
By allocating a local stack object ListNode dummy(0);:
- No heap allocation for the dummy: zero heap fragmentation and automatic destructor reclamation.
- Uniform loop body:
tail->next = new ListNode(...)unconditionally executes every iteration. - The true head is cleanly accessed via
dummy.next.
Optimal C++ Implementations
Approach 1 · Iterative with Sentinel Pointer (Production Standard)
Single-pass iterative loop over the union predicate l1 != nullptr || l2 != nullptr || carry != 0. Guarantees zero heap leak of dummy node and minimal CPU instruction footprint:
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
// Sentinel node: eliminates branch checks for the head of the new list
ListNode dummy(0);
ListNode* tail = &dummy;
int carry = 0; // Invariant: carry in {0, 1}
// Loop invariant: continue while digits remain in l1 or l2,
// or a pending carry bit needs to be materialized into a node
while (l1 != nullptr || l2 != nullptr || carry != 0) {
int sum = carry;
if (l1 != nullptr) {
sum += l1->val;
l1 = l1->next;
}
if (l2 != nullptr) {
sum += l2->val;
l2 = l2->next;
}
// Inductive state transition:
carry = sum / 10; // c_{k+1} = floor(sigma_k / 10)
tail->next = new ListNode(sum % 10); // s_k = sigma_k mod 10
tail = tail->next;
}
return dummy.next;
}
};Approach 2 · Recursive Structural Induction
Directly translates the inductive mathematical specification into functional recursion. Each stack frame computes one digit and passes carry to the recursive call:
class SolutionRecursive {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2, int carry = 0) {
// Base case: both streams exhausted and no lingering carry
if (!l1 && !l2 && carry == 0) {
return nullptr;
}
int sum = carry;
if (l1) { sum += l1->val; l1 = l1->next; }
if (l2) { sum += l2->val; l2 = l2->next; }
// Inductively construct node and recurse for the next positional rank 10^{k+1}
ListNode* current = new ListNode(sum % 10);
current->next = addTwoNumbers(l1, l2, sum / 10);
return current;
}
};| Approach | Time Complexity | Auxiliary Space | Call Stack Depth | Allocated Nodes |
|---|---|---|---|---|
| Iterative Sentinel (Optimal) | O(max(m, n)) | O(1) | O(1) | ≤ max(m, n) + 1 |
| Recursive Induction | O(max(m, n)) | O(max(m, n)) stack | ≤ max(m, n) + 1 | ≤ max(m, n) + 1 |
Advanced Follow-up · In-Place Node Mutation
In memory-constrained embedded systems or hot allocator paths, dynamically allocating new nodes invokes the OS heap allocator (malloc / operator new), which introduces latency and cache misses.
If caller contract permits mutating the input lists, we can reuse either l1 or l2 in place by overwriting node->val = sum % 10. Only if a final carry exists () does a single node need to be allocated at the tail, reducing heap allocations from to at most !