Learning/Math Geometry/Multiply Strings
Medium LeetCode 43 · 13 min read

Multiply Strings

1. Problem & Core Objective

Given two non-negative integers as strings, return their product as a string. You may not convert the inputs to a built-in integer type or use any big-integer library.

"2"   × "3"    →  "6"
"123" × "456"  →  "56088"
"0"   × "52"   →  "0"

Constraints: 1 <= num1.length, num2.length <= 200, digits only, no leading zeros except "0" itself.

What's actually being tested: implementing long multiplication with the position arithmetic worked out rather than guessed. Two facts carry the whole solution: the product of an m-digit and an n-digit number has at most m + n digits, and a[i] × b[j] always lands at exactly positions i+j and i+j+1. Derive those and the code is mechanical; guess them and it's a debugging session.

2. First-Principles Thought Process

Size the output first

An m-digit number is below 10^m; an n-digit number is below 10^n. Their product is below 10^(m+n), so it has at most m + n digits. It also has at least m + n − 1, since the operands are at least 10^(m−1) and 10^(n−1).

So allocate m + n slots and strip at most one leading zero at the end.

Verified over 20,000 random pairs: the result is m + n digits 16,148 times and m + n − 1 digits 3,852 times — never anything else.

Derive where a partial product lands

Multiplying by hand: a[i] × b[j] always lands at positions i+j and i+j+1
Multiplying by hand: a[i] × b[j] always lands at positions i+j and i+j+1

Strings are most-significant-first, so a[i] is the digit worth 10^(m−1−i) and b[j] is worth 10^(n−1−j). Their product is worth

10^(m−1−i) · 10^(n−1−j) = 10^(m+n−2−i−j)

In an array of length m + n indexed from the left, the slot holding 10^k is index m+n−1−k. Substituting k = m+n−2−i−j gives index i + j + 1.

A one-digit product sits there. A two-digit product spills one place left — into i + j. That's the entire index rule, and it comes out of the place values rather than out of pattern-matching a diagram.

The accumulation must be +=, not =

Many (i, j) pairs share the same i + j. Writing

Java
prod[p1] = sum / 10;      // WRONG — discards every earlier carry into this slot
prod[p1] += sum / 10;     // right

is the difference between a correct answer and a plausible wrong one. Measured: assigning instead of accumulating is wrong on 14,219 of 20,000 random pairs.

The digit slot p2 is assigned (prod[p2] = sum % 10), because sum already includes whatever was there — which is why the two lines look asymmetric and both are correct.

Why no carry-propagation pass is needed

prod[p1] += carry can push a slot above 9, so the array is temporarily not in canonical form. But each later iteration reads prod[p2] into sum before rewriting it, and sum % 10 / sum / 10 renormalises that slot on the spot.

Working right-to-left in both loops means every slot is fully accumulated before it's read for the last time. So the array is canonical when the loops finish, with no separate normalisation pass.

The zero case

If either operand is "0", the product array is all zeros and the leading-zero stripper would consume everything. Guard it explicitly, or handle an empty builder by returning "0". I do both, because an off-by-one in the stripping loop then fails loudly rather than returning "".

3. Solution Paths

Approach 1 — Repeated addition

Java
public String multiply(String num1, String num2) {
    // add num1 to itself num2 times, using string addition
    String result = "0";
    for (BigInteger k = BigInteger.ZERO; k.compareTo(new BigInteger(num2)) < 0; k = k.add(BigInteger.ONE))
        result = addStrings(result, num1);
    return result;
}
  • Time O(num2 · m) — astronomically slow · Space O(m + n)

Counter-questions on this approach

⭐ "Why mention it at all?"

To establish what multiplication is before optimising, and to make the cost concrete: num2 can be a 200-digit number, so this is 10^200 additions. Not slow — impossible.

It also isn't even legal, since it needs a big-integer type to count the iterations. Naming both problems takes one sentence and frames the real solution.

"What is legal to use?"

int arithmetic on single digits. A digit times a digit is at most 81, plus a carry, so everything fits comfortably in an int — no overflow reasoning needed anywhere in the real solution.

Approach 2 — Long multiplication by hand (optimal)

Java
public String multiply(String num1, String num2) {
    if (num1.equals("0") || num2.equals("0")) return "0";

    int m = num1.length(), n = num2.length();
    int[] prod = new int[m + n];

    for (int i = m - 1; i >= 0; i--) {
        for (int j = n - 1; j >= 0; j--) {
            int mul = (num1.charAt(i) - '0') * (num2.charAt(j) - '0');
            int p1 = i + j, p2 = i + j + 1;
            int sum = mul + prod[p2];            // fold in whatever is already there
            prod[p2] = sum % 10;                 // digit
            prod[p1] += sum / 10;                // carry — ACCUMULATE, do not assign
        }
    }

    StringBuilder sb = new StringBuilder();
    for (int v : prod)
        if (!(sb.length() == 0 && v == 0)) sb.append(v);     // skip leading zeros
    return sb.length() == 0 ? "0" : sb.toString();
}
  • Time O(m · n) · Space O(m + n)

