Lumænaut_

Grinding LeetCode.
Day 2: Add Two Numbers

From grade-school addition to linked list mastery

Color ASCII art: an outdated computer on a desk with a bookshelf in the background, suggesting a classic coding workspace.
Illustration in ASCII art style: an outdated computer on a desk with a bookshelf behind it.

Problem Statement

You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each node 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.

Input: l1 = [2,4,3], l2 = [5,6,4]
Output: [7,0,8]
Explanation: 342 + 465 = 807.

Input: l1 = [0], l2 = [0]
Output: [0]

Input: l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
Output: [8,9,9,9,0,0,0,1]
          

Constraints:

  • The number of nodes in each linked list is in the range [1, 100].
  • 0 <= Node.val <= 9
  • It is guaranteed that the list represents a number that does not have leading zeros.

Rewriting in Simple Terms

Imagine you have two numbers written backwards. For instance, 342 is stored as 2 → 4 → 3. You’re asked to add them digit by digit from left to right, just like we did in elementary school, but with the lists reversed. The twist? The result also has to be a reversed linked list. So instead of dealing with big integers, we simulate manual addition with a carry. That’s all there is to it — except we need to do it efficiently while traversing two linked lists that might have different lengths.

Road to Optimal: From Naive to Masterful

Step 1 – The Naive Approach: Convert to Numbers

The most straightforward idea: traverse each linked list, build the actual integer (by reversing digit order), add them, then convert the sum back to a reversed linked list. In Python that might look like:


def get_number(head):
    num = 0
    place = 1
    while head:
        num += head.val * place
        place *= 10
        head = head.next
    return num
# Then do addition and reconstruct
          

This works, but it's fatally flawed when numbers exceed 64-bit integers. Since each linked list can have up to 100 digits, the integer can be as large as 10100 — Python’s big ints can handle it, but in languages like C++ or Java, we would overflow. Also, the problem explicitly intends to train us on linked list traversal and carry management, not big number libraries. So this naive method is inefficient both conceptually and in spirit.

Step 2 – Simulation with Carry (The Standard Approach)

