We cannot use BigInteger or directly convert to integers, so we must add digit by digit
(like we do on paper).
Steps (Algorithm):
1. Start from the last digit (units place) of both strings.
2. Add the two digits + carry.
3. Store the result digit (sum % 10) in a StringBuilder.
4. Carry forward (sum / 10) to the next step.
5. Continue until both strings are fully traversed and no carry is left.
6. Reverse the StringBuilder (since digits were added from right to left).
7. Return the final string.
public static String addStrings(String num1, String num2) {
StringBuilder result = new StringBuilder();
int i = num1.length() - 1; // pointer for num1
int j = num2.length() - 1; // pointer for num2
int carry = 0;
while (i >= 0 || j >= 0 || carry != 0) {
int digit1 = (i >= 0) ? num1.charAt(i) - '0' : 0;
int digit2 = (j >= 0) ? num2.charAt(j) - '0' : 0;
int sum = digit1 + digit2 + carry;
result.append(sum % 10); // store unit digit
carry = sum / 10; // update carry
i--;
j--;
}
return result.reverse().toString();
}
Time Complexity Analysis
● Each digit of both numbers is processed once.
● Suppose m = length(num1), n = length(num2).
● The loop runs at most max(m, n) times.
● Each iteration does O(1) operations.
● Reversing the StringBuilder also takes O(max(m, n)).
✅ Time Complexity: O(max(m, n))
✅ Space Complexity: O(max(m, n)) (to store result string).
MULTIPLICATION :
public static String multiply(String num1, String num2) {
if (num1.equals("0") || num2.equals("0")) return "0";
int m = num1.length();
int n = num2.length();
int[] product = new int[m + n]; // maximum digits
// Multiply each digit
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 sum = mul + product[i + j + 1];
product[i + j] += sum / 10; // carry
product[i + j + 1] = sum % 10; // store remainder
}
}
// Build result string
StringBuilder result = new StringBuilder();
for (int num : product) {
if (!(result.length() == 0 && num == 0)) { // skip leading zeros
result.append(num);
}
}
return result.toString();
✅
Time Complexity = O(m × n)
Space Complexity = O(m + n) (for product array, final result string).
Modular Binary Exponentiation
Modular Binary Exponentiation (a.k.a. Fast Exponentiation or Exponentiation by Squaring)
is a method to compute
efficiently in O(log n) time by repeatedly squaring.
Recursive Version (Java)
public static long powerRecursive(long a, long n, long m) {
if (n == 0) return 1 % m;
long half = powerRecursive(a, n / 2, m);
long result = (half * half) % m;
if (n % 2 == 1) {
result = (result * a) % m;
}
return result;
}
Iterative version (java)
public static long powerIterative(long a, long n, long m) {
long result = 1;
a = a % m; // reduce base first
while (n > 0) {
if ((n & 1) == 1) { // if n is odd
result = (result * a) % m;
}
a = (a * a) % m; // square the base
n >>= 1; // divide n by 2
}
return result;
}
Modular Fast Multiplication:
It is a technique to compute efficiently without
causing overflow, by breaking multiplication into smaller steps
using divide-and-conquer.
Recursive Version
public static long modMultiplyRecursive(long a, long b, long m) {
if (b == 0) return 0;
long half = modMultiplyRecursive(a, b / 2, m);
long result = (half + half) % m;
if (b % 2 == 1) {
result = (result + a) % m;
}
return result;
}
Iterative Version (Java)
public static long modMultiplyIterative(long a, long b, long m) {
long result = 0;
a = a % m;
while (b > 0) {
if ((b & 1) == 1) { // if b is odd
result = (result + a) % m;
}
a = (2 * a) % m;
b >>= 1; // divide b by 2
}
return result;
}
Tribonacci series:
The Tribonacci series is defined as:
Transformation Matrix T
MATRIX EXPONENTION
static long[][] multiply(long[][] A, long[][] B) {
long[][] C = new long[2][2];
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
for (int k = 0; k < 2; k++) {
C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % MOD;
}
}
}
return C;
}
// Fast matrix exponentiation
static long[][] power(long[][] M, int exp) {
long[][] result = { {1, 0}, {0, 1} }; // Identity matrix
while (exp > 0) {
if ((exp & 1) == 1) result = multiply(result, M);
M = multiply(M, M);
exp >>= 1;
}
return result;
}
// Compute nth Fibonacci
static long fibonacci(int n) {
if (n == 0) return 0;
long[][] F = { {1, 1}, {1, 0} };
long[][] Fn = power(F, n - 1);
return Fn[0][0]; // F(n)
}
// Compute sum of Fibonacci numbers up to F(n)
public static long fibonacciSum(int n) {
if (n < 0) return 0;
return (fibonacci(n + 2) - 1 + MOD) % MOD;
}
floating-point errors
Floating-point numbers (IEEE-754 standard) represent real numbers
with finite binary precision. This leads to errors programmers often
overlook. Here are four common pitfalls, each with explanation and
example:
1. Precision/Representation Error:
Computers store floating-point numbers in binary (base-2). Some
decimal numbers cannot be represented exactly in binary.
Why? 0.1 and 0.2 cannot be represented exactly in binary
floating-point, so small rounding errors appear.
2) Rounding Error
● During calculations, results are rounded to the nearest
representable number
Why? 1/3 is an infinite decimal and must be rounded to fit finite bits.
3) Loss of Significance (Cancellation Error)
● When subtracting two very close numbers, most significant digits
cancel out, leaving only rounding noise.
● Why? Subtraction amplifies tiny floating-point representation
errors.
4) Overflow and Underflow
● Overflow: When a number is too large to represent → becomes
Infinity.
● Underflow: When a number is too small (close to 0) → becomes
0.0.
● Example:
Given an array of integers arr[] of size n and an integer k,
count the number of non-empty subarrays whose sum is
divisible by k.Formally, count pairs (i, j) such that:
Java Implementation
public class SubarrayDivisibleByK {
public static int countSubarraysDivByK(int[] nums, int k) {
Map<Integer, Integer> count = new HashMap<>();
count.put(0, 1); // remainder 0 occurs once initially
int prefix = 0, result = 0;
for (int num : nums) {
prefix += num;
int remainder = prefix % k;
if (remainder < 0) remainder += k; // handle negative values
if (count.containsKey(remainder)) {
result += count.get(remainder);
}
count.put(remainder, count.getOrDefault(remainder, 0) + 1);
}
return result;
}
Implement a Python or Java method to compute the factorial of a
given number n, where the result may exceed the range of standard
integer types
Sols)
Store the result as an array of digits (least significant digit at index
0).
i. Initially, factorial result = 1.
ii. For every i = 2 to n, multiply result by i digit
by digit.
iii. Carry over digits as in manual multiplication
iv. After finishing, print digits in reverse order.
for (int i = 0; i < resSize; i++) {
int prod = res[i] * x + carry;
res[i] = prod % 10; // store last digit
carry = prod / 10; // carry forward
}
// store carry digits
while (carry > 0) {
res[resSize] = carry % 10;
carry /= 10;
resSize++;
}static void factorial(int n) {
int[] res = new int[10000]; // array to hold large result
res[0] = 1;
int resSize = 1; // number of digits in result
for (int x = 2; x <= n; x++) {
resSize = multiply(x, res, resSize);
}
// print result in reverse order
for (int i = resSize - 1; i >= 0; i--) {
System.out.print(res[i]);
}
Implement a method in Java or Python to determine the last
non-zero digit of a given factorial n!.
// Function to find last non-zero digit of n!
static int lastNonZeroDigit(int n) {
if (n == 0) return 1;
// Recurrence relation:
// D(n) = D(n/5) * Dsmall(n%5) * 2^(n/5) (mod 10)
int[] small = {1, 1, 2, 6, 4}; // last non-zero digits of
0!,1!,2!,3!,4!
int quotient = n / 5;
int remainder = n % 5;
int result = lastNonZeroDigit(quotient); // recurse on n/5
result = (result * small[remainder]) % 10; // multiply last
digits of remainder factorial
// Adjust for powers of 2 contributed by quotient (since 2s >
5s)
if (quotient % 2 != 0) {
result = (result * 4) % 10; // 2^(odd) cycles affect result
}
return result;
}
Each step does O(1) work.
Time Complexity: O(log n)
Space Complexity: O(log n) (recursive stack).
(a) Counting Trailing Zeros in n!
public class TrailingZeros {
public static int countTrailingZeros(int n) {
int count = 0;
while (n >= 5) {
n /= 5;
count += n;
}
return count;
}
Time Complexity: O(logn)O(\log n)O(logn)
Space Complexity: O(1)O(1)O(1) (just uses a counter).
b) finding duplicates
int slow = nums[0];
int fast = nums[0];
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while (slow != fast);
// Phase 2: Find entrance of cycle
slow = nums[0];
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
c) fining majority:
public static int majorityElement(int[] nums) {
int count = 0, candidate = 0;
for (int num : nums) {
if (count == 0) {
candidate = num;
}
count += (num == candidate) ? 1 : -1;
}
return candidate;
}
Binary exponentiation :
def binary_exponentiation(a, n):
result = 1
base = a
while n > 0:
if n % 2 == 1: # if n is odd
result *= base
base *= base # square the base
n //= 2 # halve the exponent
return result
We have an array A of size n, with each element a 64-bit integer.
Adjacent elements differ in exactly one bit (like a Gray code path).
We need to check if there exist four distinct indices (a, b, c, d)
such that:
public static boolean hasFourXORZero(long[] A) {
int n = A.length;
Map<Long, Pair> map = new HashMap<>();
// Check all pairs
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
long xorVal = A[i] ^ A[j];
if (map.containsKey(xorVal)) {
Pair prev = map.get(xorVal);
// Ensure distinct indices
if (prev.first != i && prev.second != j &&
prev.first != j && prev.second != i) {
return true; // Found 4 numbers
}
} else {
map.put(xorVal, new Pair(i, j));
}
}
}
return false;
}