Sortowanie przez scalanie i quicksort w Javie

Zaktualizowano 22 października 2026

Sortowanie przez scalanie (Merge Sort) i sortowanie szybkie (Quicksort) sortują w Javie szybko nawet duże tablice, podczas gdy sortowanie bąbelkowe, przez wybór i przez wstawianie potrzebują O(n²) kroków. Przy 1000 elementów to około miliona porównań, przy milionie elementów już bilion. Metody z tego artykułu robią to samo w O(n log n), czyli przy milionie elementów w około 20 milionach kroków. Sztuczka, która za nimi stoi, nazywa się dziel i zwyciężaj.

Dziel i zwyciężaj: sortowanie przez scalanie

Dziel i zwyciężaj (divide and conquer) rozkłada problem na mniejsze podproblemy tego samego rodzaju, rozwiązuje je rekurencyjnie i składa rozwiązania w całość. Sortowanie przez scalanie robi to tak:

  1. Warunek zakończenia: Tablica z 0 lub 1 elementem jest już posortowana.
  2. Dziel: Podziel tablicę w środku (length / 2) na dwie połowy.
  3. Zwyciężaj: Posortuj obie połowy rekurencyjnie.
  4. Połącz: Scal (merge) dwie posortowane połowy.

Przy scalaniu dwa wskaźniki pokazują na początek obu połówek. Mniejszy element trafia do wyniku i tylko jego wskaźnik przesuwa się dalej. Gdy jedna połowa jest pusta, resztę drugiej dopisuje się na końcu.

Złożoność: Tablica jest dzielona na pół log₂ n razy, a na każdym poziomie każdy element jest łącznie scalany raz. Daje to O(n log n), zawsze, także w najgorszym przypadku. Cena: sortowanie przez scalanie potrzebuje O(n) dodatkowej pamięci na tablice pomocnicze.

import java.util.Arrays;
public class Main {    public static int[] mergeSort(int[] values) {        if (values.length <= 1) {            return values;        }        int mid = values.length / 2;        int[] left = mergeSort(Arrays.copyOfRange(values, 0, mid));        int[] right = mergeSort(Arrays.copyOfRange(values, mid, values.length));        return merge(left, right);    }
    public static int[] merge(int[] a, int[] b) {        int[] result = new int[a.length + b.length];        int i = 0;        int j = 0;        int k = 0;        while (i < a.length && j < b.length) {            if (a[i] <= b[j]) {                result[k++] = a[i++];            } else {                result[k++] = b[j++];            }        }        while (i < a.length) {            result[k++] = a[i++];        }        while (j < b.length) {            result[k++] = b[j++];        }        return result;    }
    public static void main(String[] args) {        int[] values = {38, 27, 43, 3, 9, 82, 10};        System.out.println(Arrays.toString(mergeSort(values)));        System.out.println(Arrays.toString(merge(new int[]{1, 4, 9}, new int[]{2, 3, 10})));    }}

Sortowanie szybkie (Quicksort)

Quicksort dzieli inaczej: wybiera element osiowy (pivot) i dzieli tablicę tak, że wszystko, co mniejsze, stoi po jego lewej stronie, a cała reszta po prawej. Potem pivot stoi na swoim ostatecznym miejscu p. Rekurencyjnie sortuje się już tylko obszary na lewo i na prawo od niego. Łączenie nie jest potrzebne, a quicksort działa w miejscu, bez tablicy pomocniczej.

W podziale Lomuto pivotem jest ostatni element a[high]. Indeks i oznacza, gdzie trafi następny mniejszy element. Drugi indeks j biegnie od low do high - 1. Jeśli a[j] < pivot, a[i] i a[j] zamieniają się miejscami, a i przesuwa się dalej. Na końcu pivot zamienia się z a[i].

Złożoność: Jeśli pivot dzieli przedział mniej więcej w środku, daje to, tak jak przy sortowaniu przez scalanie, O(n log n) i to jest przypadek średni. Jeśli jednak pivot jest zawsze najmniejszym lub największym elementem, przedział zmniejsza się tylko o jeden: O(n²) w najgorszym przypadku. Przy Lomuto dzieje się tak akurat dla tablicy już posortowanej. Dlatego w praktyce pivot wybiera się często losowo albo jako medianę z trzech elementów.

import java.util.Arrays;
public class Main {    public static void quickSort(int[] a, int low, int high) {        if (low >= high) {            return;        }        int p = partition(a, low, high);        quickSort(a, low, p - 1);        quickSort(a, p + 1, high);    }
    // Lomuto: the last element is the pivot    public static int partition(int[] a, int low, int high) {        int pivot = a[high];        int i = low;        for (int j = low; j < high; j++) {            if (a[j] < pivot) {                swap(a, i, j);                i++;            }        }        swap(a, i, high);        return i;    }
    public static void swap(int[] a, int i, int j) {        int tmp = a[i];        a[i] = a[j];        a[j] = tmp;    }
    public static void main(String[] args) {        int[] a = {7, 2, 9, 4, 5};        int p = partition(a, 0, a.length - 1);        System.out.println("pivot at " + p + ": " + Arrays.toString(a));
        int[] b = {38, 27, 43, 3, 9, 82, 10};        quickSort(b, 0, b.length - 1);        System.out.println(Arrays.toString(b));    }}

