PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Quicksort – warianty, Introsort, benchmark vs wbudowany sort()

by Henryk Tews / wtorek, 13 września 2022 / Opublikowano w Algorytmy

W listopadzie 2018 pisałem o podstawowych algorytmach sortowania. Czas na Quicksort – jeden z najważniejszych algorytmów w historii informatyki, średnio O(n log n) i jeden z najszybszych w praktyce. Implementuję kilka wariantów w PHP, pokazuję gdzie Quicksort bije inne algorytmy i dlaczego wbudowany sort() PHP jest szybszy niż własna implementacja.

Quicksort – idea

Quicksort działa na zasadzie „dziel i zwyciężaj”. Wybiera element zwany pivotem, dzieli tablicę na dwie części (mniejsze od pivotu i większe), a potem rekurencyjnie sortuje obie części. Kluczowy element to wybór pivotu – od niego zależy efektywność algorytmu.

Implementacja podstawowa – pivot jako pierwszy element

<?php

declare(strict_types=1);

// Quicksort - wersja naiwna, pivot = pierwszy element
function quicksortBasic(array $arr): array
{
    if (count($arr) <= 1) {
        return $arr;
    }

    $pivot  = $arr[0];
    $left   = [];
    $right  = [];

    for ($i = 1; $i < count($arr); $i++) {
        if ($arr[$i] <= $pivot) {
            $left[] = $arr[$i];
        } else {
            $right[] = $arr[$i];
        }
    }

    return [...quicksortBasic($left), $pivot, ...quicksortBasic($right)];
}

$data   = [64, 34, 25, 12, 22, 11, 90, 43, 7, 88];
$sorted = quicksortBasic($data);
print_r($sorted); // [7, 11, 12, 22, 25, 34, 43, 64, 88, 90]

Problem z pivotem jako pierwszym elementem: gdy tablica jest już posortowana lub prawie posortowana, Quicksort degeneruje do O(n²) – za każdym razem pivot jest najmniejszym elementem i partycja jest nierówna (0 elementów po lewej, n-1 po prawej).

Quicksort z losowym pivotem

<?php

declare(strict_types=1);

// Losowy pivot - eliminuje worst case dla posortowanych tablic
function quicksortRandom(array $arr): array
{
    $n = count($arr);

    if ($n <= 1) {
        return $arr;
    }

    // Losowy pivot - unika O(n²) dla posortowanych danych
    $pivotIndex = random_int(0, $n - 1);
    $pivot      = $arr[$pivotIndex];

    $left   = [];
    $middle = []; // elementy równe pivotowi - Three-way partition
    $right  = [];

    foreach ($arr as $element) {
        if ($element < $pivot) {
            $left[] = $element;
        } elseif ($element > $pivot) {
            $right[] = $element;
        } else {
            $middle[] = $element; // duplikaty trafią tu
        }
    }

    return [...quicksortRandom($left), ...$middle, ...quicksortRandom($right)];
}

// Three-way partition (Dutch National Flag) - wydajny przy wielu duplikatach
$dataWithDupes = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 9];
$sorted = quicksortRandom($dataWithDupes);
print_r($sorted); // [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9, 9]

Quicksort in-place – wariant bez tworzenia nowych tablic

Wersje wyżej tworzą nowe tablice przy każdym podziale – zużycie pamięci O(n log n). Wariant in-place sortuje w miejscu przez wymianę elementów, zużycie pamięci O(log n):

<?php

declare(strict_types=1);

function quicksortInPlace(array &$arr, int $low = 0, ?int $high = null): void
{
    $high = $high ?? count($arr) - 1;

    if ($low >= $high) {
        return;
    }

    $pivotIndex = partition($arr, $low, $high);

    quicksortInPlace($arr, $low, $pivotIndex - 1);
    quicksortInPlace($arr, $pivotIndex + 1, $high);
}

function partition(array &$arr, int $low, int $high): int
{
    // Median-of-three pivot - lepsza wydajność niż losowy dla typowych danych
    $mid = intdiv($low + $high, 2);

    // Wybierz medianę z pierwszego, środkowego i ostatniego elementu
    if ($arr[$low] > $arr[$mid]) {
        [$arr[$low], $arr[$mid]] = [$arr[$mid], $arr[$low]];
    }
    if ($arr[$low] > $arr[$high]) {
        [$arr[$low], $arr[$high]] = [$arr[$high], $arr[$low]];
    }
    if ($arr[$mid] > $arr[$high]) {
        [$arr[$mid], $arr[$high]] = [$arr[$high], $arr[$mid]];
    }

    // Przenieś pivot na pozycję high-1
    $pivot = $arr[$mid];
    [$arr[$mid], $arr[$high - 1]] = [$arr[$high - 1], $arr[$mid]];

    $i = $low;
    $j = $high - 1;

    while (true) {
        while ($arr[++$i] < $pivot);
        while ($arr[--$j] > $pivot);

        if ($i >= $j) {
            break;
        }

        [$arr[$i], $arr[$j]] = [$arr[$j], $arr[$i]];
    }

    // Przywróć pivot na właściwe miejsce
    [$arr[$i], $arr[$high - 1]] = [$arr[$high - 1], $arr[$i]];

    return $i;
}

