I kako ga koristiti za rješavanje problema

Rekurzija je jedna od najstrašnijih tema s kojima se studenti suočavaju u programiranju. Teško je to razumjeti jer ljudski mozak nije sposoban izvesti rekurziju - ali računala jesu. Upravo je to razlog zašto je rekurzija tako moćan alat za programere, ali to također znači da je učenje njezine uporabe izuzetno teško. Želim vam pomoći u izgradnji intuicije za rekurziju kako biste je mogli koristiti za rješavanje problema.
Asistent sam u nastavi za uvodni tečaj informatike na svom sveučilištu. Rekurziju sam objasnio na potpuno isti način desetak puta ovaj tjedan. Čini se da moje objašnjenje pomaže većini učenika. Ovaj članak ima najopćenitije objašnjenje na vrhu, a najkonkretnije objašnjenje na dnu. Na taj način možete početi na početku i zaustaviti se čim osjetite da dovoljno dobro razumijete rekurziju. Naveo sam nekoliko primjera u Javi, a oni su dovoljno jednostavni da ih može protumačiti svatko s nekim iskustvom u programiranju.
Što je rekurzija?
Da bismo razumjeli rekurziju, vratimo se korak od programiranja. Krenimo od uspostavljanja opće definicije pojma. Nešto je rekurzivno ako je donekle definirano vlastitom definicijom. To vam vjerojatno ne pomaže mnogo da razumijete rekurziju, pa pogledajmo matematičku definiciju. Poznate su vam funkcije - jedan broj uđe, a drugi izađe. Izgledaju ovako:
f (x) = 2x
Promijenimo ovu ideju malo i umjesto toga razmislimo o slijedu. Slijed uzima cjelobrojni broj i izlazi cjelobrojni broj.
A (n) = 2n
Sekvence se mogu smatrati funkcijama s ulazima i izlazima koji su ograničeni samo na pozitivne cijele brojeve. Općenito, nizovi počinju s 1. To znači da je A (0) 1. Gornji slijed je sljedeći:
A (n) = 1, 2, 4, 6, 8, 10, ... gdje je n = 0, 1, 2, 3, 4, 5, ...
Sada uzmite u obzir sljedeći slijed:
A (n) = 2 x A (n-1)
Ovaj je slijed rekurzivno definiran. Drugim riječima, vrijednost bilo kojeg datog elementa ovisi o vrijednosti drugog elementa. Ovaj slijed izgleda ovako:
A (n) = 1, 2, 4, 8, 16,… gdje je n = 0, 1, 2, 3, 4,…
Bilo koji element definiran je kao 2 puta veći od prethodnog elementa.
- N = 4 element, 16, definiran je kao 2 puta veći od prethodnog elementa.
- N = 3 element, 8, definiran je kao 2 puta veći od prethodnog elementa.
- N = 2 element, 4, definiran je kao 2 puta veći od prethodnog elementa.
- N = 1 element, 2, definiran je kao 2 puta veći od prethodnog elementa.
- N = 0 element, 1, definiran je kao ...
Element n = 0 ne može se rekurzivno definirati. Ne postoji prethodni element. To nazivamo osnovnim slučajem i nužna je posljedica rekurzivnih definicija. Moraju biti izričito predstavljeni u vašem kodu . Mogli bismo predstaviti ovaj rekurzivni slijed na Javi ovako:
public int A(int n){ if (n == 0) return 1; return 2 * A(n - 1);}
Trebali biste se upoznati s anatomijom rekurzivne metode. Primijetite osnovni slučaj: ako je n 0, element je definiran kao 1. Inače, element je definiran kao 2 puta veći od prethodnog elementa. Moramo rekurzivno pozvati metodu da bismo dobili vrijednost prethodnog elementa, a zatim je pomnožiti s 2. Sve rekurzivne metode imat će ove dvije komponente:
- Osnovni slučaj, koji vraća dobro definiranu vrijednost.
- Rekurzivni slučaj, koji vraća rekurzivno definiranu vrijednost.

