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
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
prod[p1] = sum / 10; // WRONG — discards every earlier carry into this slot
prod[p1] += sum / 10; // rightis 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
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 · SpaceO(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:
num2can be a 200-digit number, so this is10^200additions. 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?"
intarithmetic on single digits. A digit times a digit is at most 81, plus a carry, so everything fits comfortably in anint— no overflow reasoning needed anywhere in the real solution.
Approach 2 — Long multiplication by hand (optimal)
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)· SpaceO(m + n)
Counter-questions on this approach
⭐ "Derive i + j and i + j + 1."
a[i]is worth10^(m−1−i)andb[j]is worth10^(n−1−j), so the product is worth10^(m+n−2−i−j). In a left-indexed array of lengthm+n, the slot for10^kis indexm+n−1−k, which givesi+j+1. The tens digit is worth ten times as much, so it goes one slot left, ati+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
sumalready readprod[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 / 10discards 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 mostm+ndigits; and at least10^(m−1) · 10^(n−1) = 10^(m+n−2), so at leastm+n−1. Exactly one leading zero at most.Verified on 20,000 random pairs: 16,148 results were
m+ndigits and 3,852 werem+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 intosumand rewrites it assum % 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.
mulis at most 81, andprod[p2]stays below 10 after each normalisation, sosumis at most 90 — nowhere nearintrange. The accumulating slotprod[p1]is bounded bymin(m,n) × 9plus 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() == 0check 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 + 1relative 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
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))· SpaceO(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 doesnfull 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
addStringsavoidsO(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+1comes from.
4. Why the Optimal Solution Wins
| Approach | Time | Space | Verdict |
|---|---|---|---|
| Repeated addition | O(10^n) | O(m+n) | Impossible, and needs a forbidden type |
Digit array, i+j / i+j+1 | O(m·n) | O(m+n) | No inner-loop allocation; no carry pass |
| Row-by-row partial products | O(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
num1.charAt(i) - '0' // 0..9, relies on ASCII digits being contiguous
(char) (d + '0') // backnew int[m + n] is zero-filled, which is the correct starting state for an accumulator array.
StringBuilder for the output
StringBuilder sb = new StringBuilder();
sb.append(v); // int overload — appends the decimal text, not a char
sb.length() == 0sb.append((char) v) would append a control character — the int overload is what's wanted here.
Skipping leading zeros while building
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+)
"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 below10^mand ann-digit number below10^n, so the product is below10^(m+n)— at mostm+ndigits. It's also at least10^(m+n−2), so at leastm+n−1. So I allocatem+nslots and strip at most one leading zero.For the positions:
a[i]is worth10^(m−1−i)andb[j]is worth10^(n−1−j), so their product is worth10^(m+n−2−i−j). In a left-indexed array of lengthm+n, that's indexi+j+1. A two-digit product spills one place left, intoi+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 samei+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, becausesumalready 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
sumand rewrites it assum % 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
sumis at most 90.
O(m·n)time, which at 200 digits is 40,000 operations. Karatsuba would beO(n^1.585)and FFTO(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:
| Input | Expected | Tests |
|---|---|---|
"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 nines | 400 digits | Constraint 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 reachesO(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
% 10and/ 10with% baseand/ base, and the array size bound is unchanged —m + ndigits in any base. Nothing in the position derivation mentions ten.In practice big-integer libraries use base
2^32for 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
ntable lookups plus additions rather thanm·ndigit 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
BigIntegeron 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 alwaysm+norm+n−1, which is a property check that needs no oracle and catches every allocation or stripping bug.