← All Posts
DSA · Linked Lists · Part 15 of 28

Arithmetic on Linked Lists

Treat a linked list as a big integer — one decimal digit per node — and a whole family of problems opens up: addition, subtraction, multiplication, and increment. The single most important decision you make before writing any of them is the digit order, because it decides which direction the carry flows and therefore how hard the code is. Get the order right and addition is eight lines; get it wrong and you are fighting the list the whole way.

Why bother modelling a number as a list at all, when a machine int adds in a single instruction? Because a digit list models arbitrary precision. The values in these problems routinely exceed what a 64-bit integer can hold, so you cannot simply walk the list into an int, add, and walk back out — the intermediate conversion overflows and gives a wrong answer. Digit-by-digit arithmetic with an explicit carry is the only correct approach once numbers outgrow the machine word, which is precisely the problem big-integer libraries were built to solve.

A List of Digits Is a Big Integer

The number 342 can live in a list two ways:

Arithmetic carries propagate from least significant to most significant. So LSD order lets the carry flow in the same direction you traverse — head to tail — which is why the classic "Add Two Numbers" stores digits reversed. MSD order reads naturally to a human but forces you to reach the tail before you can start adding, which is the entire difficulty of the "II" variants below.

Add Two Numbers (LeetCode 2) — LSD First

Both numbers are stored least-significant-first, so you add head-to-tail with a running carry and grow the result with a dummy head:

ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
    ListNode dummy(0);
    ListNode* tail = &dummy;
    int carry = 0;
    while (l1 || l2 || carry) {          // one condition, three cases handled
        int sum = carry;
        if (l1) { sum += l1->val; l1 = l1->next; }
        if (l2) { sum += l2->val; l2 = l2->next; }
        carry = sum / 10;
        tail->next = new ListNode(sum % 10);
        tail = tail->next;
    }
    return dummy.next;
}

The elegance is entirely in the loop condition. while (l1 || l2 || carry) unifies what would otherwise be three separate special cases:

Without the unified condition you would write a tail loop for the longer number and a trailing if (carry) tail->next = new ListNode(1); after it. One while absorbs all of it. O(max(n, m)) time, O(max(n, m)) nodes for the result.

▶ Add 342 + 465 — column by column, watch the carry

Digits are stored least-significant-first: 2→4→3 and 5→6→4. Each step adds one column and shows the carry flowing into the next.

Dry run: 342 + 465 in reverse-digit form

Columndigit(l1) + digit(l2) + carry_inResult digitcarry_out
0 (ones)2 + 5 + 0 = 770
1 (tens)4 + 6 + 0 = 1001
2 (hundreds)3 + 4 + 1 = 880

Result LSD-first: 7 → 0 → 8, which reads as 807 — and indeed 342 + 465 = 807. The carry out of the tens column is what turns a naive 4 + 6 = 10 into a digit 0 plus a carried 1.

Add Two Numbers II (LeetCode 445) — MSD First

Now the digits are stored most-significant-first (3→4→2 for 342), and you are not allowed to know the length in advance cheaply. Addition still needs to start from the least-significant end — the tail. There are three standard ways to get there.

Approach A — reverse both, add, reverse back. Turn it into problem 2, then reverse the result. O(1) extra space, but it mutates the input lists (you can reverse them back to restore). Approach B — two stacks. Push every digit, then pop both stacks together so you consume least-significant digits first, prepending each result node. O(n) space, no mutation. Approach C — recursion with padding. Pad the shorter number with leading zeros, recurse to the tail, and let the carry ripple back up the call stack.