Sortowanie przez zliczanie (Counting Sort)

Algorytm oparty na porównaniach nigdy nie może być szybszy niż O(n log n). Sortowanie przez zliczanie omija tę granicę, bo w ogóle nie porównuje. Działa jednak tylko dla małych, nieujemnych liczb całkowitych, na przykład oczek na kostce, ocen albo punktów:

  1. Utwórz tablicę liczników count o długości max + 1.
  2. Dla każdej wartości v: count[v]++.
  3. Przejdź po count i zapisz każdą wartość v dokładnie count[v] razy do wyniku.

Złożoność: O(n + k), gdzie k to liczba możliwych wartości. Przy 100 rzutach kostką z 6 możliwymi wynikami to rozwiązanie nie do pobicia. Przy wartościach do miliarda tablica liczników potrzebowałaby jednak miliarda miejsc.

import java.util.Arrays;
public class Main {    public static int[] countingSort(int[] values, int max) {        int[] count = new int[max + 1];        for (int v : values) {            count[v]++;        }        int[] result = new int[values.length];        int k = 0;        for (int v = 0; v <= max; v++) {            for (int c = 0; c < count[v]; c++) {                result[k++] = v;            }        }        return result;    }
    public static void main(String[] args) {        int[] dice = {6, 1, 3, 6, 2, 1, 5, 6};        System.out.println(Arrays.toString(countingSort(dice, 6)));    }}

Stabilność i porównanie

Algorytm sortowania jest stabilny, jeśli elementy o równym kluczu zachowują swoją pierwotną kolejność. Liczy się to, gdy sortujesz według kilku kryteriów: najpierw alfabetycznie, potem stabilnie według oceny, a przy tej samej ocenie kolejność alfabetyczna zostaje zachowana.

Sortowanie przez scalanie jest stabilne, jeśli merge przy remisie bierze element z lewej połowy, czyli porównuje przez <=. Z < stabilność ginie. Quicksort nie jest stabilny, bo zamiany rozrzucają równe elementy daleko od siebie.

  • Sortowanie przez scalanie: zawsze O(n log n), O(n) dodatkowej pamięci, stabilne.
  • Quicksort: średnio O(n log n), w najgorszym przypadku O(n²), w miejscu, niestabilny.
  • Sortowanie przez zliczanie: O(n + k), O(k) dodatkowej pamięci na liczniki, tylko dla małych liczb całkowitych.

Java korzysta z obu: Arrays.sort dla int[] to potomek quicksorta, a dla obiektów i List.sort z kolekcji Javy to stabilny algorytm pokrewny sortowaniu przez scalanie.

import java.util.ArrayList;import java.util.List;
public class Main {    record Order(String id, int priority) {}
    public static List<Order> sortByPriority(List<Order> orders) {        if (orders.size() <= 1) {            return new ArrayList<>(orders);        }        int mid = orders.size() / 2;        List<Order> left = sortByPriority(orders.subList(0, mid));        List<Order> right = sortByPriority(orders.subList(mid, orders.size()));        List<Order> result = new ArrayList<>();        int i = 0;        int j = 0;        while (i < left.size() && j < right.size()) {            // <= keeps equal keys in their order: stable            if (left.get(i).priority() <= right.get(j).priority()) {                result.add(left.get(i++));            } else {                result.add(right.get(j++));            }        }        while (i < left.size()) {            result.add(left.get(i++));        }        while (j < right.size()) {            result.add(right.get(j++));        }        return result;    }
    public static void main(String[] args) {        List<Order> orders = List.of(            new Order("A", 2), new Order("B", 1), new Order("C", 2),            new Order("D", 1), new Order("E", 2));        for (Order o : sortByPriority(orders)) {            System.out.println(o.priority() + " " + o.id());        }    }}

Typowe błędy

  • Zapomniana reszta przy scalaniu

    Główna pętla merge kończy się, gdy tylko jedna połowa jest pusta. Jeśli potem brakuje pętli, które kopiują resztę drugiej połowy, na końcu wyniku zostają zera. Potrzebujesz obu: jednej dla reszty z lewej i jednej dla reszty z prawej.

  • Złe granice w rekurencji

    W quicksorcie pivot na pozycji p nie należy już do żadnej części. Jeśli wywołasz rekurencję z quickSort(a, low, p) zamiast p - 1, ten sam przedział może wracać w kółko, aż pojawi się StackOverflowError. W sortowaniu przez scalanie z granicami w tej samej tablicy (left..mid, mid+1..right) dzieje się to samo, jeśli mid występuje dwa razy albo wcale.

  • Stabilność stracona przez <

    W merge warunek if (a[i] < b[j]) wygląda niewinnie. Przy remisie bierze jednak najpierw element z prawej połowy, choć pierwotnie stał on dalej. Wynik jest nadal posortowany, ale już nie stabilny. Poprawnie jest <=.

Zadania z tego tematu

Przećwicz to, czego się nauczyłeś, od razu w przeglądarce. Testy natychmiast sprawdzają twoje rozwiązanie.

Powiązane artykuły