Uvod u rekurziju

Lekcija 1 od 7 · Rekurzija i bektreking

Rekurzija je način rešavanja problema u kome funkcija poziva samu sebe, ali za manji problem. U ovoj lekciji vidimo kako se piše rekurzivna funkcija, šta je bazni slučaj i kako računar „pamti“ nedovršene pozive.

Pre nego što počneš: potrebno predznanje – Funkcije u C++-u.
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.

Šta je rekurzija?

Zamisli red ljudi i pitanje „koji si po redu?“. Osoba ne broji ceo red, nego pita osobu ispred sebe i svom odgovoru doda 1. Prva osoba u redu ne pita nikoga – ona zna da je prva. Tako radi i rekurzivna funkcija:

  • bazni slučaj – najmanji problem, čiji je odgovor poznat bez novih poziva (prva osoba u redu);
  • rekurzivni korak – problem se svodi na isti, ali manji problem (pitaj osobu ispred sebe i dodaj 1).

Bez baznog slučaja funkcija bi se pozivala beskonačno – program se prekida greškom (prekoračenje steka).

Prvi primer: odbrojavanje

Funkcija ispisuje broj n, a zatim „ostatak posla“ prepušta pozivu za n - 1:

#include <iostream>
using namespace std;

void odbrojavanje(int n) {
    if (n == 0) {                 // bazni slučaj
        cout << "Start!" << endl;
        return;
    }
    cout << n << " ";
    odbrojavanje(n - 1);          // rekurzivni korak: isti posao za manji broj
}

int main() {
    odbrojavanje(5);
    return 0;
}

Izlaz: 5 4 3 2 1 Start!. Svaki poziv radi samo mali deo posla – ispisuje jedan broj.

Kako se izvršava: stek poziva

Funkcija koja vraća vrednost čeka rezultat poziva za manji problem, pa tek onda završava svoj račun. Pogledajmo zbir prvih n prirodnih brojeva: zbir(n) = n + zbir(n - 1), a zbir(0) = 0.

#include <iostream>
using namespace std;

int zbir(int n) {
    if (n == 0) return 0;         // bazni slučaj
    return n + zbir(n - 1);       // čeka rezultat za n - 1
}

int main() {
    int n;
    cin >> n;
    cout << zbir(n) << endl;
    return 0;
}
Izvršavanje poziva zbir(4)
KorakPozivŠta se dešava
1zbir(4)treba mu zbir(3) – čeka
2zbir(3)treba mu zbir(2) – čeka
3zbir(2)treba mu zbir(1) – čeka
4zbir(1)treba mu zbir(0) – čeka
5zbir(0)bazni slučaj, vraća 0
6zbir(1)vraća 1 + 0 = 1
7zbir(2)vraća 2 + 1 = 3
8zbir(3)vraća 3 + 3 = 6
9zbir(4)vraća 4 + 6 = 10

Nedovršeni pozivi čuvaju se na steku poziva: poslednji započet poziv prvi se završava. Zato je dubina rekurzije ograničena – za nekoliko stotina hiljada ugnježdenih poziva memorija za stek može da se potroši.

Rad pre i posle rekurzivnog poziva

Redosled naredbi u odnosu na rekurzivni poziv menja rezultat. Funkcija ispod ispisuje cifre broja s leva na desno, iako se cifre izdvajaju s desna (n % 10): ispis je posle poziva, pa se izvršava tek kada se vrate svi kraći pozivi.

#include <iostream>
using namespace std;

void cifre(int n) {
    if (n < 10) {                 // jednocifren broj: bazni slučaj
        cout << n << " ";
        return;
    }
    cifre(n / 10);                // prvo sve cifre osim poslednje
    cout << n % 10 << " ";        // pa poslednja cifra
}

int main() {
    int n;
    cin >> n;
    cifre(n);
    cout << endl;
    return 0;
}

Ako se ispis premesti pre poziva (cout << n % 10; cifre(n / 10);), cifre se ispisuju obrnutim redom.

Rekurzija ili petlja?

PetljaRekurzija
Ponavljanjetelo petljenovi poziv funkcije
Krajuslov petlje postane netačanbazni slučaj
Memorijastalnaraste sa dubinom (stek poziva)
Kada je boljajednostavno ponavljanje (zbir, brojanje)problem se prirodno deli na manje iste probleme: stabla, grafovi, bektreking, „podeli pa vladaj“

Svaka rekurzija može da se zameni petljom (ponekad uz sopstveni stek), ali je rekurzivno rešenje često mnogo kraće i jasnije – videćemo to kod Hanojskih kula i bektrekinga.

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

Ispis od 1 do n

Napiši rekurzivnu funkciju koja ispisuje brojeve od 1 do n (u jednom redu, razdvojene razmakom). Petlje nisu dozvoljene.

Primer

Ulaz

5

Izlaz

1 2 3 4 5
ZADATAK 02Težina: Lako

Zbir cifara

Napiši rekurzivnu funkciju int zbirCifara(int n) i ispiši zbir cifara unetog prirodnog broja.

Primer

Ulaz

4072

Izlaz

13
ZADATAK 03Težina: Srednje teško

Koliko puta se pojavljuje cifra

Učitaj prirodan broj n i cifru c. Rekurzivno izbroj koliko puta se cifra c pojavljuje u zapisu broja n.

Primer

Ulaz

1231271 1

Izlaz

3
ZADATAK 04Težina: Srednje teško

Palindrom (rekurzivno)

Napiši rekurzivnu funkciju bool palindrom(const string &s, int i, int j) koja proverava da li je deo reči od pozicije i do j palindrom. Ispiši DA ili NE za učitanu reč.

Primer 1

Ulaz

anavolimilovana

Izlaz

DA

Primer 2

Ulaz

rekurzija

Izlaz

NE
Česte greške i saveti
  • Uvek prvo napiši bazni slučaj, pa tek onda rekurzivni korak.
  • Proveri da li se argument zaista smanjuje ka baznom slučaju (npr. n - 1, n / 10), inače rekurzija ne staje.
  • Parametre tipa string i vector prenosi kao const & – inače se pri svakom pozivu pravi kopija.
  • Za vrlo duboku rekurziju (više stotina hiljada poziva) bolje je rešenje petljom.

Proveri znanje

Otvori kviz

Mini kviz: rekurzija

1. Šta je bazni slučaj rekurzije?




2. Šta ispisuje f(3) ako je void f(int n){ if(n==0) return; f(n-1); cout<<n; }?




3. Šta se dešava kada rekurzivna funkcija nema bazni slučaj?