Brzo sortiranje (Quick Sort)

Lekcija 3 od 5 · Sortiranje

Brzo sortiranje bira jedan element – pivot – i preraspoređuje niz tako da levo od pivota budu manji, a desno veći elementi. Zatim isto radi, rekurzivno, za levi i desni deo. U proseku radi O(n log n) i u praksi je jedan od najbržih algoritama sortiranja.

Pre nego što počneš: potrebno predznanje – sortiranje objedinjavanjem, rekurzija.
Kako da učiš: svaki primer prekucaj i pokreni, a zadatke na kraju prvo reši sam/a; uputstvo i rešenje otvori tek posle toga. Svi programi su u C++-u.

Razdvajanje oko pivota

Za pivot uzimamo poslednji element dela niza a[l..d]. Promenljiva i pokazuje kraj dela sa elementima manjim od pivota. Prolazimo sa j od l do d − 1: kada je a[j] < pivot, proširujemo „mali“ deo (i++) i menjamo a[i] i a[j]. Na kraju pivot stavljamo odmah iza malog dela – na poziciju i + 1, koja je njegovo konačno mesto.

Razdvajanje niza 7 2 9 4 3 8 5 (podvučen je deo manji od pivota)
ja[j]AkcijaNiz posle koraka (pivot = 5)
početak, i = −17 2 9 4 3 8 5
077 > 5, ništa7 2 9 4 3 8 5
12i = 0, zamena a[0] i a[1]2 7 9 4 3 8 5
299 > 5, ništa2 7 9 4 3 8 5
34i = 1, zamena a[1] i a[3]2 4 9 7 3 8 5
43i = 2, zamena a[2] i a[4]2 4 3 7 9 8 5
588 > 5, ništa2 4 3 7 9 8 5
krajpivot na mesto i + 1 = 32 4 3 5 9 8 7

Posle razdvajanja broj 5 je na mestu na kome će biti i u sortiranom nizu. Ostaje da se sortiraju delovi 2 4 3 i 9 8 7 – istim postupkom.

Kompletan program

#include <iostream>
#include <vector>
using namespace std;

vector<int> a;

// razdvaja a[l..d] oko pivota a[d]; vraća konačnu poziciju pivota
int razdvoji(int l, int d) {
    int pivot = a[d];
    int i = l - 1;                    // a[l..i] su elementi manji od pivota
    for (int j = l; j < d; j++) {
        if (a[j] < pivot) {
            i++;
            swap(a[i], a[j]);
        }
    }
    swap(a[i + 1], a[d]);             // pivot ide iza manjih elemenata
    return i + 1;
}

void quickSort(int l, int d) {
    if (l >= d) return;               // 0 ili 1 element: već sortirano
    int p = razdvoji(l, d);
    quickSort(l, p - 1);              // levo od pivota
    quickSort(p + 1, d);              // desno od pivota
}

int main() {
    int n;
    cin >> n;
    a.resize(n);
    for (int &x : a) cin >> x;
    quickSort(0, n - 1);
    for (int x : a) cout << x << " ";
    cout << endl;
    return 0;
}

Za opadajući poredak u funkciji razdvoji uslov se menja u a[j] > pivot.

Video: brzo sortiranje

Postupak razdvajanja i rekurzivnih poziva prikazan je u videu.

Složenost i izbor pivota

Ako pivot svaki put podeli niz na dva približno jednaka dela, nivoa rekurzije ima oko log₂ n, a na svakom nivou se obradi n elemenata – ukupno O(n log n).

Najgori slučaj nastaje kada je pivot uvek najmanji ili najveći element – na primer kada je niz već sortiran, a za pivot uzimamo poslednji element. Tada se od n elemenata odvaja samo pivot, pa je složenost O(n²), a dubina rekurzije n.

Rešenje: pivot biramo slučajno. Pre razdvajanja zamenimo slučajno izabrani element sa poslednjim:

#include <cstdlib>
// na početku funkcije razdvoji(l, d):
int r = l + rand() % (d - l + 1);   // slučajna pozicija iz [l, d]
swap(a[r], a[d]);                   // ona postaje pivot
Izbor / umetanjeMerge sortQuick sortstd::sort
ProsečnoO(n²)O(n log n)O(n log n)O(n log n)
Najgori slučajO(n²)O(n log n)O(n²)O(n log n)
Dodatna memorijaneda, nne (samo rekurzija)ne
Stabilanumetanje dadanene (postoji stable_sort)

Funkcija sort iz C++ biblioteke koristi poboljšano brzo sortiranje koje se, ako rekurzija postane preduboka, prebacuje na drugi algoritam – zato nikada ne radi O(n²).

Primena: k-ti najmanji element

Posle razdvajanja pivot je na svojoj konačnoj poziciji p. Ako tražimo element koji bi u sortiranom nizu bio na poziciji k, dovoljno je nastaviti samo u jednom delu: levo ako je k < p, desno ako je k > p. Ovaj algoritam (quickselect) u proseku radi O(n). U C++ postoji gotova funkcija nth_element.

Zadaci za vežbu

Za svaki zadatak postoji primer, uputstvo i rešenje. Rešenja su provereno tačna na primerima iz teksta.

ZADATAK 01Težina: Srednje

K-ti najmanji element

Dato je n različitih celih brojeva i broj k (1 ≤ k ≤ n ≤ 105). Ispiši k-ti najmanji broj (za k = 1 to je najmanji).

Ulaz: u prvom redu n i k, u drugom n brojeva.

Primer 1

Ulaz

7 3
7 2 9 4 3 8 5

Izlaz

4

Primer 2

Ulaz

5 5
10 -3 8 1 6

Izlaz

10
ZADATAK 02Težina: Srednje

Otporno brzo sortiranje

Dato je n brojeva (1 ≤ n ≤ 105) koji su već sortirani opadajuće. Sortiraj ih rastuće brzim sortiranjem tako da program radi brzo i za najveće n.

Primer

Ulaz

5
9 7 5 3 1

Izlaz

1 3 5 7 9
Česte greške i saveti
  • Pivot se na kraju razdvajanja stavlja na poziciju i + 1, a rekurzija se poziva za [l, p − 1] i [p + 1, d] – pivot se više ne dira.
  • Bez slučajnog pivota sortiran ili obrnuto sortiran ulaz pravi O(n²) i može da prekorači stek.
  • Ako u nizu ima mnogo jednakih elemenata, i ova verzija je spora (svi jednaki odlaze na istu stranu). Na takmičenju zato koristi sort.
  • Brzo sortiranje nije stabilno.

Proveri znanje

Otvori kviz

Brzo sortiranje

1. Gde se nalazi pivot posle razdvajanja?




2. Kada brzo sortiranje sa pivotom na poslednjem mestu radi O(n²)?




3. Kako se izbegava najgori slučaj?




4. Koja funkcija iz C++ biblioteke nalazi k-ti element bez potpunog sortiranja?