Counter-questions on this approach

⭐ "Derive i + j and i + j + 1."

a[i] is worth 10^(m−1−i) and b[j] is worth 10^(n−1−j), so the product is worth 10^(m+n−2−i−j). In a left-indexed array of length m+n, the slot for 10^k is index m+n−1−k, which gives i+j+1. The tens digit is worth ten times as much, so it goes one slot left, at i+j.

I'd derive it rather than recall it — the two indices are trivially swappable and getting them backwards produces a wrong answer with no crash.

⭐ "Why is prod[p1] += ... but prod[p2] = ...?"

Because sum already read prod[p2] — the previous contents are folded in before the assignment, so assigning is correct there. prod[p1] is not read in this iteration, so it must accumulate.

Writing prod[p1] = sum / 10 discards every earlier carry into that slot. Measured wrong on 14,219 of 20,000 random pairs — and it still returns a digit string, never an error. The asymmetry between the two lines looks like an inconsistency and isn't.

⭐ "Why is m + n the right array size?"

The product is below 10^m · 10^n = 10^(m+n), so at most m+n digits; and at least 10^(m−1) · 10^(n−1) = 10^(m+n−2), so at least m+n−1. Exactly one leading zero at most.

Verified on 20,000 random pairs: 16,148 results were m+n digits and 3,852 were m+n−1 — never any other length.

"Is a separate carry-normalisation pass needed at the end?"

No. A slot can temporarily exceed 9 after += sum / 10, but the next iteration to touch it reads it into sum and rewrites it as sum % 10. Going right-to-left in both loops guarantees each slot is fully accumulated before its final read.

Worth saying explicitly, because "the array is temporarily invalid" is an unusual invariant and an interviewer may well probe it.

"Can anything overflow?"

No. mul is at most 81, and prod[p2] stays below 10 after each normalisation, so sum is at most 90 — nowhere near int range. The accumulating slot prod[p1] is bounded by min(m,n) × 9 plus carries, which for 200-digit inputs is a few thousand.

That's worth checking rather than assuming: an unbounded accumulator is exactly where this pattern would break.

"Why the explicit zero guard if the stripper handles it?"

Belt and braces. The sb.length() == 0 check at the end already returns "0" for an all-zero array, but the up-front guard makes the intent obvious and turns "the stripper ate everything" from a silent path into a case I chose.

I verified both by making one in seven random test cases have a zero operand.

"Could you reverse the strings first and index from the left?"

Yes, and the arithmetic becomes i + j / i + j + 1 relative to a least-significant-first array, which some people find clearer. It costs two string reversals and a third at the end. Same complexity; purely a readability preference.

Approach 3 — Accumulate one row at a time

Java
public String multiply(String num1, String num2) {
    if (num1.equals("0") || num2.equals("0")) return "0";
    String result = "0";
    for (int j = num2.length() - 1; j >= 0; j--) {
        String partial = multiplyByDigit(num1, num2.charAt(j) - '0');
        partial = partial + "0".repeat(num2.length() - 1 - j);     // shift left
        result = addStrings(result, partial);
    }
    return result;
}
  • Time O(n · (m + n)) · Space O(m + n)

Counter-questions on this approach

⭐ "How does this compare to Approach 2?"

It's literally the schoolbook layout — one partial product per digit of the second operand, shifted and summed. Asymptotically the same O(m·n) for comparable lengths, but it allocates a string per row and does n full additions.

The value is explanatory: if the index arithmetic in Approach 2 feels like magic, this is the version that shows where it comes from.

"Why "0".repeat(...) rather than tracking an offset?"

Readability only. Tracking an offset inside addStrings avoids O(n) allocations. At 200 digits neither matters, and the explicit padding makes the shift visible.

"Would you write it?"

No — Approach 2 is shorter and has no string allocation in the inner loop. I'd describe this one if asked where i+j+1 comes from.

4. Why the Optimal Solution Wins

ApproachTimeSpaceVerdict
Repeated additionO(10^n)O(m+n)Impossible, and needs a forbidden type
Digit array, i+j / i+j+1O(m·n)O(m+n)No inner-loop allocation; no carry pass
Row-by-row partial productsO(n·(m+n))O(m+n)The clearest explanation; more allocation

O(m·n) is the schoolbook bound. Karatsuba does O(n^1.585) and FFT-based methods O(n log n), but both are slower in practice below a few hundred digits — which is exactly the constraint here.

Write Approach 2, and derive the index rule out loud from the place values. That derivation is the difference between knowing this problem and having memorised two expressions that are easy to transpose.

5. Java Prerequisites

Char-to-digit

Java
num1.charAt(i) - '0'          // 0..9, relies on ASCII digits being contiguous
(char) (d + '0')              // back

new int[m + n] is zero-filled, which is the correct starting state for an accumulator array.

StringBuilder for the output

Java
StringBuilder sb = new StringBuilder();
sb.append(v);                 // int overload — appends the decimal text, not a char
sb.length() == 0

