← All algorithms

Fast Exponentiation

Math · view source on GitHub

Read this in: English · Português · Español

Category: Math

The problem

Raising a number to an integer power. Multiplying the base by itself once per unit of the exponent — the direct reading of what "exponent" even means — is O(exponent). For a large exponent, that's a large number of multiplications for what is, mathematically, a much smaller amount of genuinely new information.

The solution

Exponentiation by squaring: base^n equals (base^2)^(n/2) whenever n is even, so squaring the base while halving the exponent reaches the same result — and that halving compounds every step, the same doubling-in-reverse shape Binary Search exploits on a sorted array. For an odd exponent, one factor of base is peeled off first (multiplied directly into the running result) so the remaining exponent is even again and the halving can continue. Reading the exponent's bits from least significant to most significant — squaring the base once per bit, and folding it into the result exactly when that bit is set — turns the whole computation into O(log exponent) multiplications instead of O(exponent).

flowchart LR
    A["2^10, binary 1010"] --> B["bit 0 (LSB) = 0 → skip; square base to 2^2=4"]
    B --> C["bit 1 = 1 → result *= 4; square base to 4^2=16"]
    C --> D["bit 2 = 0 → skip; square base to 16^2=256"]
    D --> E["bit 3 = 1 → result *= 256 → 4 * 256 = 1024"]

Classic example

classic/FastExponentiation implements power with the bit-by-bit squaring loop above, handling negative exponents as the reciprocal of the positive-exponent result, plus bruteForcePower (one multiplication per unit of exponent) included specifically for the benchmark below. FastExponentiationTest uses 2^10 = 1024 specifically because 10 in binary is 1010 — a mix of set and unset bits in one case — plus the zero-exponent identity, zero raised to a positive power, negative exponents, brute force agreeing with fast exponentiation, and the guard against the one genuinely undefined case (zero raised to a negative power).

Applied example: insurer actuarial reserve projection

applied/CompoundGrowthCalculator projects an actuarial reserve's accumulated value after many compounding periods — principal * (1 + periodicRate)^periods — the same growth-factor computation whether the periods are months in a reserve projection or years in a long-dated annuity schedule. CompoundGrowthCalculatorTest verifies a three-period projection against the compound-interest formula computed by hand (R$1,000 at 5% for 3 periods → R$1,157.625), the zero-period and zero-principal identities, and the guard rails (negative principal, a periodic rate at or below -100%, negative periods).

Benchmark

./gradlew :math:fast-exponentiation:jmh

Real run on this machine (JMH 1.37, JDK 26.0.2, 2 warmup + 3 measurement iterations, 1 fork):

Cost exponent=10,000 exponent=1,000,000 exponent=100,000,000
fast exponentiation 0.015 µs 0.021 µs 0.031 µs
brute force 16.52 µs 1,643.86 µs 164,002.01 µs

Brute force tracked its O(exponent) prediction almost exactly: a 100x increase in the exponent produced a 99.5x increase in cost (10,000 → 1,000,000), then a 99.8x increase on the next 100x step (1,000,000 → 100,000,000) — about as clean a linear confirmation as this repo has captured. Fast exponentiation barely moved (1.4x, then 1.48x) — matching the O(log n) prediction closely too: log2(1,000,000) / log2(10,000) ≈ 1.5, and log2(100,000,000) / log2(1,000,000) ≈ 1.33. At exponent=100,000,000, brute force is ~5,290,387x slower than fast exponentiation for the identical answer.

When not to use it

Test coverage

100% instruction coverage, 100% branch coverage (JaCoCo). Reproduce it yourself:

./gradlew :math:fast-exponentiation:jacocoTestReport

Report at math/fast-exponentiation/build/reports/jacoco/test/html/index.html.

Further reading

Unit tests

src/test/java/com/algorithms/math/fastexponentiation/classic/FastExponentiationTest.java
package com.algorithms.math.fastexponentiation.classic;

import org.assertj.core.data.Offset;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class FastExponentiationTest {

    private static final Offset<Double> TOLERANCE = Offset.offset(1e-9);

    @Test
    void raisesAnEvenAndOddMixOfExponentBitsCorrectly() {
        // 10 in binary is 1010 - exercises both the "bit set" and "bit unset" branches.
        assertThat(FastExponentiation.power(2, 10)).isCloseTo(1024.0, TOLERANCE);
    }

    @Test
    void anyBaseToTheZerothPowerIsOne() {
        assertThat(FastExponentiation.power(7, 0)).isCloseTo(1.0, TOLERANCE);
    }

    @Test
    void zeroToAPositivePowerIsZero() {
        assertThat(FastExponentiation.power(0, 3)).isCloseTo(0.0, TOLERANCE);
    }

    @Test
    void aNegativeExponentIsTheReciprocal() {
        assertThat(FastExponentiation.power(2, -2)).isCloseTo(0.25, TOLERANCE);
    }

    @Test
    void bruteForceAgreesWithFastExponentiation() {
        assertThat(FastExponentiation.bruteForcePower(2, 10))
                .isCloseTo(FastExponentiation.power(2, 10), TOLERANCE);
        assertThat(FastExponentiation.bruteForcePower(2, -2))
                .isCloseTo(FastExponentiation.power(2, -2), TOLERANCE);
    }

    @Test
    void rejectsZeroRaisedToANegativeExponent() {
        assertThatThrownBy(() -> FastExponentiation.power(0, -1)).isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> FastExponentiation.bruteForcePower(0, -1)).isInstanceOf(IllegalArgumentException.class);
    }
}
src/test/java/com/algorithms/math/fastexponentiation/applied/CompoundGrowthCalculatorTest.java
package com.algorithms.math.fastexponentiation.applied;

import org.assertj.core.data.Offset;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class CompoundGrowthCalculatorTest {

    private static final Offset<Double> TOLERANCE = Offset.offset(1e-9);

    private final CompoundGrowthCalculator calculator = new CompoundGrowthCalculator();

    @Test
    void projectsAReserveOverThreeCompoundingPeriods() {
        // R$1,000 at 5% per period for 3 periods: 1000 * 1.05^3 = 1157.625.
        assertThat(calculator.accumulatedValue(1_000, 0.05, 3)).isCloseTo(1157.625, TOLERANCE);
    }

    @Test
    void zeroPeriodsLeavesThePrincipalUnchanged() {
        assertThat(calculator.accumulatedValue(1_000, 0.05, 0)).isCloseTo(1000.0, TOLERANCE);
    }

    @Test
    void zeroPrincipalStaysZero() {
        assertThat(calculator.accumulatedValue(0, 0.05, 10)).isCloseTo(0.0, TOLERANCE);
    }

    @Test
    void rejectsANegativePrincipal() {
        assertThatThrownBy(() -> calculator.accumulatedValue(-1, 0.05, 3)).isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void rejectsAPeriodicRateOfMinusOneHundredPercentOrWorse() {
        assertThatThrownBy(() -> calculator.accumulatedValue(1_000, -1.0, 3)).isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> calculator.accumulatedValue(1_000, -1.5, 3)).isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void rejectsANegativePeriodCount() {
        assertThatThrownBy(() -> calculator.accumulatedValue(1_000, 0.05, -1)).isInstanceOf(IllegalArgumentException.class);
    }
}

View full JaCoCo coverage report →