NOVINKA - Online rekvalifikační kurz Python programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 1 - Úvod do rekurze

Vítejte u první lekce kurzu o rekurzi. Rekurze je jednou ze zdánlivě obtížnějších kapitol programování a s její praktickou aplikací se v každodenním programátorském životě příliš často nesetkáme. Na druhou stranu nám její zvládnutí otevře nové obzory a umožní řešit problémy, které bychom jinými způsoby řešili jen velmi obtížně.

V dnešním tutoriálu o rekurzivních algoritmech si ukážeme, co je to rekurze, a jak se vytváří rekurzivní metoda.

Rekurzivní charakter úlohy

Začněme příkladem. Představme si, že chceme napsat metodu suma(n), která jako parametr přebírá kladné celé číslo n a vrací součet čísel od jedné do n. Implementaci takové metody pomocí cyklu bychom jistě zvládli, my si však můžeme všimnout, že výpočet suma(n) v sobě obsahuje výpočet několika dalších sum, např. pro suma(4) dostaneme:

Rekurzivní úloha - Rekurzivní algoritmy

Vzoreček pro výpočet sumy tedy můžeme zapsat takto:

  • suma(n) = 1, pokud n == 1,
  • suma(n) = n + suma(n – 1), pokud n > 1.

O takových úlohách říkáme, že mají rekurzivní charakter, tzn. že ve svém řešení obsahují dílčí úlohy stejného typu.

Stejný rekurzivní charakter bychom našli u mnoha dalších úloh, např. u výpočtu celočíselné mocniny: xn = x · xn-1, nebo u výpočtu faktoriálu: n! = n · (n-1)!.

Rekurzivní metoda

Vraťme se ale k metodě pro výpočet sumy. Vyjdeme z rekurzivního vzorečku, který přepíšeme do podoby metody:

  • def suma(n):
        if n == 1:
            return 1
        else:
            return n + suma(n - 1)
  • int suma(int n)
    {
        if (n == 1)
            return 1;
        else
            return n + suma(n - 1);
    }
  • public static function suma(int $n): int
    {
        if ($n == 1)
            return 1;
        else
            return $n + suma($n - 1);
    }
  • public static int suma(int n)
    {
        if (n == 1)
            return 1;
        else
            return n + suma(n - 1);
    }

Právě jsme napsali tzv. rekurzivní metodu.

V programátorské terminologii je rekurzivní metoda taková metoda, která někde ve svém těle volá sebe samu.

Všimněme si dvou důležitých věcí:

  • metoda má ukončovací podmínku, při jejímž splnění se další volání už neprovede (zde if n == 1),
  • při rekurzivním volání se mění argument metody – proces se posouvá ke splnění úlohy.

Jak probíhají rekurzivní volání

Při rekurzivním volání se děje to samé, co při jakémkoliv jiném volání metody. Dojde k pozastavení probíhajícího vlákna, volající metoda předá řízení volané metodě a čeká na návratovou hodnotu. Důležité je ale pochopit, že v případě rekurze se toto děje opakovaně a v jedné chvíli máme rozpracováno několik metod, které postupují podle stejného schématu. Časový průběh při volání např. suma(4) vypadá takto:

Rekurzivní volání - Rekurzivní algoritmy

Všimněme si důležité věci: Metoda, která byla volána jako první, končí jako poslední a naopak. Toto schéma označujeme jako LIFO.

Schématu LIFOLast In First Out odpovídá fungování datové struktury zásobníku, který metody pro svůj běh využívají.

Další jednoduché rekurzivní metody

Uveďme si několik dalších příkladů jednoduchých rekurzivních metod.

Klesající posloupnost čísel

Začneme metodou, která jako parametr dostane celé číslo n a na terminál vypíše klesající posloupnost čísel od n do 1. Implementace takové rekurzivní metody může vypadat například takto:

  • def vypisKlesajici(n):
        if n == 0:
            return
        print(n)
        vypisKlesajici(n - 1)
  • void vypisKlesajici(int n)
    {
        if (n == 0)
            return;
        Console.WriteLine(n);
        vypisKlesajici(n - 1);
    }
  • public static function vypisKlesajici(int $n): void
    {
        if ($n == 0)
            return;
        echo("$n<br>");
        vypisKlesajici($n - 1);
    }
  • public static void vypisKlesajici(int n)
    {
        if (n == 0)
            return;
        System.out.println(n);
        vypisKlesajici(n - 1);
    }

Rostoucí posloupnost čísel

