Files
ubbook/runtime/recursion.md
2024-12-30 13:52:33 +01:00

71 lines
4.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Рекурсия
Многие алгоритмы очень красиво и компактно записываются в рекурсивной форме.
Сортировки, обходы графов, строковые алгоритмы.
Однако рекурсия требует места для хранения промежуточного состояния — на куче или в стеке.
Конечно, есть хвостовая рекурсия, которая естественным образом может быть оптимизирована в цикл. Но это не гарантировано стандартом. Да и не всегда рекурсия именно хвостовая.
Stack overflow не совсем неопределенное поведение, но точно не то, чего хочется видеть на боевом стенде. Потому в серьезных приложениях предпочитают итеративные алгоритмы рекурсивным. Если, конечно, нет гарантии, что глубина рекурсии мала.
В деле искоренения рекурсии из своей программы нужно быть очень внимательным. И не только в корректной имплементации алгоритмов.
Помимо алгоритмов, рекурсивными могут быть и структуры данных. И тут в игру вступает RAII, правила нуля, порядок вызовов деструкторов и `noexcept`.
```C++
struct Node {
int value = 0;
std::vector<Node> children;
};
```
Такая структура совершенно законна для определения дерева, [компилируется и работает](https://godbolt.org/z/evecMd). И может быть удобнее, чем вариант с умными указателями.
Нам не нужно никак вручную управлять ресурсами, вектор позаботится обо всем самостоятельно. Пользуемся «правилом нуля» и не пишем ни деструктор, ни конструктора копирования, ни оператора перемещения/копирования, ничего — красота!
Однако, деструктор, сгенерированный компилятором, будет рекурсивным! И при слишком большой глубине дерева мы получим переполнение стека.
Хорошо, пишем свой деструктор: нам нужна очередь, чтобы обойти вершины дерева... А очередь это аллокация памяти. А аллокация памяти — операция, бросающая исключения. И вот у нас деструктор будет бросать исключения. Что совсем не хорошо.
Можно написать деструктор без аллокаций и рекурсии. Но его алгоритмическая сложность будет квадратичной:
1. Находим вершину, у которой последний элемент в векторе потомков является листом.
2. Удаляем этот элемент из вектора.
3. Повторяем, пока дерево не закончится
Для обычного связанного списка проблема также сохраняется
```C++
struct List {
int value = 0;
std::unique_ptr<List> next;
};
```
Но в этом случае рекурсия является хвостовой и можно надеяться, что оптимизатор справится. Но вы же гоняете тесты и на дебажных сборках, верно?
Так что пишем деструктор, а вместе с ним все остальные специальные методы (в данном случае только перемещающие операции)
```C++
struct List {
int value = 0;
std::unique_ptr<List> next;
~List() {
while (next) {
// деструктор все также рекурсивен,
// но теперь глубина рекурсии — 1 вызов
next = std::move(next->next);
}
}
List() noexcept = default;
List(List&&) noexcept = default;
List& operator=(List&&) noexcept = default;
};
```
---------
С рекурсивными структурами данных в C++ нужно быть очень аккуратными. Не просто так в
Rust написать их «очевидным» способом тяжело.