Лого на 91. НЕГ „Проф. Константин Гълъбов“

Модул 2 · Урок 3

Рекурсия

Функция, която вика себе си. База и индуктивен случай. Стек на извикванията, класически задачи: факториел, Фибоначи, двоично търсене.

Какво е рекурсия

Рекурсията е метод, който вика сам себе си. Звучи странно — как нещо да работи, ако в средата на работата си стартира ново копие от себе си? Тайната е, че всяко следващо извикване работи върху по-малък вход, докато стигне до тривиален случай, който може да реши директно.

Ключово

Всяка рекурсивна функция има две части:

  • База (base case) — най-простият случай, който знаем как да решим без рекурсия.
  • Индуктивен случай (recursive case) — изразяваме задачата чрез по-малка версия на същата задача.

Класически пример — факториел

Факториелът се дефинира така:

  • 0! = 1 ← база
  • n! = n · (n−1)! ← индуктивен случай

В код:

static long Factorial(int n)
{
    if (n <= 1) return 1;          // база
    return n * Factorial(n - 1);   // индуктивен случай
}

Console.WriteLine(Factorial(5));   // 120

Без база — безкрайна рекурсия

Ако махнеш if (n <= 1) return 1;, методът ще продължи да вика себе си с все по-малки числа: 5, 4, 3, 2, 1, 0, −1, −2… До StackOverflowException. Винаги има база.

Стек на извикванията

Когато Factorial(4) извика Factorial(3), текущото му състояние се запазва в стек на извикванията (call stack). Всяко ниво чака долното да върне резултат:

Стек на извикванията при Factorial(4)Factorial(4)→ 4 · 6 = 24Factorial(3)→ 3 · 2 = 6Factorial(2)→ 2 · 1 = 2Factorial(1)→ 1 (база)Връща се отдолу нагоре: 1 → 2 → 6 → 24
Фиг. 1 — Стекът на извикванията расте надолу, после се връща нагоре

Бележка

Това е същият Стек, който видяхме в предишния урок — но за извиквания на методи. Затова твърде дълбока рекурсия хвърля StackOverflowException.

Още класически примери

Сума на числата от 1 до n

static int Sum(int n)
{
    if (n == 0) return 0;          // база
    return n + Sum(n - 1);         // индуктивен случай
}

Console.WriteLine(Sum(100));   // 5050

Степенуване — x на n-та

static double Power(double x, int n)
{
    if (n == 0) return 1;
    return x * Power(x, n - 1);
}

Console.WriteLine(Power(2, 10));   // 1024

Числата на Фибоначи

F(0) = 0, F(1) = 1, F(n) = F(n−1) + F(n−2):

static long Fibonacci(int n)
{
    if (n < 2) return n;
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Console.WriteLine(Fibonacci(10));   // 55

Внимание — бавно за големи n

Тази версия пресмята едно и също много пъти. Fib(40) отнема секунди, Fib(50) — минути. За практика ползвай итеративен вариант или мемоизация (запомняне на изчислените стойности).

Двоично търсене (рекурсивно)

В сортиран масив — гледаме средния елемент. Ако е по-голям от търсеното — търсим в лявата половина; ако е по-малък — в дясната.

static int BinarySearch(int[] arr, int target, int left, int right)
{
    if (left > right) return -1;       // база — не намерено
    int mid = (left + right) / 2;
    if (arr[mid] == target) return mid;
    if (arr[mid] > target)
        return BinarySearch(arr, target, left, mid - 1);
    else
        return BinarySearch(arr, target, mid + 1, right);
}

int[] arr = { 1, 3, 5, 7, 9, 11, 13, 15 };
Console.WriteLine(BinarySearch(arr, 11, 0, arr.Length - 1));   // 5

Рекурсия vs цикъл

Почти всяка рекурсия може да се напише и с цикъл (и обратното). Кога кое?

РекурсияЦикъл (итерация)
ЧетимостЧесто по-кратка и близка до математическата дефиницияПо-многословна, но „по-механична"
ПаметЗаема място в стека на извикваниятаМинимална — само локални променливи
СкоростПо-бавна (има overhead от извикването)Малко по-бърза
ДълбочинаОграничена от размера на стека (≈ 10 000 нива)Неограничена
Подходяща заДървовидни структури, разделяй и владей, графиПрости повторения, обхождане на масиви

Иterative variant of factorial

static long FactorialIter(int n)
{
    long result = 1;
    for (int i = 2; i <= n; i++) result *= i;
    return result;
}

За факториел итеративният вариант е и по-кратък, и по-бърз, и не рискува препълване на стека. За много задачи обаче рекурсията се чете много по-естествено.

Често срещани грешки

  • Липсваща база — рекурсията не спира и хвърля StackOverflowException.
  • Грешна база — спира, но връща грешен резултат. Пример: if (n == 0) return 0; за факториел вместо return 1.
  • Не намалява входа return Factorial(n); вместо Factorial(n - 1) също дава безкрайна рекурсия.
  • Дублирани изчисления — както при Фибоначи; решава се с мемоизация или преобразуване в цикъл.

Какво трябва да запомниш

  • Рекурсия = метод, който вика себе си с по-малък вход.
  • Винаги има база (спира) и индуктивен случай (продължава).
  • Извикванията се пазят в стека → твърде дълбоко = stack overflow.
  • Кога рекурсия: естествено рекурсивни задачи (дървета, графи, разделяй и владей).
  • Кога цикъл: прости повторения, чести и дълбоки рекурсии, приоритет към скорост.
  • Шаблон: if (база) return известната-стойност; return нещо(вика-се-с-по-малък-вход);