Math Algorithms: Practice Problems with Solutions
Welcome to the practice problems for math algorithms. If you need the primitives fresh, the code templates have the Euclidean GCD, fast exponentiation, and the sieve in all 6 languages. Each problem below includes a hint, a visual walkthrough, and the full solution.
Recommended Study Order
The problems build from digit mechanics to full number theory. Reverse Integer and Palindrome Number teach the digit-by-digit loop that shows up in almost every pure-math question. Count Primes exercises the sieve, and Pow(x, n) exercises fast exponentiation. Factorial Trailing Zeroes and Water and Jug Problem are the insight problems: each one hides a short number-theory argument behind an ordinary-looking question. Super Pow, the Hard problem, combines modular exponentiation with an exponent that does not fit in any integer type, so it pulls together everything earlier in the list.
Easy Problems
1. Reverse Integer
LeetCode 7 | Difficulty: Easy
Brief: Given a 32-bit signed integer, return its digits reversed. Return 0 if the reversed value would fall outside the 32-bit range.
Why this pattern: Digit extraction with repeated modulo and division is the most basic math primitive. The overflow guard is what makes this a real interview question instead of a toy.
Hint: Check the overflow condition before you append the next digit, not after. By the time the value has grown past the limit, it is already corrupted.
Visual:
graph TD
A["x = 123"] --> B["digit = 3, x = 12"]
B --> C["rev = 3"]
C --> D["digit = 2, x = 1"]
D --> E["rev = 32"]
E --> F["digit = 1, x = 0"]
F --> G["rev = 321"]
Code:
function reverse(x) {
const INT_MAX = 2147483647;
const INT_MIN = -2147483648;
let rev = 0;
while (x !== 0) {
const digit = x % 10;
x = Math.trunc(x / 10);
// Check before appending: once rev grows past the
// 32-bit limit there is no way to recover
if (rev > Math.trunc(INT_MAX / 10) || (rev === Math.trunc(INT_MAX / 10) && digit > 7)) return 0;
if (rev < Math.trunc(INT_MIN / 10) || (rev === Math.trunc(INT_MIN / 10) && digit < -8)) return 0;
rev = rev * 10 + digit;
}
return rev;
}The overflow checks compare against INT_MAX / 10 before multiplying, and the digit clauses handle the exact boundary where the last digit pushes past the range. Python and Ruby do not overflow, so there you compute first and check the range at the end. Java, C++, Go, and JavaScript truncate negative division toward zero, which makes x % 10 and x / 10 agree on digit extraction, so no sign handling is needed there.
Time
2. Palindrome Number
LeetCode 9 | Difficulty: Easy
Brief: Determine whether an integer is a palindrome without converting it to a string.
Why this pattern: Reversing only half of the number with the same digit-extraction loop avoids rebuilding the whole integer, which could overflow on large inputs.
Hint: Stop the loop once the reversed half grows past the remaining half. The middle digit of an odd-length number takes care of itself.
Visual:
graph TD
A["x = 1221"] --> B["rev = 1, x = 122"]
B --> C["rev = 12, x = 12"]
C --> D["x <= rev, compare"]
D --> E["12 == 12, true"]
Code:
function isPalindrome(x) {
// Negative numbers and any positive number ending
// in 0 can never read the same backwards
if (x < 0 || (x % 10 === 0 && x !== 0)) {
return false;
}
let rev = 0;
while (x > rev) {
rev = rev * 10 + x % 10;
x = Math.floor(x / 10);
}
// Even length: x == rev. Odd length: the middle
// digit sits on top of rev, so drop it with /10.
return x === rev || x === Math.floor(rev / 10);
}The early return for negative numbers and numbers ending in 0 is required, because the half-reversal loop assumes the input still has digits left to compare. When the loop ends, x holds the first half and rev holds the reversed second half. Odd-length inputs leave the middle digit on top of rev, so comparing x == rev / 10 handles both lengths with one condition.
Time
Medium Problems
3. Count Primes
LeetCode 204 | Difficulty: Medium
Brief: Count the number of primes strictly less than n.
Why this pattern: Checking each number individually costs too much across a large range. The Sieve of Eratosthenes marks every composite once and counts the survivors in a single pass.
Hint: The sieve only needs to run up to sqrt(n). Every composite below n has a prime factor at or below sqrt(n).
Visual:
graph TD
A["n = 10"] --> B["Mark 4, 6, 8 with 2"]
B --> C["Mark 9 with 3"]
C --> D["Survivors: 2, 3, 5, 7"]
D --> E["Answer: 4"]
Code:
function countPrimes(n) {
if (n <= 2) return 0;
const isPrime = new Array(n).fill(true);
isPrime[0] = isPrime[1] = false;
// A composite below n always has a prime factor
// at or below sqrt(n), so the marking pass stops there
for (let i = 2; i * i < n; i++) {
if (isPrime[i]) {
// Start at i*i: smaller multiples were already
// marked by a smaller prime factor
for (let j = i * i; j < n; j += i) {
isPrime[j] = false;
}
}
}
return isPrime.filter(Boolean).length;
}The guard n <= 2 handles the empty cases so the array never gets indexed out of range. The boundary is “less than n”, so the array has size n and 1 is marked as not prime before the loop. The inner loop starts at i*i because smaller multiples already got marked by a smaller factor, which is what keeps the total work around N log log N instead of N sqrt(N).
Time
4. Pow(x, n)
LeetCode 50 | Difficulty: Medium
Brief: Implement pow(x, n) for a float base and an integer exponent, which may be negative.
Why this pattern: This is fast exponentiation directly. Multiplying x by itself n times is O(N). Squaring the base and halving the exponent is O(log N).
Hint: For a negative exponent, invert the base and negate the exponent once, then run the squaring loop.
Visual:
graph TD
A["2^10"] --> B["res = 1, base = 2, exp = 10"]
B --> C["exp even: base = 4, exp = 5"]
C --> D["exp odd: res = 4, base = 16, exp = 2"]
D --> E["exp even: base = 256, exp = 1"]
E --> F["exp odd: res = 1024, exp = 0"]
F --> G["return 1024"]
Code:
function myPow(x, n) {
// A negative exponent means the reciprocal,
// so invert the base and flip the exponent
if (n < 0) {
x = 1 / x;
n = -n;
}
let result = 1;
while (n > 0) {
// When the current bit of the exponent is set,
// the running base belongs in the result
if (n % 2 === 1) result *= x;
x *= x;
n = Math.floor(n / 2);
}
return result;
}The loop reads the exponent bit by bit. When a bit is set, the current power contributes to the result, and every iteration squares the base regardless. That doubling is why the loop runs about log2(n) times instead of n. Fixed-width languages copy the exponent to 64 bits before negating, because negating the 32-bit minimum value overflows and stays negative.
Time
5. Factorial Trailing Zeroes
LeetCode 172 | Difficulty: Medium
Brief: Return the number of trailing zeros in n!.
Why this pattern: This is a pure number-theory insight problem. A trailing zero needs a factor of 10, which needs a 2 and a 5. Twos are plentiful in a factorial, so the answer is the count of factors of 5.
Hint: Count the multiples of 5, then 25, then 125. Each successive power of 5 adds one more factor of 5 to the total.
Visual:
graph TD
A["n = 25"] --> B["Multiples of 5: 5, 10, 15, 20, 25"]
B --> C["Count: 5"]
A --> D["Multiples of 25: 25"]
D --> E["Count: 1"]
C --> F["Total: 6"]
E --> F
Code:
function trailingZeroes(n) {
let count = 0;
// Each power of 5 contributes one more factor of 5
// to the total, so keep dividing n by 5
while (n >= 5) {
n = Math.floor(n / 5);
count += n;
}
return count;
}The loop divides n by 5 and adds each quotient. For n = 25 that is 25/5 = 5 plus 25/25 = 1, for a total of 6, because 25 itself contributes two factors of 5. The repeated division is what catches every power of 5, and it runs about log5(n) times, which keeps even very large factorials in range without ever computing the factorial itself.
Time
6. Water and Jug Problem
LeetCode 365 | Difficulty: Medium
Brief: With jugs of capacity x and y, decide whether you can measure exactly target liters.
Why this pattern: Every amount you can measure is a linear combination of the two capacities, and the achievable amounts are exactly the multiples of gcd(x, y). Bezout’s identity is doing the work here, and the Euclidean GCD template is the whole solution.
Hint: Check that target is a multiple of gcd(x, y) and not larger than the sum of the two capacities.
Visual:
graph TD
A["x = 3, y = 5, target = 4"] --> B["gcd(3, 5) = 1"]
B --> C["4 is a multiple of 1"]
C --> D["4 <= 3 + 5"]
D --> E["Measurable: true"]
Code:
function canMeasureWater(x, y, target) {
if (x + y < target) return false;
// Both jugs empty can only measure zero
if (x + y === 0) return target === 0;
const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b));
// Every measurable amount is a multiple of the GCD
return target % gcd(x, y) === 0;
}Pouring between two jugs keeps the total amount constant except when a jug is emptied or filled, so the reachable amounts are exactly the multiples of the GCD up to the combined capacity. The sum check bounds the total, and the empty case needs its own guard so the modulo never divides by zero. Java and C++ widen the sum before comparing because the capacities can approach the 32-bit limit.
Time
Hard Problems
7. Super Pow
LeetCode 372 | Difficulty: Hard
Brief: Compute a^b mod 1337, where the exponent b is given as an array of decimal digits.
Why this pattern: The exponent does not fit in any integer type, so the squaring loop cannot take it directly. Processing digits left to right, each digit shifts the running result one decimal place left, and modular exponentiation keeps every value bounded.
Hint: After each digit, raise the running result to the 10th power and multiply in a^digit, all modulo 1337.
Visual:
graph TD
A["a = 2, b = [1, 0]"] --> B["digit 1: result = 2^1 = 2"]
B --> C["digit 0: result = 2^10 mod 1337"]
C --> D["2^10 = 1024"]
D --> E["Answer: 1024"]
Code:
function superPow(a, b) {
const MOD = 1337;
const modPow = (base, exp) => {
let result = 1;
base %= MOD;
while (exp > 0) {
if (exp % 2 === 1) result = result * base % MOD;
base = base * base % MOD;
exp = Math.floor(exp / 2);
}
return result;
};
let result = 1;
for (const digit of b) {
// Appending a digit multiplies the exponent by 10,
// so raise the running result to 10 and add a^digit
result = modPow(result, 10) * modPow(a, digit) % MOD;
}
return result;
}Reading digits left to right, appending digit d turns the exponent from e into 10e + d, so the result transforms from a^e into (a^e)^10 * a^d. Raising the running result to the 10th power uses the same squaring loop from the template, and taking the modulo at every multiply keeps the values small enough for any language. The exponent never exists as a number, which is the point of the problem.
Time
These seven problems cover the full range of math algorithms. Start with digit manipulation in Reverse Integer and Palindrome Number, move to the two workhorse primitives in Count Primes and Pow(x, n), then test your number-theory instincts on Factorial Trailing Zeroes and Water and Jug Problem. Super Pow is the capstone: it combines modular exponentiation with an exponent that only exists as digits, so it exercises everything above.