// Approach B: two stacks, no input mutation, result built MSD-first by prepending.
ListNode* addTwoNumbersII(ListNode* l1, ListNode* l2) {
    std::stack<int> s1, s2;
    for (; l1; l1 = l1->next) s1.push(l1->val);
    for (; l2; l2 = l2->next) s2.push(l2->val);

    ListNode* head = nullptr;
    int carry = 0;
    while (!s1.empty() || !s2.empty() || carry) {
        int sum = carry;
        if (!s1.empty()) { sum += s1.top(); s1.pop(); }
        if (!s2.empty()) { sum += s2.top(); s2.pop(); }
        carry = sum / 10;
        ListNode* node = new ListNode(sum % 10);
        node->next = head;      // prepend => most-significant digit ends up first
        head = node;
    }
    return head;
}
ApproachExtra spaceMutates input?Notes
A. Reverse ×3O(1)Yes (restore after)Three passes; fiddly if input must stay intact.
B. Two stacksO(n)NoCleanest; prepends result. The interviewer’s favourite.
C. Recursion + paddingO(n) stackNoElegant, but a very long number overflows the stack.
What to say in the interview: "Because I must not reverse the inputs, I’ll push both onto stacks and pop them together — that consumes digits least-significant-first without mutating the lists, and prepending each result node gives me the answer most-significant-first." That names the constraint (no mutation) and the trick (stacks reverse traversal for free).

Subtraction: Borrow and Sign

Subtraction adds two wrinkles addition does not have: the result can be negative, and you can only subtract a smaller magnitude from a larger one digit by digit. So the algorithm is: compare magnitudes first, subtract the smaller from the larger with a borrow, attach the correct sign, and strip leading zeros from the result (500 - 499 = 1, not 001).

// a, b least-significant-first, with |a| >= |b| guaranteed by the caller.
ListNode* subtractMagnitude(ListNode* a, ListNode* b) {
    ListNode dummy(0);
    ListNode* tail = &dummy;
    int borrow = 0;
    while (a) {
        int d = a->val - borrow - (b ? b->val : 0);
        if (d < 0) { d += 10; borrow = 1; } else { borrow = 0; }
        tail->next = new ListNode(d);
        tail = tail->next;
        a = a->next;
        if (b) b = b->next;
    }
    return stripLeadingZeros(dummy.next);
}

// In LSD-first form the most-significant digit is the LAST node,
// so "leading zeros" are trailing zero nodes. Keep at least one digit.
ListNode* stripLeadingZeros(ListNode* head) {
    ListNode* lastNonZero = head;
    for (ListNode* p = head; p; p = p->next)
        if (p->val != 0) lastNonZero = p;
    for (ListNode* p = lastNonZero->next; p; ) { ListNode* nx = p->next; delete p; p = nx; }
    lastNonZero->next = nullptr;
    return head;
}

The sign lives outside the digit list. Compare magnitudes by length first (longer wins), and on a tie compare digit by digit from the most-significant end; if b > a, swap the operands, subtract, and remember to flag the result negative. Each step is O(n), and the whole subtraction is O(n) time, O(n) nodes.

Multiplying Two Lists

Schoolbook multiplication is a double loop: every digit of a times every digit of b lands at position i + j of the product, accumulated, and then the carries are normalised in a single sweep. Multiplication is the one operation that genuinely wants random access by digit position, so it is cleanest to spill the lists into arrays first — a preview of why real bignum libraries never use lists.

// a, b least-significant-first. Returns the product, least-significant-first.
ListNode* multiply(ListNode* A, ListNode* B) {
    std::vector<int> a, b;
    for (; A; A = A->next) a.push_back(A->val);
    for (; B; B = B->next) b.push_back(B->val);

    std::vector<int> prod(a.size() + b.size(), 0);
    for (size_t i = 0; i < a.size(); ++i)
        for (size_t j = 0; j < b.size(); ++j)
            prod[i + j] += a[i] * b[j];         // accumulate partial products

    int carry = 0;                              // normalise carries in one pass
    for (size_t k = 0; k < prod.size(); ++k) {
        int cur = prod[k] + carry;
        prod[k] = cur % 10;
        carry = cur / 10;
    }

    size_t hi = prod.size();                    // strip most-significant zeros
    while (hi > 1 && prod[hi - 1] == 0) --hi;
    ListNode dummy(0);
    ListNode* tail = &dummy;
    for (size_t k = 0; k < hi; ++k) { tail->next = new ListNode(prod[k]); tail = tail->next; }
    return dummy.next;
}