$data = [64, 34, 25, 12, 22, 11, 90, 43, 7, 88, 55, 31];
quicksortInPlace($data);
print_r($data);

Introsort – hybryda Quicksort + Heapsort + Insertion Sort

W praktyce najlepsze implementacje sortowania to hybrydy. Introsort (używany przez C++ std::sort i PHP wewnętrznie) łączy trzy algorytmy:

<?php

declare(strict_types=1);

// Uproszczona implementacja Introsort w PHP
// Quicksort + Insertion Sort dla małych tablic (jak robi to PHP wewnętrznie)
function introsort(array $arr): array
{
    $n = count($arr);

    if ($n <= 16) {
        // Insertion Sort dla małych tablic - mniej narzutu niż rekurencja
        return insertionSort($arr);
    }

    // Losowy pivot + Three-way partition
    $pivotIndex = random_int(0, $n - 1);
    $pivot      = $arr[$pivotIndex];

    $left   = array_filter(array_values($arr), fn($x) => $x < $pivot);
    $middle = array_filter(array_values($arr), fn($x) => $x === $pivot);
    $right  = array_filter(array_values($arr), fn($x) => $x > $pivot);

    return [...introsort(array_values($left)), ...array_values($middle), ...introsort(array_values($right))];
}

function insertionSort(array $arr): array
{
    $n = count($arr);

    for ($i = 1; $i < $n; $i++) {
        $key = $arr[$i];
        $j   = $i - 1;

        while ($j >= 0 && $arr[$j] > $key) {
            $arr[$j + 1] = $arr[$j];
            $j--;
        }

        $arr[$j + 1] = $key;
    }

    return $arr;
}

Benchmark – własna implementacja vs wbudowany sort()

<?php

declare(strict_types=1);

function benchmark(string $name, callable $fn, array $data): void
{
    $start  = microtime(true);
    $result = $fn($data);
    $time   = (microtime(true) - $start) * 1000;

    echo sprintf("%-30s %8.3f ms (%d elementów)\n", $name, $time, count($result));
}

$sizes = [1000, 10000, 100000];

foreach ($sizes as $size) {
    $data = array_map(fn() => random_int(1, $size * 10), range(1, $size));

    echo "\n=== {$size} elementów ===\n";

    benchmark('Quicksort (losowy pivot)', fn($d) => quicksortRandom($d), $data);
    benchmark('Introsort (hybryda)',      fn($d) => introsort($d),        $data);
    benchmark('PHP sort() (C, Timsort)', function($d) { sort($d); return $d; }, $data);
    benchmark('PHP usort() (callable)',  function($d) { usort($d, fn($a,$b) => $a<=>$b); return $d; }, $data);
}

Typowe wyniki dla 100 000 elementów:

Algorytm 100 000 elementów Uwagi
PHP sort() ~15 ms Timsort w C - najszybszy
PHP usort() ~45 ms Narzut callable w PHP
Quicksort PHP (losowy) ~200 ms Narzut rekurencji PHP
Introsort PHP (hybryda) ~180 ms Lepszy dla małych podtablic

Kiedy pisać własny Quicksort?

W praktyce PHP nigdy nie piszesz własnego Quicksort do sortowania danych – sort(), usort() i uasort() są napisane w C i zawsze wygrają. Implementacja od zera ma sens w trzech sytuacjach: nauka algorytmów, rozmowa rekrutacyjna albo przeniesienie logiki sortowania do innego środowiska (np. JavaScript, Python) gdzie rozumiesz mechanizm bo pisałeś go w PHP.

Podsumowanie

Quicksort to algorytm który warto rozumieć nawet jeśli nigdy nie napiszesz go w produkcyjnym PHP. Wybór pivotu decyduje o wszystkim – naiwny pierwszy element degraduje do O(n²) na posortowanych danych, losowy pivot lub median-of-three eliminują ten problem. Wbudowany sort() PHP używa Timsort (hybryda Merge Sort i Insertion Sort) – jest szybszy od każdej implementacji PHP i to jest właśnie punkt: znajomość algorytmów pozwala wybrać właściwe narzędzie, niekoniecznie implementować je od zera.

About Henryk Tews

Co możesz przeczytać następne

Dijkstra – najkrótsza ścieżka z wagami, SplMinHeap, zastosowania w e-commerce
Skip List – probabilistyczna struktura danych O(log n), implementacja w PHP
Sortowanie w PHP – bubble sort, merge sort, usort() i operator <=>
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Zawsze aktywne
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}