Napravimo još jedan primjer, nastavljajući s matematičkim kontekstom. Fibonaccijev niz se često koristi za ilustraciju rekurzije. Bilo koji element Fibonaccijeve sekvence zbroj je dva prethodna elementa. To ide ovako:
F (n) = 1, 1, 2, 3, 5, 8,… gdje je n = 0, 1, 2, 3, 4, 5,…
- N = 5, element 8 definiran je kao zbroj elementa n = 4 i n = 3 ...
U ovom trenutku trebali biste oklijevati. U prethodnom primjeru svaki je element ovisio samo o jednom drugom elementu, sada svaki element ovisi o dva druga elementa. Ovo komplicira stvari.
- N = 4 element, 5, definiran je kao zbroj n = 3 elementa i n = 2 elementa.
- N = 3 element, 3, definiran je kao zbroj n = 2 elementa i n = 1 elementa.
- N = 2 element, 2, definiran je kao zbroj n = 1 elementa i n = 0 elementa.
- N = 1 element, 1, definiran je kao zbroj n = 0 elementa i ...
Element n = 1 ne može se rekurzivno definirati. Niti element n = 0 ne može. Ti se elementi ne mogu rekurzivno definirati jer rekurzivna definicija zahtijeva dva prethodna elementa. Element n = 0 nema prethodnih elemenata, a element n = 1 ima samo jedan prethodni element. To znači da postoje dva osnovna slučaja. Prije pisanja bilo kojeg koda, zapisao bih nešto poput ovoga:
Element n = 0 definiran je kao 1. Element n = 1 definiran je kao 1.
N element definiran je kao zbroj n-1 elementa i n-2 elementa.
Sada imamo ideju o tome kako je ovaj zadatak rekurzivno definiran, i možemo nastaviti i napisati neki kod. Nikadazapočnite pisati kod bez da ste prethodno prirodno razumjeli zadatak.
public int F(int n) if (n == 0
Skup poziva

Kao programeri, želimo imati intuiciju za rekurziju kako bismo je mogli koristiti za rad. Da bismo to učinili učinkovito, moramo razumjeti kako računalo obrađuje rekurziju.
Postoji podatkovna struktura koju računalo koristi za praćenje poziva metoda koji se nazivaju hrpa poziva . Svaki poziv metode stvara lokalne varijable iz parametara metode. Računalo mora pohraniti ove varijable dok se metoda izvršava. Zatim, računalo uklanja vrijednosti kad se metoda vrati kako bi se izbjeglo gubljenje memorije.
The call stack (and stacks in general) function as you might imagine some sort of real-life stack would. Imagine a stack of papers on your desk — it starts as nothing, and then you add papers one by one. You don’t know anything about any of the papers in the stack except for the paper on top. The only way you can remove papers from the stack is by taking them off the top, one-by-one, in the opposite order that they were added.
This is essentially how the call stack works, except the items in the stack are activation records instead of papers. Activation records are just little pieces of data that store the method name and parameter values.
Without recursion, the call stack is pretty simple. Here’s an example. If you had some code that looked like this…
public static void main(String[] args) System.out.println(myMethod(1));
…The call stack would look like this:
* myMethod(int a)
* main(String[] args)
Here we see two methods under execution, main
and myMethod
. The important thing to notice is that main
cannot be removed from the stack until myMethod
is removed from the stack. In other words, main
cannot complete until myMethod
is called, executed, and returns a value.
This is true for any case of method composition (a method within a method) — so let’s look at recursive example: the A(int n)
method we wrote earlier. Your code might look like this:
public static void main(String[] args) System.out.println(A(4));
public static int A(int n){ if (n == 0) return 1; return 2 * A(n - 1);}
When main
is called, A
is called. When A
is called, it calls itself. So the call stack will start building up like so:
* A(4)* main(String[] args)
A(4)
calls A(3)
.
* A(3)* A(4)* main(String[] args)
Now, it’s important to note that A(4)
cannot be removed from the call stack until A(3)
is removed from the call stack first. This makes sense, because the value of A(4)
depends on the value of A(3)
. The recursion carries on…
* A(0)* A(1)* A(2)* A(3)* A(4)* main(String[] args)
When A(0)
is called, we have reached a base case. This means that the recursion is completed, and instead of making a recursive call, a value is returned. A(0)
comes off the stack, and the rest of the calls are then able to come off the stack in succession until A(4)
is finally able to return its value to main.
Here’s the intuition: the return value of any method call depends on the return value of another method call. Therefore, all the method calls must be stored in memory until a base case is reached. When the base case is reached, the values start becoming well-defined instead of recursively defined. For example, A(1)
is recursively defined until it knows the definition of the base case, 1. Then, it is well-defined as 2 times 1.
When we are trying to solve problems with recursion, it is often more effective to think about the order in which values are returned. This is the opposite of the order in which calls are made. This order is more useful because it consists of well-defined values, instead of recursively defined values.
For this example, it is more useful to consider that A(0)
returns 1, and then A(1)
returns 2 times 1, and then A(2)
returns 2 times A(1)
, and so on. However, when we are writing our code, it can easier to frame it in the reverse order (the order that the calls are made). This is another reason that I find it helpful to write the base case and the recursive case down before writing any code.
Helper Methods and Recursion vs. Loops

We are programmers, not mathematicians, so recursion is simply a tool. In fact, recursion is a relatively simple tool. It’s very similar to loops in that both loops and recursion induce repetition in the program.
You may have heard that any repetitive task can be done using either a while loop or a for loop. Some tasks lend themselves better to while loops and other tasks lend themselves better to for loops.
The same is true with this new tool, recursion. Any repetitive task can be accomplished with either a loop or recursion, but some tasks lend themselves better to loops and others lend themselves better to recursion.
When we use loops, it is sometimes necessary to make use of a local variable to “keep track” of a calculation. Here’s an example.
public double sum (double[] a){ double sum = 0.0; for (int i = 0; i < a.length; i++) sum += a[i]; return sum;
}
This method takes an array of doubles as a parameter and returns the sum of that array. It uses a local variable, sum
, to keep track of the working sum. When the loop is completed, sum
will hold the actual sum of all values in the array, and that value is returned. This method actually has two other local variables that are less obvious. There is the double array a
, whose scope is the method, and the iterator i
(keeps track of the index), whose scope is the for loop.
What if we wanted to accomplish this same task using recursion?
public double recursiveSum(double[] a) # recursively calculate sum
This task is repetitive, so it is possible to do it using recursion, though it is probably more elegantly accomplished using a loop. We just need to create a few local variables to keep track of the working sum and the index, right?
Alas, this is impossible. Local variables only exist in the context of a single method call, and recursion makes use of repeated method calls to accomplish a repetitive task. This means that local variables are pretty much useless when we are using recursion. If you are writing a recursive method and you feel as though you need a local variable, you probably need a helper method.
A helper method is a recursive method that makes use of additional parameters to keep track of values. For recursiveSum
, our helper method might look like this:
public double recursiveSum(double[] a, double sum, int index){ if (index == a.length) return sum; sum += a[index]; return recursiveSum(a, sum, index + 1);}
This method builds the sum by passing the working value to a new method call with the next index. When there are no more values in the array, the working sum is the actual sum.
Now we have two methods. The “starter method,” and the helper method.
public double recursiveSum(double[] a) # recursively calculate sum
public double recursiveSum(double[] a, double sum, int index){ if (index == a.length) return sum; sum += a[index]; return recursiveSum(a, sum, index + 1);}
The term “helper method” is actually a bit of a misnomer. It turns out that the helper method does all the work, and the other method is just a starter. It simply calls the helper method with the initial values that start the recursion.
public double recursiveSum(double[] a) return recursiveSum(a, 0.0, 0);
public double recursiveSum(double[] a, double sum, int index){ if (index == a.length) return sum; sum += a[index]; return recursiveSum(a, sum, index + 1);}
Note that the values used in the starter call to the helper method are the same values used to initialize the local variables in the loop example. We initialize the variable used to keep track of the sum to 0.0
, and we initialize the variable used to keep track of the index to 0
.
Earlier, I said that local variables are useless in the context of recursion. This isn’t completely true, because the method parameters are indeed local variables. They work for recursion because new ones are created every time the method is called. When the recursion is executed, there are many method calls being stored in the call stack, and as a result there are many copies of the local variables.
You might ask, “If the helper method does all the work, why do we even need the starter method? Why don’t we just call the helper method with the initial values, and then you only need to write one method?”
Well, remember that we were trying to replace the method that used a for loop. That method was simple. It took an array as a parameter and returned the sum of the array as a double. If we replaced this method with one that took three parameters, we would have to remember to call it with the proper starting values. If someone else wanted to use your method, it would be impossible if he or she didn’t know the starting values.
For these reasons, it makes sense to add another method that takes care of these starting values for us.
Wrapping up
Recursion is a pretty challenging concept, but you made it all the way to the end of my explanation. I hope you understand the magic a little better. I now officially grant you the title of “Grand-Wizard of Recursion.” Congratulations!