Introduction to the Add Two Numbers Problem
The "Add Two Numbers" problem is one of the most iconic algorithmic challenges, popularized by LeetCode and frequently encountered in technical interviews. The task involves two non-empty linked lists that represent two non-negative integers. Each list stores digits in reverse order, meaning the head of the list holds the least significant digit. Your goal is to add these two numbers and return the result as a new linked list in the same reversed format.
While the concept of adding two numbers sounds trivial, doing so with linked lists forces you to think carefully about digit-by-digit arithmetic, carry propagation, and edge cases. This problem tests your understanding of linked list traversal, pointer manipulation, and basic arithmetic logic — all fundamental skills for any JavaScript developer.
Why This Problem Matters
You might wonder why we would represent numbers as linked lists when JavaScript handles large integers natively with the BigInt type. The answer lies in what the problem teaches rather than what it solves practically. Here are several reasons this problem is valuable:
- Linked List Mastery: It reinforces how to traverse, construct, and manipulate linked lists — a data structure that underpins many real-world systems like hash map buckets, adjacency lists, and memory allocation chains.
- Carry Handling: Implementing manual addition with carry propagation mirrors how CPUs perform arithmetic, deepening your understanding of low-level computation.
- Edge Case Awareness: Lists of unequal length, final carry overflow, and empty result scenarios teach defensive programming.
- Interview Readiness: This problem appears so frequently in coding interviews that mastering it builds confidence and pattern recognition for similar challenges.
Understanding the Problem Statement
Before writing any code, let us clearly define the problem. You are given two linked lists, l1 and l2. Each node contains a single digit (0–9), and the digits are stored in reverse order. For example, the number 342 is represented as 2 -> 4 -> 3. You must add the two numbers and return the sum as a linked list in the same reversed order.
Consider this example: l1 = [2, 4, 3] represents 342, and l2 = [5, 6, 4] represents 465. Their sum is 807, which should be returned as [7, 0, 8]. Notice how the reverse ordering actually simplifies the addition because you start from the least significant digit, exactly as you would when adding numbers by hand.
Defining the Linked List Node
In JavaScript, we first need a class to represent each node in the linked list. This class holds a value and a reference to the next node.
class ListNode {
constructor(val = 0, next = null) {
this.val = val;
this.next = next;
}
}
This simple class is the foundation for everything that follows. The default values allow us to create nodes easily, and the next pointer lets us chain nodes together.
Step-by-Step Solution
Now let us build the solution incrementally. The core idea is to iterate through both lists simultaneously, adding corresponding digits along with any carry from the previous step. We construct a new linked list as we go.
Step 1: Initialize Variables
We need a variable to track the carry, a dummy head node to simplify list construction, and a current pointer that moves along as we append new nodes.
function addTwoNumbers(l1, l2) {
let carry = 0;
const dummyHead = new ListNode(0);
let current = dummyHead;
// More code follows...
}
The dummy head is a common technique in linked list problems. Instead of handling the first node as a special case, we create a placeholder node and append real nodes after it. At the end, we return dummyHead.next, which points to the actual first node of our result.
Step 2: Traverse Both Lists
We loop while there are still nodes in either list or a carry remains. Inside the loop, we extract the digit values, defaulting to 0 if one list is shorter than the other.
function addTwoNumbers(l1, l2) {
let carry = 0;
const dummyHead = new ListNode(0);
let current = dummyHead;
let p1 = l1;
let p2 = l2;
while (p1 !== null || p2 !== null || carry !== 0) {
const val1 = p1 ? p1.val : 0;
const val2 = p2 ? p2.val : 0;
const sum = val1 + val2 + carry;
carry = Math.floor(sum / 10);
const digit = sum % 10;
current.next = new ListNode(digit);
current = current.next;
if (p1) p1 = p1.next;
if (p2) p2 = p2.next;
}
return dummyHead.next;
}
Let us break down what happens inside the loop. First, we retrieve the digit from each list, using 0 as a fallback when a list has been exhausted. We compute the sum of both digits plus any carry from the previous iteration. The new digit is the sum modulo 10, and the new carry is the integer division of the sum by 10. We then create a new node with this digit, attach it to our result list, and advance the current pointer. Finally, we move p1 and p2 forward if they still point to valid nodes.
Step 3: Handle the Final Carry
Notice that our while loop condition includes carry !== 0. This is crucial because after processing all digits from both lists, there might still be a carry that needs to become a new node. For example, adding 5 and 5 gives 10, which produces a digit of 0 and a carry of 1. Without this condition, we would lose that final carry and return an incorrect result.
Testing the Solution
To verify our solution works correctly, let us create helper functions to build linked lists from arrays and convert them back to arrays for easy inspection.
function arrayToList(arr) {
const dummy = new ListNode(0);
let current = dummy;
for (const num of arr) {
current.next = new ListNode(num);
current = current.next;
}
return dummy.next;
}
function listToArray(head) {
const result = [];
let current = head;
while (current !== null) {
result.push(current.val);
current = current.next;
}
return result;
}
Now we can run several test cases to confirm correctness across different scenarios.
// Test case 1: Basic addition
const l1 = arrayToList([2, 4, 3]);
const l2 = arrayToList([5, 6, 4]);
console.log(listToArray(addTwoNumbers(l1, l2))); // [7, 0, 8]
// Test case 2: Different lengths
const l3 = arrayToList([9, 9, 9, 9]);
const l4 = arrayToList([9, 9]);
console.log(listToArray(addTwoNumbers(l3, l4))); // [8, 9, 0, 0, 1]
// Test case 3: Result with final carry
const l5 = arrayToList([5]);
const l6 = arrayToList([5]);
console.log(listToArray(addTwoNumbers(l5, l6))); // [0, 1]
// Test case 4: One number is zero
const l7 = arrayToList([0]);
const l8 = arrayToList([0]);
console.log(listToArray(addTwoNumbers(l7, l8))); // [0]
Each test case exercises a different edge case. The first is a straightforward addition. The second handles lists of unequal length. The third verifies that a final carry creates a new node. The fourth confirms that adding two zeros produces a single zero node.
Complexity Analysis
Understanding the time and space complexity of your solution is essential, especially in interview settings. Let us analyze both dimensions.
Time Complexity
The algorithm traverses each node of both lists exactly once. If m is the length of l1 and n is the length of l2, the loop runs at most max(m, n) + 1 times. The extra iteration accounts for a possible final carry. Therefore, the time complexity is O(max(m, n)), which is optimal since we must examine every digit at least once.
Space Complexity
The result linked list contains at most max(m, n) + 1 nodes, again accounting for the final carry. Aside from the output, we only use a constant number of variables (carry, current, p1, p2). Thus, the space complexity is O(max(m, n)) for the output structure, or O(1) auxiliary space if we do not count the output.
Best Practices
Writing a correct solution is only part of the job. Writing clean, maintainable, and robust code is equally important. Here are several best practices to keep in mind.
- Use a dummy head node: This eliminates the need for special-casing the first node and makes the code cleaner and less error-prone.
- Keep the loop condition comprehensive: Including
carry !== 0in the condition ensures you never miss a trailing carry, which is a common bug. - Guard against null nodes: Always check whether a pointer is null before accessing its
valornextproperties. Using the ternary operator with a default of 0 is a concise way to handle this. - Write helper functions for testing: Functions like
arrayToListandlistToArraymake it much easier to test and debug your solution. - Test edge cases thoroughly: Always test with empty-ish inputs (single zero nodes), lists of unequal length, and cases that produce a final carry.
- Avoid mutating input lists: Our solution creates a new list rather than modifying the inputs, which is a good habit that prevents unintended side effects.
Recursive Alternative
For completeness, here is a recursive implementation. Some developers find recursion more elegant, though it risks stack overflow for very long lists.
function addTwoNumbersRecursive(l1, l2, carry = 0) {
if (l1 === null && l2 === null && carry === 0) {
return null;
}
const val1 = l1 ? l1.val : 0;
const val2 = l2 ? l2.val : 0;
const sum = val1 + val2 + carry;
const node = new ListNode(sum % 10);
node.next = addTwoNumbersRecursive(
l1 ? l1.next : null,
l2 ? l2.next : null,
Math.floor(sum / 10)
);
return node;
}
This recursive version follows the same logic but expresses it in a functional style. Each call handles one digit and delegates the rest to the next recursive call. The base case triggers when both lists are exhausted and no carry remains.
Common Pitfalls to Avoid
Even experienced developers can stumble on this problem. Here are some mistakes to watch out for.
- Forgetting the final carry: If your loop condition only checks whether
p1orp2is non-null, you will miss the case where a carry remains after the last digits are processed. - Incorrect carry calculation: Using
Math.floor(sum / 10)is correct. Avoid using bitwise operators orparseInt, which can behave unexpectedly with certain values. - Modifying input lists: Reusing nodes from the input lists to build the result can corrupt the original data and lead to subtle bugs in larger programs.
- Not handling unequal lengths: Always default to 0 when one list runs out of nodes. Failing to do so will cause null reference errors.
Conclusion
The Add Two Numbers problem is a deceptively simple challenge that exercises multiple core programming skills at once. By breaking it down into manageable steps — initializing a dummy head, traversing both lists in parallel, computing sums with carry, and handling edge cases — you arrive at a clean and efficient solution. The iterative approach runs in linear time and space, handles all edge cases gracefully, and serves as a reliable template for similar linked list problems. Whether you are preparing for interviews or simply sharpening your algorithmic thinking, mastering this problem builds a strong foundation for tackling more complex data structure challenges in JavaScript and beyond.