← All algorithms

Bubble Sort

Sorting · view source on GitHub

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

Category: Sorting

The problem

Sorting a small batch of already-mostly-ordered data shouldn't need a general-purpose, guaranteed-O(n log n) algorithm's constant overhead — and a naive sort that always does the same amount of work regardless of how ordered the input already is wastes that opportunity. What actually varies from one call to the next isn't just the size of the input, it's how far the input already is from sorted.

The solution

Repeatedly walk the array comparing adjacent pairs and swapping the ones that are out of order — the largest not-yet-placed value "bubbles" to its correct position by the end of each pass. The one detail that makes this worth teaching at all: track whether a pass made any swap, and stop the moment a full pass makes zero of them. That single early-exit check is what makes the algorithm adaptive — O(n) on already-sorted input, and scaling with how much disorder is actually present rather than always paying the full O(n²), which is what a bubble sort without the early exit — or an equivalent like plain selection sort — pays unconditionally.

flowchart TB
    subgraph "before pass 1"
        direction LR
        A0["5"] --- A1["3"] --- A2["8"] --- A3["1"] --- A4["9"] --- A5["2"]
    end
    subgraph "after pass 1 — largest unsorted value bubbled to the end"
        direction LR
        B0["3"] --- B1["5"] --- B2["1"] --- B3["8"] --- B4["2"] --- B5["9"]
    end
Case Cost Why
Best (already sorted) O(n) a single pass makes zero swaps, the early-exit check stops immediately
Worst (reverse sorted) O(n²) every one of the n−1 passes makes a swap, none can exit early
Average / nearly sorted between O(n) and O(n²) cost tracks how far out of place elements actually are, not just n

Classic example

classic/BubbleSort is a generic, Comparator-driven implementation — no Arrays.sort/Collections.sort shortcut. Making it generic over Comparator<? super T> instead of hardcoding int[] is what lets the applied example below reuse this exact same sort method on a domain type instead of needing a second, parallel implementation. BubbleSortTest covers an unordered array, an already-sorted array, a reverse-sorted array (the two extremes the benchmark below measures), duplicates, a single-element and an empty array, a custom (descending) comparator, and both null-argument guards.

Applied example: legacy mainframe daily ledger correction

applied/DailyLedgerReorder models a real shape legacy mainframe batch jobs still run into: yesterday's ledger file closed already sorted by posting time, and now a single late correction entry needs to be spliced back into its correct position before the batch can be reprocessed. Appending the correction and re-running bubble sort over the whole (still almost entirely sorted) batch is a legitimate choice specifically because the disruption is small and localized — bubble sort's adaptivity means the real cost tracks how far out of place that one correction is, not the size of the whole batch. DailyLedgerReorderTest covers a correction that belongs in the middle, one that belongs at the very start, one that belongs at the very end (the trivial already-in-place case), and both null-argument guards.

Benchmark

./gradlew :sorting:bubble-sort:jmh

Real run on this machine (JMH 1.37, JDK 26.0.2, 2 warmup + 3 measurement iterations, 1 fork). Three input orderings at each size: already sorted, nearly sorted (a small number of adjacent-pair swaps scattered through the array — genuinely localized disorder, not just a handful of random long-range swaps, which can accidentally reproduce worst-case-like disruption even at a "small" swap count), and fully random:

Sort cost size=100 size=1,000 size=10,000
already sorted 0.099 µs 0.750 µs 10.363 µs
nearly sorted 0.232 µs 1.997 µs 40.054 µs
random 17.971 µs 2,198.934 µs 337,009.203 µs

At size=10,000, the random case is ~32,527x slower than the already-sorted case, and ~8,414x slower than the nearly-sorted case — on the exact same code, the only variable is how ordered the input already was. That's the adaptivity claim from "The solution" above, turned into a measured number instead of an assertion: this is precisely what Cormen, Leiserson, Rivest & Stein's CLRS states abstractly in Problem 2-2 ("Correctness of bubblesort") — O(n) best case, O(n²) worst case — made concrete on this machine. The random case's confidence interval at size=10,000 is wide (single-digit-millisecond-scale JVM/GC noise dominates an O(n²) workload at that size on a shared dev machine) — the ~1,000x-plus gap between orderings is the reliable signal here, not the last digit of any individual number.

When not to use it

Test coverage

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

./gradlew :sorting:bubble-sort:jacocoTestReport

Report at sorting/bubble-sort/build/reports/jacoco/test/html/index.html.

Further reading

Unit tests

src/test/java/com/algorithms/sorting/bubblesort/classic/BubbleSortTest.java
package com.algorithms.sorting.bubblesort.classic;

import org.junit.jupiter.api.Test;

import java.util.Comparator;

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

class BubbleSortTest {