The double loop is O(n·m); the carry-normalisation pass is O(n + m). Accumulating into prod[i + j] before normalising is the crucial move — it separates "collect the raw column sums" from "resolve the carries", so a column can briefly hold a value far larger than 9 without any special handling.

Plus One and the Nines Cascade (LeetCode 369)

Adding one to a number stored most-significant-first is all about the run of trailing nines: 1299 + 1 = 1300. The rightmost non-9 digit absorbs the increment, and every 9 to its right becomes 0. If there is no non-9 digit at all (999), the number grows a new leading digit.

ListNode* plusOne(ListNode* head) {
    ListNode* lastNotNine = nullptr;
    for (ListNode* p = head; p; p = p->next)
        if (p->val != 9) lastNotNine = p;       // rightmost non-9 digit

    if (!lastNotNine) {                          // all nines: 999 + 1 = 1000
        ListNode* one = new ListNode(1);
        one->next = head;
        for (ListNode* p = head; p; p = p->next) p->val = 0;
        return one;
    }
    lastNotNine->val += 1;                       // increment it
    for (ListNode* p = lastNotNine->next; p; p = p->next) p->val = 0;   // zero the tail
    return head;
}

The recursive version is prettier: recurse to the tail, add one there, and return the carry back up. A digit that is 9 turns to 0 and passes the carry along; the first non-9 digit stops it.

int addOne(ListNode* node) {          // returns the carry out of this node
    if (!node) return 1;              // the "+1" itself, arriving past the last digit
    int sum = node->val + addOne(node->next);
    node->val = sum % 10;
    return sum / 10;
}

// Sentinel trick: a leading 0 node absorbs a possible final carry with NO special case.
ListNode* plusOne(ListNode* head) {
    ListNode* sentinel = new ListNode(0);
    sentinel->next = head;
    addOne(sentinel);                 // if all nines, the carry stops in the sentinel
    if (sentinel->val == 0) {         // no overall carry: drop the sentinel
        ListNode* real = sentinel->next;
        delete sentinel;
        return real;
    }
    return sentinel;                  // sentinel became 1: it is the new leading digit
}
Why the sentinel is worth it. The iterative version needs an explicit "all nines" branch that allocates a new head. The sentinel version has no branch for the carry-out case: a leading zero is always there to absorb it, and you simply check afterwards whether it was used. Prepending a dummy to swallow an edge case is the same move that makes dummy heads so useful everywhere else.

Binary to Integer and the Horner Frame (LeetCode 1290)

The gentlest warm-up in this whole family: a list of bits, most-significant-first, to be read as an integer. Fold left with Horner’s scheme:

int getDecimalValue(ListNode* head) {
    int acc = 0;
    for (ListNode* p = head; p; p = p->next)
        acc = acc * 2 + p->val;       // shift the accumulator left, drop in the next bit
    return acc;
}

The pattern acc = acc * base + digit is Horner’s method, and it generalises immediately: use * 10 to parse a decimal list, * 16 for hex, * base for anything. It is a single O(n) pass, O(1) space, and it is exactly how a parser turns a string of digits into a number — no pow, no place-value bookkeeping, just repeated multiply-and-add.

Why Real Bignum Libraries Use Arrays

Every algorithm above is a teaching device. No production big-integer library — GMP, OpenSSL’s BN, Java’s BigInteger — stores one decimal digit per linked node. They use arrays of 32- or 64-bit limbs, and the reasons are the same ones from the memory-model post:

The list versions are how you learn carry, borrow, and place value; the array versions are how you ship them.

Check Yourself

Each item gives a situation; pick the statement that is actually true.

Practice