Sortiranje objedinjavanjem (Merge Sort)

Lekcija 2 od 5 · Sortiranje

Sortiranje objedinjavanjem deli niz na dve polovine, sortira svaku polovinu (rekurzivno) i zatim ih spaja u jedan sortiran niz. Radi u vremenu O(n log n) i za milion brojeva završava posao za delić sekunde.

Pre nego što počneš: potrebno predznanje – jednostavni algoritmi sortiranja, uvod u rekurziju.
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.

Prvi korak: spajanje dva sortirana niza

Ključni deo algoritma je spajanje (objedinjavanje) dva već sortirana niza. Gledamo prve elemente oba niza, manji od njih prepisujemo u rezultat i pomeramo se dalje u nizu iz koga je uzet. Kada se jedan niz potroši, prepišemo ostatak drugog.

Spajanje nizova {3, 5, 10} i {-34, 16}
ABUzimamoRezultat
3 5 10-34 16-34 (iz B)-34
3 5 10163 (iz A)-34 3
5 10165 (iz A)-34 3 5
101610 (iz A)-34 3 5 10
–16ostatak B-34 3 5 10 16
#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n, m;
    cin >> n;
    vector<int> a(n);
    for (int &x : a) cin >> x;
    cin >> m;
    vector<int> b(m);
    for (int &x : b) cin >> x;

    vector<int> c;
    int i = 0, j = 0;
    while (i < n && j < m) {          // dok oba niza imaju elemente
        if (a[i] <= b[j]) c.push_back(a[i++]);
        else c.push_back(b[j++]);
    }
    while (i < n) c.push_back(a[i++]); // ostatak niza a
    while (j < m) c.push_back(b[j++]); // ostatak niza b

    for (int x : c) cout << x << " ";
    cout << endl;
    return 0;
}

Svaki element se prepisuje tačno jednom, pa spajanje radi u vremenu O(n + m).

Algoritam: podeli, sortiraj, spoji

  1. Ako niz ima jedan element, već je sortiran (bazni slučaj rekurzije).
  2. Podeli niz na levu polovinu a[l..m] i desnu a[m+1..d], gde je m = (l + d) / 2.
  3. Rekurzivno sortiraj levu i desnu polovinu.
  4. Spoji dve sortirane polovine.
[38 27 43 3 9 82 10]
[38 27 43 3]        [9 82 10]          deljenje
[38 27] [43 3]      [9 82] [10]
[38][27][43][3]     [9][82][10]
[27 38] [3 43]      [9 82] [10]        spajanje
[3 27 38 43]        [9 10 82]
[3 9 10 27 38 43 82]
#include <iostream>
#include <vector>
using namespace std;

vector<int> a, pom;   // pom je pomoćni niz za spajanje

void mergeSort(int l, int d) {
    if (l >= d) return;               // jedan element: već sortiran
    int m = (l + d) / 2;
    mergeSort(l, m);                  // sortiraj levu polovinu
    mergeSort(m + 1, d);              // sortiraj desnu polovinu

    // spajanje a[l..m] i a[m+1..d] u pom[l..d]
    int i = l, j = m + 1, k = l;
    while (i <= m && j <= d) {
        if (a[i] <= a[j]) pom[k++] = a[i++];
        else pom[k++] = a[j++];
    }
    while (i <= m) pom[k++] = a[i++];
    while (j <= d) pom[k++] = a[j++];
    for (k = l; k <= d; k++) a[k] = pom[k];   // vrati spojeni deo u a
}

int main() {
    int n;
    cin >> n;
    a.resize(n);
    pom.resize(n);
    for (int &x : a) cin >> x;

    mergeSort(0, n - 1);

    for (int x : a) cout << x << " ";
    cout << endl;
    return 0;
}
Zašto a[i] <= a[j], a ne <? Kada su elementi jednaki, prvo uzimamo onaj iz leve polovine. Tako jednaki elementi zadržavaju redosled i algoritam je stabilan.

Video: sortiranje objedinjavanjem

U videu je postupak prikazan korak po korak na primeru.

Zašto je O(n log n)?

Niz se deli na pola sve dok delovi ne postanu dužine 1. Od n elemenata do 1 stiže se posle oko log₂ n deljenja – to je broj nivoa. Na svakom nivou spajanja se ukupno prepiše svih n elemenata. Zato je ukupan rad oko n · log₂ n.

nn² (izbor, mehurić)n · log₂ n (merge sort)
1 0001 000 000oko 10 000
100 0001010oko 1 700 000
1 000 0001012oko 20 000 000

Merge sort uvek radi O(n log n), bez obzira na raspored elemenata, ali troši dodatnu memoriju za pomoćni niz veličine n.

Primena: brojanje inverzija

Inverzija je par pozicija i < j za koji je a[i] > a[j]. Broj inverzija pokazuje koliko je niz „daleko“ od sortiranog (i jednak je broju zamena koje napravi metoda mehurića). Prebrojavanje svih parova traje O(n²), ali merge sort to radi usput:

kada pri spajanju uzimamo element iz desne polovine (a[j] < a[i]), on je manji od svih preostalih elemenata leve polovine, a[i..m]. Svaki od njih čini inverziju sa njim, pa broju inverzija dodajemo m − i + 1.

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

vector<int> a, pom;

long long brojInverzija(int l, int d) {
    if (l >= d) return 0;
    int m = (l + d) / 2;
    long long br = brojInverzija(l, m) + brojInverzija(m + 1, d);
    int i = l, j = m + 1, k = l;
    while (i <= m && j <= d) {
        if (a[i] <= a[j]) pom[k++] = a[i++];
        else {
            br += m - i + 1;          // a[j] je manji od svih a[i..m]
            pom[k++] = a[j++];
        }
    }
    while (i <= m) pom[k++] = a[i++];
    while (j <= d) pom[k++] = a[j++];
    for (k = l; k <= d; k++) a[k] = pom[k];
    return br;
}

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

Rezultat može biti do n(n−1)/2, pa za n = 105 koristimo tip long long.

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: Lako

Unija dva sortirana niza

Data su dva niza sortirana rastuće (bez ponavljanja unutar istog niza). Ispiši sve brojeve koji se javljaju bar u jednom nizu, rastuće i bez ponavljanja.

Ulaz: n, pa n brojeva; zatim m, pa m brojeva (1 ≤ n, m ≤ 105).

Primer

Ulaz

4
1 3 5 7
3
3 4 7

Izlaz

1 3 4 5 7
ZADATAK 02Težina: Srednje

Sortiranje velikog niza

Dato je n celih brojeva (1 ≤ n ≤ 106). Ispiši ih sortirane opadajuće. Algoritam O(n²) je ovde prespor.

Primer

Ulaz

6
4 -1 7 4 0 10

Izlaz

10 7 4 4 0 -1
Česte greške i saveti
  • Bazni slučaj je l >= d; bez njega rekurzija nikada ne staje.
  • Pomoćni niz napravi jednom (globalno), a ne u svakom pozivu – inače program troši mnogo vremena na pravljenje nizova.
  • Ne zaboravi da posle spajanja prepišeš pom[l..d] nazad u a.
  • Broj inverzija čuvaj u long long.

Proveri znanje

Otvori kviz

Sortiranje objedinjavanjem

1. Koliko vremena traje spajanje dva sortirana niza dužina n i m?




2. Koja je složenost merge sorta u najgorem slučaju?




3. Koja je mana merge sorta u odnosu na sortiranje izborom?




4. Koliko inverzija ima niz 3 1 2?