    @Test
    void sortsAnUnorderedArrayIntoAscendingOrder() {
        Integer[] array = {5, 3, 8, 1, 9, 2};

        BubbleSort.sort(array, Comparator.naturalOrder());

        assertThat(array).containsExactly(1, 2, 3, 5, 8, 9);
    }

    @Test
    void alreadySortedArrayStaysSorted() {
        Integer[] array = {1, 2, 3, 4, 5};

        BubbleSort.sort(array, Comparator.naturalOrder());

        assertThat(array).containsExactly(1, 2, 3, 4, 5);
    }

    @Test
    void reverseSortedArrayEndsUpAscending() {
        Integer[] array = {5, 4, 3, 2, 1};

        BubbleSort.sort(array, Comparator.naturalOrder());

        assertThat(array).containsExactly(1, 2, 3, 4, 5);
    }

    @Test
    void duplicatesAreHandledCorrectly() {
        Integer[] array = {3, 1, 3, 2, 1};

        BubbleSort.sort(array, Comparator.naturalOrder());

        assertThat(array).containsExactly(1, 1, 2, 3, 3);
    }

    @Test
    void singleElementArrayStaysUnchanged() {
        Integer[] array = {42};

        BubbleSort.sort(array, Comparator.naturalOrder());

        assertThat(array).containsExactly(42);
    }

    @Test
    void emptyArrayStaysEmpty() {
        Integer[] array = {};

        BubbleSort.sort(array, Comparator.naturalOrder());

        assertThat(array).isEmpty();
    }

    @Test
    void sortsUsingACustomComparatorForDescendingOrder() {
        Integer[] array = {1, 5, 3, 2, 4};

        BubbleSort.sort(array, Comparator.reverseOrder());

        assertThat(array).containsExactly(5, 4, 3, 2, 1);
    }

    @Test
    void rejectsANullArray() {
        assertThatThrownBy(() -> BubbleSort.sort(null, Comparator.naturalOrder()))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void rejectsANullComparator() {
        assertThatThrownBy(() -> BubbleSort.sort(new Integer[] {1, 2}, null))
                .isInstanceOf(IllegalArgumentException.class);
    }
}
src/test/java/com/algorithms/sorting/bubblesort/applied/DailyLedgerReorderTest.java
package com.algorithms.sorting.bubblesort.applied;

import org.junit.jupiter.api.Test;

import java.time.Instant;
import java.time.temporal.ChronoUnit;

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

class DailyLedgerReorderTest {

    private static final Instant BASE = Instant.parse("2026-08-17T09:00:00Z");

    @Test
    void spliceALateCorrectionIntoTheMiddleOfTheBatch() {
        DailyLedgerReorder reorder = new DailyLedgerReorder();
        LedgerEntry[] batch = {
                entry("a", 0),
                entry("b", 1),
                entry("c", 3),
                entry("d", 4),
        };
        LedgerEntry correction = entry("late", 2);

        LedgerEntry[] result = reorder.reorderWithCorrection(batch, correction);

        assertThat(result).extracting(LedgerEntry::id).containsExactly("a", "b", "late", "c", "d");
    }

    @Test
    void correctionThatBelongsAtTheStartMovesToTheFront() {
        DailyLedgerReorder reorder = new DailyLedgerReorder();
        LedgerEntry[] batch = {entry("a", 1), entry("b", 2), entry("c", 3)};
        LedgerEntry correction = entry("earliest", 0);

        LedgerEntry[] result = reorder.reorderWithCorrection(batch, correction);

        assertThat(result).extracting(LedgerEntry::id).containsExactly("earliest", "a", "b", "c");
    }

    @Test
    void correctionThatBelongsAtTheEndStaysAppended() {
        DailyLedgerReorder reorder = new DailyLedgerReorder();
        LedgerEntry[] batch = {entry("a", 0), entry("b", 1)};
        LedgerEntry correction = entry("latest", 2);

        LedgerEntry[] result = reorder.reorderWithCorrection(batch, correction);

        assertThat(result).extracting(LedgerEntry::id).containsExactly("a", "b", "latest");
    }

    @Test
    void rejectsANullBatch() {
        DailyLedgerReorder reorder = new DailyLedgerReorder();

        assertThatThrownBy(() -> reorder.reorderWithCorrection(null, entry("x", 0)))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void rejectsANullCorrection() {
        DailyLedgerReorder reorder = new DailyLedgerReorder();

        assertThatThrownBy(() -> reorder.reorderWithCorrection(new LedgerEntry[0], null))
                .isInstanceOf(IllegalArgumentException.class);
    }

    private static LedgerEntry entry(String id, long offsetMinutes) {
        return new LedgerEntry(id, BASE.plus(offsetMinutes, ChronoUnit.MINUTES));
    }
}

View full JaCoCo coverage report →