---
# System prepended metadata

title: Rekurencja oraz Sortowania

---

# Rekurencja oraz Sortowania
## Rekurencja
### Wprowadzenie
Na zajęciach zaczeliśmy od prostego przedstawienia rekurencji jako funkcji która zamiast wzracać właściwy wynik pewnego problemu, zwracała wynik jej wywołania dla mniejszych argumentów.

TL;DR rozwiązujemy problem korzystając z pewnych bazowych faktów.

Przykład:
*Posiadamy funkcję $f$ dla której zachodzi $f(n)=f(n-1)+1$ i $f(0)=2$. Napisz algorytm obliczający $f(n)$ dla podanego $n$*.

Jako domyślny homo sapiens, najprościej by nam było rozpisywać kolejne wartości funkcji, aż dojdziemy do $f(n)$.
$f(0)=2$
$f(1)=f(0)+1=3$
$f(2)=f(1)+1=4$
...

Warto zauważyć, że rozwiązanie to może być odwrócone do góry nogami. Mianowicie może zacząć od $f(n)$ i używając podstawiania obliczyć wynik:
$f(n)=f(n-1)+1$
$f(n)=(f(n-2)+1)+1$
...

Schodzilibyśmy coraz niżej, aż funkcja po prawej miała argument $0$, dla którego znamy wartość funkcji.