sb.append((char) v) would append a control character — the int overload is what's wanted here.

Skipping leading zeros while building

Java
for (int v : prod)
    if (!(sb.length() == 0 && v == 0)) sb.append(v);
return sb.length() == 0 ? "0" : sb.toString();

The final ternary handles an all-zero array, which the loop would otherwise turn into "".

String.repeat (Java 11+)

Java
"0".repeat(k)

BigInteger is what you'd actually use — and is explicitly forbidden here, which is what makes it the right reference implementation for testing.

6. Interview Communication Guide

Clarifying questions: May I use BigInteger or Long.parseLong (no — that's the point of the problem)? Can the inputs have leading zeros (no, except "0" itself)? Are they always non-negative (yes)? How long can they be (200 digits, so O(m·n) is 40,000 operations — schoolbook is fine)?

The pitch

"I'll do schoolbook long multiplication on a digit array, and the two things worth deriving up front are the output size and where each partial product lands.

An m-digit number is below 10^m and an n-digit number below 10^n, so the product is below 10^(m+n) — at most m+n digits. It's also at least 10^(m+n−2), so at least m+n−1. So I allocate m+n slots and strip at most one leading zero.

For the positions: a[i] is worth 10^(m−1−i) and b[j] is worth 10^(n−1−j), so their product is worth 10^(m+n−2−i−j). In a left-indexed array of length m+n, that's index i+j+1. A two-digit product spills one place left, into i+j. I'd rather derive that than recall it — the two indices are trivially swappable and transposing them gives a wrong answer with no crash.

The subtle line is the carry: prod[p1] += sum / 10, accumulating rather than assigning, because many (i, j) pairs share the same i+j. Assignment there is wrong on about 70% of random inputs and still returns a plausible digit string. The digit slot is a plain assignment, because sum already read its previous contents — the asymmetry looks wrong and both halves are right.

And no final carry-normalisation pass is needed. A slot can temporarily exceed 9, but the next iteration to touch it reads it into sum and rewrites it as sum % 10. Going right-to-left in both loops means every slot is fully accumulated before its last read.

Nothing can overflow: a digit product is at most 81 and the slot is under 10, so sum is at most 90.

O(m·n) time, which at 200 digits is 40,000 operations. Karatsuba would be O(n^1.585) and FFT O(n log n), but both lose to schoolbook below a few hundred digits — so the constraint is telling me not to bother."

Edge cases to volunteer:

InputExpectedTests
"0" × "52""0"The stripper would otherwise return ""
"9" × "9""81"Single digits carrying — result is m+n long
"2" × "3""6"Single digits not carrying — result is m+n−1
"123" × "456""56088"The canonical trace
"999" × "999""998001"Maximum carrying at every position
200 nines × 200 nines400 digitsConstraint limit; long would have wrapped at 19

Name "0" × "52" and "2" × "3". The first is the only input where the output array is entirely zeros; the second is the smallest where the result is shorter than m + n, which is the case a fixed-length output would get wrong.

7. Follow-Up Questions — Modified Constraints

⭐ "Add two number strings instead."

Much simpler: walk both from the right with a shared carry, padding the shorter with zeros, then reverse. O(max(m,n)).

That's the helper Approach 3 needs, and it's Add Two Numbers in string form. The list version is easier because lists store least-significant-first.

⭐ "What if the numbers could be negative?"

Strip the signs, multiply the magnitudes, and apply the sign rule — negative iff exactly one operand is negative. The one case to handle is a zero result, which must be "0" and not "-0".

Sign handling stays outside the core loop, which is why it's a clean extension rather than a rewrite.

"Can you beat O(m·n)?"

Karatsuba splits each number in half and does three multiplications instead of four, giving O(n^1.585). Toom–Cook generalises it, and FFT-based multiplication reaches O(n log n).

All of them lose to schoolbook below a few hundred digits because of their constants — which is exactly the regime this problem sits in. Naming the crossover is more useful than naming the algorithms.

"What if the base weren't 10?"

Replace % 10 and / 10 with % base and / base, and the array size bound is unchanged — m + n digits in any base. Nothing in the position derivation mentions ten.

In practice big-integer libraries use base 2^32 for exactly this reason: fewer, larger limbs and the same algorithm.

"What if one number were fixed and you multiplied by it many times?"

Precompute its multiples 0 through 9 once, then each multiplication is n table lookups plus additions rather than m·n digit products. That's the classic optimisation for repeated multiplication by a constant.

"Implement division too?"

Long division — repeated subtract-and-shift, estimating each quotient digit. Considerably harder than multiplication because the estimation step needs care, and MIN_VALUE-style edge cases don't exist but zero-divisor and normalisation ones do.

"How would you test it?"

Differentially against BigInteger on random pairs across lengths 1 to 12, with one in seven having a zero operand — I used 20,000 cases. Plus a separate assertion that the output length is always m+n or m+n−1, which is a property check that needs no oracle and catches every allocation or stripping bug.