Что такое рекурсия в программировании?

Ответ

Рекурсия — это подход в программировании, при котором функция (или метод) вызывает саму себя непосредственно или через цепочку других вызовов для решения задачи.

Ключевые компоненты рекурсивной функции:

  1. Базовый случай (терминальный случай): Простое условие, при котором функция возвращает результат без дальнейших рекурсивных вызовов. Обязателен для предотвращения бесконечной рекурсии и переполнения стека вызовов.
  2. Рекурсивный шаг: Вызов функции самой себя с изменёнными (обычно упрощёнными) аргументами, которые приближают задачу к базовому случаю.

Классический пример: вычисление факториала. n! = n * (n-1)!, где 0! = 1 (базовый случай).

function factorial(int $n): int {
    // 1. Базовый случай
    if ($n <= 1) {
        return 1;
    }
    // 2. Рекурсивный шаг: задача сводится к меньшей (n-1)
    return $n * factorial($n - 1);
}

echo factorial(5); // 120
// Вызовы: factorial(5) -> 5 * factorial(4)
//                   -> 5 * (4 * factorial(3))
//                   -> 5 * (4 * (3 * factorial(2)))
//                   -> 5 * (4 * (3 * (2 * factorial(1))))
//                   -> 5 * (4 * (3 * (2 * 1))) = 120

Более практический пример: обход дерева каталогов.

function listFilesRecursively(string $directory): void {
    $items = scandir($directory);

    foreach ($items as $item) {
        if ($item === '.' || $item === '..') {
            continue; // Пропускаем служебные записи
        }

        $path = $directory . DIRECTORY_SEPARATOR . $item;

        if (is_dir($path)) {
            // Рекурсивный шаг: зашли в подкаталог
            echo "Каталог: $pathn";
            listFilesRecursively($path);
        } else {
            // Базовый "случай" для этого уровня рекурсии — это файл
            echo "  Файл: $itemn";
        }
    }
}

listFilesRecursively('/path/to/project');

Плюсы рекурсии:

  • Элегантность и читаемость для задач, имеющих рекурсивную природу (обход деревьев, графов, синтаксический разбор, задачи типа "Ханойские башни", "Фибоначчи").
  • Позволяет писать компактный код для сложных вложенных структур.

Минусы и ограничения:

  • Потребление памяти: Каждый рекурсивный вызов помещает новый контекст выполнения в стек. При большой глубине рекурсии это может привести к переполнению стека (Stack Overflow).
  • Производительность: Часто проигрывает итеративным решениям из-за накладных расходов на вызовы функций.

Когда использовать: Когда задача естественным образом разбивается на идентичные подзадачи (разделяй и властвуй), и глубина рекурсии гарантированно невелика или контролируема. В иных случаях часто предпочтительнее итерация с использованием циклов и явных структур данных (например, стека).

Ответ 18+ 🔞

А, ну вот, рекурсия, говоришь? Ёпта, это ж классика, как водка с огурцом! Смотри, чтобы не охуеть с непривычки, я тебе на пальцах объясню.

Представь, что у тебя есть функция, которая умеет решать только простейшую задачку. А чтобы решить что-то посложнее, она, хитрая жопа, просто зовёт на помощь... саму себя, но чуть более глупую версию. И так до тех пор, пока не упрётся в самый простой случай, который уже и решать не надо. Это и есть рекурсия, ебать мои старые костыли.

Вот смотри, два главных кита, на которых она держится, а то упадёт и разобьётся, как мартышлюшка с пальмы:

  1. Базовый случай. Это когда уже всё, приехали. Дальше рекурсить нельзя, иначе уйдёшь в бесконечность и накроешься медным тазом с переполнением стека. Это типа стоп-кран.
  2. Рекурсивный шаг. А вот тут уже начинается магия. Функция, не долго думая, вызывает саму себя, но с чуть более простой задачей. Как будто говорит: «Э, сабака сука, я сама не справлюсь, но вот тебе почти такая же задачка, только попроще, реши её и верни мне ответ, я тут посчитаю».

Классический пример — факториал. Ну, это когда 5! = 5*4*3*2*1. Рекурсивно это выглядит так изящно, что аж терпения ноль ебать хочется сразу всё переписать на циклах. Но смотри:

function factorial(int $n): int {
    // 1. Базовый случай: если число 1 или меньше, просто верни 1 и не парься.
    if ($n <= 1) {
        return 1;
    }
    // 2. Рекурсивный шаг: "Я не знаю, чей факториал равен n!,
    // но знаю, что он равен n * факториал от (n-1). Эй, я-же-более-тупая-версия-меня, иди посчитай (n-1)!"
    return $n * factorial($n - 1);
}

echo factorial(5); // 120

А теперь представь, как это работает внутри: factorial(5) охуевает от сложности и просит factorial(4) посчитать свою часть. factorial(4) тоже не лыком шит и скидывает задачку на factorial(3). И так далее, пока factorial(1) не скажет: «Да похуй, вот тебе 1, заебал». И тогда всё катится обратно, как снежный ком, перемножаясь, и выдаёт 120. Красота, удивление пиздец!

А вот пример из жизни, где без рекурсии — просто пидарас шерстяной. Обход папок в файловой системе:

function listFilesRecursively(string $directory): void {
    $items = scandir($directory);

    foreach ($items as $item) {
        if ($item === '.' || $item === '..') {
            continue; // Это служебные файлы, на них забей.
        }

        $path = $directory . DIRECTORY_SEPARATOR . $item;

        if (is_dir($path)) {
            // Опа, нашли папку! Рекурсивный шаг: заходим в неё и делаем ВСЁ ТО ЖЕ САМОЕ.
            echo "Каталог: $pathn";
            listFilesRecursively($path); // Вот он, вызов себя любимого!
        } else {
            // Базовый случай для этого уровня: это файл, просто напечатали и пошли дальше.
            echo "  Файл: $itemn";
        }
    }
}

Плюсы у этого дела, конечно, есть: для задач вроде деревьев код получается чистый и понятный, прямо пизда рулю.

Но минусы — овердохуища:

  • Жрёт память. Каждый вызов — новый слой в стеке. Углубишься сильно — хлоп, переполнение стека, и всё, будет вам хиросима.
  • Часто медленнее, чем обычный цикл, потому что вызов функции — дело не бесплатное.

Так что используй рекурсию с умом, э бошка думай. Для обхода деревьев, разбора выражений — самое то. А для простого перебора чисел лучше взять цикл, а то так и до хуя с горы недалеко.

Видео-ответы