No ale dobra, jak miałby wyglądać kod takiego rozwiązania? Wbrew pozorom implementacja rekurencji jest zazwyczaj łatwiejsza od formalnego zapisu matematycznego (przynajmniej wg. mnie, Cez'a).

```c++
#include<bits/stdc++.h>
using namespace std;

//Definiujemy funkcję, która przyjmuję jakiś argument x i zwraca 
//jakąś liczbę.
int f(int x){
    
    if(x==0) return 2; //Dla wywołania funkcji f(0), zwróć 2.

    return f(x-1)+1; //W przeciwnym wypadku zwróć wartość niższej funkcji
                     //z dodaną jedynką.
}


int main(){

    int n;
    cin>>n;
    
    cout<<f(n)<<endl;

}

```

Jest to oczywiście bardzo łatwy przykład (do którego po chwili zastanowienia nie trzeba nawet użyć rekurencji ale ciii). Zastanówmy się nad trochę bardziej skomplikowanym przykładem rekurencji.


### Liczby Fibbonacci'ego

Zapewne Ci którzy się interesują matematyką bądź oglądają matematykę na YT spotkali się z liczbami Fibbonacci'ego. Są one zdefiniowane następująco:

$F_0=0$
$F_1=1$
$F_n=F_{n-1}+F_{n-2}$

Oczywiście naszym zadaniem jest:
*Dla podanego $n$, oblicz $F_n$.*

Nic skomplikowanego. Dodajemy dwie poprzednie liczby i to jest nasza najnowsza wartość którą używamy w kolejnych obliczeniach.

Problem ten można rozwiązać na dwa sposoby: rekurencyjnie i iteracyjnie. Najpierw omówimy rozwiązanie rekurencyjne, następnie iteracyjne, a na koniec zobaczymy, że iteracyjne bije na głowę rozwiązanie rekurencyjne.

#### Rozwiązanie rekurencyjne

Zgodnie z ideą rekurencji, dla podstawowych argumentów 0 i 1 zwrócimy konkretną wartość, a dla argumentów większych zwrócimy wynik jakiejś operacji na funkjach o niższych argumentach.

```c++
#include<bits/stdc++.h>
using namespace std;


int F(int x){
    
    if(x==0) return 0; //Dla wywołania funkcji f(0), zwróć 0.
    if(x==1) return 1; //Dla wywołania funkcji f(0), zwróć 0.

    return F(x-1)+F(x-2); //W przeciwnym wypadku zwróć dodane do siebie poprzednie 
                          //dwie liczby Fibbonnaci'ego
}


int main(){

    int n;
    cin>>n;
    
    cout<<F(n)<<endl;

}
```

#### Rozwiązanie iteracyjne

Podobnie jak w przykładzie na począku wprowadzenia do rekurencji, zamiast używać jakiejś rekurencji, będziemy liczyć liczby Fibbonacci'ego po kolei, zaczynając od dwójki.

```c++
#include<bits/stdc++.h>
using namespace std;


int F(int x){

    int F1=0, F2=1;
    
    
    for(int i = 2; i<=n; i++){
    
        int Suma = F1 + F2;
        
        F1 = F2;
        F2 = Suma;
    
    }
    
    return F2;

}


int main(){

    int n;
    cin>>n;
    
    cout<<F(n)<<endl;

}
```

#### Porównanie rozwiązań

Jak widzimy oba rozwiązania są proste co do implementacji. Jednakże jedno z nich jest w swojej prostocie zabójcze czasowo (i pamięciowo). Chodzi tutaj o rekurencyjne rozwiązanie.

Najpierw zastanówmy się nad złożonością naszego iteracyjnego rozwiązania. Widzimy dwie zmienne i jedną pętlę for. Można zatem z pewnością powiedzieć, że jest to złożoność liniowa czasowo, i stała pamięciowo. Żadnych haczyków.

Rekurencja natomiast jest trochę bardziej skomplikowana. Wizualna reprezentacja wywołań funkcji F(n) jest o wiele łatwiejsza do zrozumienia:
![](https://i.imgur.com/Vko5YBB.png)

Pozwoliłem sobie pożyczyć takie drzewko z google images. Widzimy, że dla każdego argumentu $x\geq2$, funkcja fib wywoła dwie kolejne funkcje. Czyli jedna funkcja wywoła dwie, te więc wywołają cztery etc.

Dostajemy więc $2+2^2+2^3...+2^n$ wywołań. Oczywiście ta liczba nie jest dokładna bo nie uwzględniłem w pełni specyfikacji problemu, ale jak można zauważyć na drzewku, różnica nie jest bardzo znacząca. Złożoność czasowa tego rozwiązania zatem jest na pewno wykładnicza, czyli o wiele wolniejsza niż liniowa. 

Pamięciowo? Zauważmy, że gdy wywołamy A w funkcji B, to po zakończeniu się funkcji A, trzeba wrócić do funkcji B, czyli komputer pamięta wszytkie wyższe wywołania funkcji. Czyli nasza najdłuższa gałąź w naszym drzewku (czyli ta skrajna lewa) jest naszą złożonością pamięciową. Otrzymujemy więc liniową złożoność czasową.

Zatem iteracyjne rozwiązania totalnie niszczy rekurencyjne czasowo jak i pamięciowo. Nie oznacza to, że rekurencja jest ogólnie do bani. Na wykładzie chociażby pokazywałem algorytm na wypisanie wszystkich permutacji ciągu liczb od $1$ do $n$. Zachęcam zainteresowanych do poczytania o rekurencji więcej.



### NWD
Na zajęciach przypomnieliśmy sobie jak można w sposób rekurencyjny zapisać NWD dwóch liczb. 

```c++
#include<bits/stdc++.h>
using namespace std;
int NWD(int a, int b)
{
    if(b == 0)                // warunek kończący rekurencje
    {
        return a;
    }
    return NWD(b, a % b);     // obliczanie rekurencyjnie NWD  
}
int main()
{
    int a,b; cin >> a >> b;
    cout << NWD(a,b);
}
```
### Szybkie potęgowanie
Na zajęciach zauważyliśmy prostą obserwację, że liczbę $a^n$ możemy zapisać jako $a^n = a^{n/2} * a^{n/2}$ dla $n$ parzystego, a dla $n$ nieparzystego $a^n =a^{n/2} * a^{n/2} * a$. Następnie skorzystaliśmy z tego faktu do napisania programu z użyciem rekurencji.
```c++
#include<bits/stdc++.h>
using namespace std;
int POT(int a, int n)
{
    if(n == 1)                // warunek kończący rekurencje
    {
        return a;
    }
    if(n % 2 == 0) 
        return POT(a,n/2)*POT(a,n/2);        // jeżeli n jest parzyste
    else 
        return POT(a,n/2)*POT(a,n/2) * a;    // jeżeli n jest nieparzyste

}
int main()
{
    int a,n;
    
    cout << POT(a,n);
}
```
## Sortowanie

### BubbleSort

Sortowanie bąbelkowe jest bardzo proste co do idei. Lecimy po tablicy elementów, i jak dwa elementy obok siebie nie są posortowane, to jest swap'ujemy.

Przykład:
0 5 3 2
^ ^
0 5 3 2
  ^ ^
0 3 5 2
    ^ ^
0 3 2 5

To było jedne przejście po tablicy. Jak widzimy elementy nie są jeszcze posortowane. To dlatego, że BubbleSort wymaga kilka takich przejść. W najgorszym przypadku (jaki jest taki przypadek?) będzie to $n$ przejść.

Zatem złożoność czasowa to $n^2$. Pamięciowa? Cały czas operujemy na tablicy więc jedynie $n$ aby pamiętać elementy oczywiście.

```c++
#include<bits/stdc++.h>
using namespace std;

void BubbleSort(int (&tab)[100], int n)
{
    
    bool Swapped = false;
    
    for(int i = 0; i<n; i++){
    
        Swapped = false;
    
        for(j = 1; j<n - i; j++){
        
            if(tab[j-1] > tab[j]){
                swap(tab[j-1], tab[j]);
                Swapped = true;
            }
        
        }
        
        if(Swapped) break;
    
    }


}
int main()
{
    int tab[100],n;
    
    cin>>n;
    for(int i = 0; i<n; i++) cin>>tab[i];
    
    cout << BubbleSort(tab, n);
}
```

W mojej implementacji są dwa usprawnienia (z czego jedno było sugerowane przez jednego z was ale byłem zbyt zmęczony aby zrozumieć o co chodzi, sory).
    1. Jeśli nie swap'owałem elementów w danym przejściu, to znaczy, że tablica jest posortowana.
    2. Przy i'tym przejściu, swap'ujemy jedynie do n-i elementu, bo przy każdym przejściu przenieśliśmy największy element na sam koniec.

### Sortowanie przez zliczanie
Jednym z algorytmów sortowania jest właśnie algorytm przez zliczanie. Jego zasada działania jest banalnie prosta. Wczytujemy nasz ciąg i zliczamy ile wystąpił dany wyraz $a_n$. Złożoność pamięciowa takiego algorytmu wynosi $O(max(a_1,a_2,...,a_n))$, gdzie $a_i$ oznaczają wyrazy ciągu podanego na wejściu, natomiast czasowa $O(n)$.
```c++
#include<bits/stdc++.h>
using namespace std;
int tab[101]    // jeśli wiemy że liczby są z przedziału od 0 do 100
int main()
{
    int n; cin >> n;
    for(int i = 0 ; i < n ; i ++)
    {
        int x; cin >> x;
        tab[x]++;                // zwiększamy ilość wystąpienia danego wyrazu
    }
    
    for(int i = 0 ; i <= 100; i++)
    {
        while(tab[i] > 0)        
        {
            cout << i << " ";
            tab[i]--;
        }
    }

}
```

### QuickSort
Algorytm ten działa w następujący sposób. Najpierw wybieramy sobie losowo "piwota", który pomoże nam podzielić dany przedział tak by na lewo było rzeczy mniejsze bądź równe piwotowi a na prawo większe bądź równe. Następnie wykonujemy takie podzielenie kolejny raz na dwa nowo powstałe przedziały.
```c++
#include<bits/stdc++.h>
using namespace std;
const int MAX = 2*1e5 + 2;

int n;
int tab[MAX];

int Podziel(int *tablica, int l, int r)
{
    int pivot = tablica[l];        // wyznaczenie piwota (tutaj jako pierwszy element przedziału a nie losowo)
    int i = l, j = r;            
    while(i <= j)        
    {
        while(tablica[i] < pivot) i++;

        while(tablica[j] > pivot) j--;

        if(i < j)
        {
            swap(tablica[i], tablica[j]);
            i++;
            j--;
        } else
        {
            break;
        }
    }        
    return j;        // miejsce podziału przedziału (l,r)

}
void QuickSort(int *tablica, int l, int r)
{
    if(l < r)
    {
        int Przedzial = Podziel(tablica, l,r);    // wyznaczenie miejsca podziału

        QuickSort(tablica, l, Przedzial);        // puszczenie QS na przedział powstały po lewej stronie
        QuickSort(tablica, Przedzial + 1, r);    // puszczenie QS na przedział powstały po prawej stronie
    }
}

int main ()
{
    ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0);

    cin >> n;
    for(int i = 0 ;i < n; i++)
    {
        cin >> tab[i];
    }
    QuickSort(tab,0,n-1);
    for(int i = 0 ;i < n; i++)
    {
        cout << tab[i] << " ";
    }

}
```

### Mergesort

Sortowanie przez scalanie jest troszkę prostsze od QuickSorta i troszkę bardziej stabilne (przy odpowiednio niesprzyjających warunkach, QuickSort może osiągnąć $n^2$).

Mianowicie będziemy dzielić naszą tablicę na dwa aż dostaniemy jedno-elementowe przedziały. Następnie zaczniemy je scalać.

Scalanie polega na przejściu po dwóch posortowanych tablicza i branie kolejnego mniejszego elementu z pierwszej bądź drugiej tablicy.

Przykład:

2 4 6
^
3 5 7
^

Scalona tablica: 2

2 4 6
  ^
3 5 7
^

Scalona tablica: 2 3

2 4 6
  ^
3 5 7
  ^

Scalona tablica: 2 3 4

... i tak dalej.

Następujące drzewko wizualizuje ten algorytm:

![](https://i.imgur.com/Sj5KSru.png)


Algorytm jest najbardziej zrozumiałem w kodzie

```c++
#include <bits/stdc++.h>

using namespace std;

vector<int> pomocniczy;

void MergeSort(vector<int> &list, int l, int p){

    if(p==l) return;

    //sortujemy dwa równe podzbiory

    int s = (l+p)/2;
    
    MergeSort(list, l, s);
    MergeSort(list, s+1, p);

    //mergeujemy te dwa podzbiory
    
    pomocniczy.clear();
    int i = l, j = s+1;

    while(i <= s || j <= p){

        if(i>s){    //osiągnęliśmy koniec pierwszego podzbioru
            pomocniczy.push_back(list[j]);
            j++;
            continue;
        }
        if(j>p){    //a tutaj drugiego
            pomocniczy.push_back(list[i]);
            i++;
            continue;
        }
        
        if(list[i] < list[j]){
            pomocniczy.push_back(list[i]);
            i++;
        }
        else{
            pomocniczy.push_back(list[j]);
            j++;
        }
    }

    for(int i = l; i<=p; i++){
        list[i]=pomocniczy[i-l];
    }
}


int main(){
    ios_base::sync_with_stdio(0); cin.tie(0);
    
    int n;
    cin>>n;

    vector<int> nums;

    for(int i = 0; i<n; i++){
        int x;
        cin>>x;
        nums.push_back(x);
    }

    MergeSort(nums, 0, n-1);

```

Do scalania można zdefiniować osobną funkcję. Zachęcam chętnych do napisania oddzielnej funkcji scalania która zostałaby użyta w funkcji MergeSort.



*Ja (Cez) i Dawid Skowronek (Skowi) zastrzegamy sobie prawo do nie brania odpowiedzialności za błędy w powyższym artykule które prawdopodobnie zostały wywołane zmęczeniem i brakiem zdolności humanistycznych.*