The insight: addition is performed digit by digit from the least significant digit (which is the head of the list, thanks to reverse order). So we walk through both lists simultaneously, sum the current digits plus a carry, store the digit (sum % 10) in a new node, and propagate the carry (sum // 10). We continue until both lists are exhausted and the carry becomes 0.

Step-by-Step Simulation: l1 = [2,4,3] , l2 = [5,6,4]
Step l1 current digit l2 current digit Carry from previous Sum (digit + digit + carry) New digit (sum % 10) New carry (sum // 10) Result list so far
1 2 5 0 7 7 0 [7]
2 4 6 0 10 0 1 [7, 0]
3 3 4 1 8 8 0 [7, 0, 8]
4 None (0) None (0) 0 0 - 0 Finished → [7,0,8]
Each row corresponds to one iteration. Notice the carry that appears in step 2 and gets consumed in step 3. The final carry remains 0, so we stop.
Edge case: l1 = [9,9,9,9,9,9,9] , l2 = [9,9,9,9] (final carry)
Step l1 digit l2 digit Carry in Sum Digit out Carry out Result list
19901881[8]
29911991[8,9]
39911991[8,9,9]
49911991[8,9,9,9]
59None11001[8,9,9,9,0]
69None11001[8,9,9,9,0,0]
79None11001[8,9,9,9,0,0,0]
8NoneNone1110[8,9,9,9,0,0,0,1]
After exhausting both lists, we still have a carry of 1, so we add an extra node with value 1. This matches the expected output [8,9,9,9,0,0,0,1].

This algorithm runs in O(max(m, n)) time, where m and n are lengths of the lists, and O(max(m, n)) space for the result list (if we ignore output, it's optimal). The key is the dummy head trick that simplifies edge cases — you never have to treat the first node specially.

Step 3 – Optimizing for Memory and Edge Cases

Could we avoid creating a new list? In theory, we could reuse one of the input lists to store the result, modifying nodes in place. This would bring space complexity down to O(1) besides the answer. However, it's messy because we might need to extend a list if the final carry adds a new node. Usually interviewers and LeetCode accept the simpler new-list construction because it's cleaner. The real optimization is recognizing we must handle the final carry separately.

Mathematical Formulation

Let the numbers be represented as:
A = Σ (a_i * 10i) , where a_0 is the head of l1 (least significant digit).
B = Σ (b_i * 10i) , where b_0 is the head of l2.
Sum S = A + B = Σ (s_i * 10i) , with 0 ≤ s_i ≤ 9, and carry propagation:

For each position i, define total_i = a_i + b_i + carry_i,
where carry_0 = 0,
s_i = total_i mod 10,
carry_{i+1} = floor(total_i / 10).

The final result has digits [s_0, s_1, ..., s_k] in a linked list, where k is the largest index with s_k ≠ 0 or carry_{k+1} = 1 (then we append one more node with digit 1).

This is exactly the elementary school addition algorithm, formalized for base 10.

Code Solutions in Multiple Languages

Below you’ll find clean implementations of the optimal algorithm in six languages. Each uses the dummy head pattern and runs in O(n) time.

Python 3

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode()
        current = dummy
        carry = 0

        while l1 or l2 or carry:
            val1 = l1.val if l1 else 0
            val2 = l2.val if l2 else 0
            total = val1 + val2 + carry
            carry = total // 10
            current.next = ListNode(total % 10)
            current = current.next

            if l1: l1 = l1.next
            if l2: l2 = l2.next

        return dummy.next
              

Python’s dynamic typing makes this code extremely readable. The dummy node avoids checking if the result list is empty. The ternary operators elegantly handle the case where one list is exhausted.

JavaScript

/**
* Definition for singly-linked list.
* function ListNode(val, next) {
*     this.val = (val===undefined ? 0 : val)
*     this.next = (next===undefined ? null : next)
* }
*/
var addTwoNumbers = function(l1, l2) {
    let dummy = new ListNode(0);
    let current = dummy;
    let carry = 0;

    while (l1 !== null || l2 !== null || carry !== 0) {
        const val1 = l1 ? l1.val : 0;
        const val2 = l2 ? l2.val : 0;
        const sum = val1 + val2 + carry;
        carry = Math.floor(sum / 10);
        current.next = new ListNode(sum % 10);
        current = current.next;

        if (l1) l1 = l1.next;
        if (l2) l2 = l2.next;
    }

    return dummy.next;
};  
              

JavaScript version: note the explicit Math.floor for integer division. The rest is identical in spirit. Great for front-end engineers brushing up on linked list manipulation.

C++

/**
* 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) {
        ListNode* dummy = new ListNode(0);
        ListNode* current = dummy;
        int carry = 0;

        while (l1 != nullptr || l2 != nullptr || carry != 0) {
            int val1 = (l1 != nullptr) ? l1->val : 0;
            int val2 = (l2 != nullptr) ? l2->val : 0;
            int sum = val1 + val2 + carry;
            carry = sum / 10;
            current->next = new ListNode(sum % 10);
            current = current->next;

            if (l1 != nullptr) l1 = l1->next;
            if (l2 != nullptr) l2 = l2->next;
        }

        ListNode* result = dummy->next;
        delete dummy;
        return result;
    }
};
        

C++ demands careful memory management. Here we create nodes with new. The dummy node is deleted after extracting the result. This code showcases modern C++ pointers and the conditional operator.

Go

/**
* Definition for singly-linked list.
* type ListNode struct {
*     Val int
*     Next *ListNode
* }
*/
func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
    dummy := &ListNode{0, nil}
    current := dummy
    carry := 0

    for l1 != nil || l2 != nil || carry != 0 {
        val1, val2 := 0, 0
        if l1 != nil {
            val1 = l1.Val
            l1 = l1.Next
        }
        if l2 != nil {
            val2 = l2.Val
            l2 = l2.Next
        }
        sum := val1 + val2 + carry
        carry = sum / 10
        current.Next = &ListNode{sum % 10, nil}
        current = current.Next
    }
    return dummy.Next
}
              

Go’s explicit nil checks and struct initialization make the algorithm clear. The dummy head pattern translates well. This is idiomatic Go — no classes, just structs with methods when needed.

Kotlin

/**
* Example:
* var li = ListNode(5)
* var v = li.`val`
* Definition for singly-linked list.
* class ListNode(var `val`: Int) {
*     var next: ListNode? = null
* }
*/
class Solution {
    fun addTwoNumbers(l1: ListNode?, l2: ListNode?): ListNode? {
        val dummy = ListNode(0)
        var current: ListNode? = dummy
        var carry = 0
        var p = l1
        var q = l2

        while (p != null || q != null || carry != 0) {
            val x = p?.`val` ?: 0
            val y = q?.`val` ?: 0
            val sum = x + y + carry
            carry = sum / 10
            current?.next = ListNode(sum % 10)
            current = current?.next

            if (p != null) p = p.next
            if (q != null) q = q.next
        }

        return dummy.next
    }
}
              

Kotlin’s safe calls (?.) and Elvis operator (?:) reduce boilerplate. The code reads similarly to Python but with static typing. The dummy node still shines.

Swift

/**
* Definition for singly-linked list.
* public class ListNode {
*     public var val: Int
*     public var next: ListNode?
*     public init() { self.val = 0; self.next = nil; }
*     public init(_ val: Int) { self.val = val; self.next = nil; }
*     public init(_ val: Int, _ next: ListNode?) { self.val = val; self.next = next; }
* }
*/
class Solution {
    func addTwoNumbers(_ l1: ListNode?, _ l2: ListNode?) -> ListNode? {
        let dummy = ListNode(0)
        var current: ListNode? = dummy
        var carry = 0
        var p = l1
        var q = l2

        while p != nil || q != nil || carry != 0 {
            let x = p?.val ?? 0
            let y = q?.val ?? 0
            let sum = x + y + carry
            carry = sum / 10
            current?.next = ListNode(sum % 10)
            current = current?.next

            p = p?.next
            q = q?.next
        }

        return dummy.next
    }
}
              

Swift uses optional chaining and nil-coalescing (??) to keep the code concise. The logic is identical across all languages, demonstrating the universality of the addition algorithm.

Edge Cases & Pitfalls

  • Different list lengths: The algorithm gracefully handles this by defaulting missing nodes to 0.
  • Final carry: e.g., 999 + 1 = 1000 → output list [0,0,0,1]. Our loop condition includes carry != 0 to add that extra node.
  • One list empty (though problem says non-empty, but in follow-ups): Just returns the other list with possible carry addition.
  • Large numbers: The solution works for 100-digit numbers because it never converts to integer — it only deals with digits and carry.

Complexity Analysis

Time Complexity: O(max(m, n)) where m, n are lengths of l1 and l2. We traverse each node at most once.

Space Complexity: O(max(m, n)) for the new list. If we disregard the output, it’s O(1) auxiliary space (only a few variables). Some solutions reuse one of the input lists to achieve O(1) extra space but at the cost of mutating inputs — it’s a tradeoff.

Lessons Learned & Practical Applications

The “Add Two Numbers” problem is a microcosm of how low-level arithmetic works in every digital system. The carry propagation algorithm you implement here is the same fundamental building block used in arbitrary-precision arithmetic libraries, cryptographic modules, and even hardware ALUs. By mastering this pattern, you gain insight into how software handles numbers beyond native machine word sizes.

Beyond interviews, this technique appears in real-world scenarios: implementing big integer arithmetic in blockchain smart contracts where gas costs demand efficient digit-by-digit operations, building high-performance financial software that requires exact decimal arithmetic, or optimizing embedded systems where memory is constrained. The dummy head pattern also teaches a reusable strategy for linked list construction — a pattern that applies to merging sorted lists, partitioning lists, and more.

Further References & Resources

With love for those grinding LeetCode, one day at a time. Master the basics, and the rest will follow.