Zkusme si napsat podobnou metodu, která ale vypisuje rostoucí posloupnost čísel od 1 do n. Mohli bychom samozřejmě začít od jedničky, vždy vypsat číslo a zavolat metodu s parametrem o jedničku větším. My ale můžeme udělat jinou úpravu: Vyměníme pořadí výpisu čísla a volání metody:

  • def vypisRostouci(n):
        if n == 0:
            return
        vypisRostouci(n - 1)
        print(n)
  • void vypisRostouci(int n)
    {
        if (n == 0)
            return;
        vypisRostouci(n - 1);
        Console.WriteLine(n);
    }
  • public static function vypisRostouci(int $n): void
    {
        if ($n == 0)
            return;
        vypisRostouci($n - 1);
        echo("$n<br>");
    }
  • public static void vypisRostouci(int n)
    {
        if (n == 0)
            return;
        vypisRostouci(n - 1);
        System.out.println(n);
    }

Metoda pro výpis rostoucí posloupnosti tedy napřed zavolá svého rekurzivního potomka, čeká na jeho návrat a teprve nakonec vypisuje svoje číslo. Metoda volaná jako první (vypisující číslo n) tedy provede výpis úplně naposled a naopak, tedy opět LIFO schéma. Výsledkem je výpis rostoucí posloupnosti, ačkoliv jsme začali voláním s parametrem n. Pokud je nám fungování těchto dvou metod jasné, jsme k pochopení celé rekurze velmi blízko.

Nejvyšší liché číslo

Posledním příkladem bude metoda, která prohledá pole celých čísel a vrátí nejvyšší v něm obsažené liché číslo. Pokud pole neobsahuje žádné liché číslo, metoda vrátí nulu:

  • def najdiNejvyssiLiche(pole, index = 0, maxLiche = 0):
        if index == len(pole):
            return maxLiche
        if pole[index] % 2 == 1 and pole[index] > maxLiche:
            maxLiche = pole[index]
        return najdiNejvyssiLiche(pole, index + 1, maxLiche)
  • int najdiNejvyssiLiche(int[] pole, int index = 0, int maxLiche = 0)
    {
        if (index == pole.Length)
            return maxLiche;
        if (pole[index] % 2 == 1 && pole[index] > maxLiche)
            maxLiche = pole[index];
        return najdiNejvyssiLiche(pole, index + 1, maxLiche);
    }
  • public static function najdiNejvyssiLiche(array $pole, int $index = 0, int $maxLiche = 0): int
    {
        if ($index == count($pole))
            return $maxLiche;
        if ($pole[$index] % 2 == 1 && $pole[$index] > $maxLiche)
            $maxLiche = $pole[$index];
        return najdiNejvyssiLiche($pole, $index + 1, $maxLiche);
    }
  • public static int najdiNejvyssiLiche(int[] pole, int index, int maxLiche)
    {
        if (index == pole.length)
            return maxLiche;
        if (pole[index] % 2 == 1 && pole[index] > maxLiche)
            maxLiche = pole[index];
        return najdiNejvyssiLiche(pole, index + 1, maxLiche);
    }

Metoda přebírá tři parametry:

  • prohledávané pole,
  • index aktuálně zkoumaného prvku,
  • nejvyšší dosud nalezenou hodnotu.

Pokud je aktuálně zkoumané číslo liché a je větší než dosavadní maximum, je do dalšího volání dosazeno na jeho místo. Opět nezapomeňme na schéma LIFO: Nejvyšší hodnota se nakonec dostane k prvně volané metodě, která ji vrátí.

Kdy rekurzi použít

Doposud jsme rekurzi demonstrovali na jednoduchých úlohách. Jejich účelem bylo ukázat princip rekurze. Nicméně všechny tyto úlohy se dají snadno vyřešit pomocí cyklů. Rekurzivními metodami můžeme nahradit jakýkoliv cyklus, ale ve skutečnosti to málokterý programátor udělá. Důvody jsou tyto:

  • rekurze zbytečně spotřebovává systémové prostředky, protože volání metod vyžaduje provedení množství operací a alokaci paměti pro argumenty, čemuž se při iteraci vyhneme,
  • program se zbytečnou rekurzí je méně srozumitelný, hůř se spravuje a obtížněji se v něm hledají chyby.

Rekurzi nepoužíváme pro úlohy, které jdou snadno vyřešit prostým cyklem.

Užitečnou pomůckou při rozhodování je kolikrát metoda ve svém těle volá sebe samu. Pokud pouze jednou, označujeme takovou rekurzi za lineární a je velmi pravděpodobné, že taková metoda půjde nahradit cyklem. Pokud metoda sebe samu volá více než jednou, označujeme ji jako stromovou a v tom případě nám může rekurze velmi pomoct.

V příští lekci, Stromová rekurze - Generování všech možností, si ukážeme, jak se rekurze skutečně používá. Naučíme se, jak vygenerovat všechny možnosti při řešení nějaké úlohy.


 

Všechny články v sekci
Rekurzivní algoritmy
Přeskočit článek
(nedoporučujeme)
Stromová rekurze - Generování všech možností
Článek pro vás napsal Jan Hnilica
Avatar
Uživatelské hodnocení:
32 hlasů
Autor se věnuje hlavně programování v C a v Pythonu
Aktivity