8.4. Демонстрация - Чем можно заменить рекурсию: стек
Шаг 1
Есть ещё один способ замены рекурсии, который, в отличие от циклов, не даёт особого выигрыша по памяти — применение стека (или иногда очереди). Порой можно встретить подобные алгоритмы на практике, если их разработчик не дошёл до использования рекурсии, либо просто как альтернативное решение с другой логикой. Хорошим примером альтернативности решений является обход деревьев, о котором мы узнали пару модулей назад: можно использовать либо рекурсивный поиск в глубину, либо поиск в ширину с очередью. Но также можно написать и поиск в глубину с использованием стека.
const root = document.body;
const resultElement = document.getElementById('result');
function traverse(node) {
const result = [];
// в рекурсивной функции...
function recursive(node) {
// ... сначала обрабатываем ноду, в которой находимся ...
result.push(node.localName);
// ...а потом вызываем её же на всех детях
for (const child of node.children) {
recursive(child);
}
}
recursive(node);
resultElement.innerHTML = result.join(' -> ');
}
traverse(root);
Шаг 2
Для этого мы просто берём наш поиск в ширину с очередью.
const root = document.body;
const resultElement = document.getElementById('result');
function traverse(node) {
const result = [];
// очередь для просмотра нод
const queue = [];
// куда мы сразу же кладем ноду, с которой будем начинать обход
queue.push(node);
// обходим, пока в очереди что-то есть
while(queue.length) {
// получаем ноду из списка
const currentNode = queue.shift();
// делаем всё, что нужно с нашей нодой (сейчас это просто перекладывание ноды в список просмотренных)
result.push(currentNode.localName);
// и кладем в очередь всех потомков нашей ноды.
// из-за того, что мы будем класть потомков в конец и доставать из начала, мы гарантированно просмотрим все ноды текущего уровня перед спуском на следующий
queue.push(...currentNode.children);
}
resultElement.innerHTML = result.join(' -> ');
}
traverse(root);
Шаг 3
И меняем очередь на стек! Опять же, решение с рекурсией гораздо нагляднее. Оно короче и понятнее, чем использование стека. Да и по памяти решение со стеком не выигрывает. Однако, во-первых, рекурсия может перестать вас устраивать в условиях «высокого» дерева. А во-вторых, просто забавно посмотреть, как алгоритмы получаются друг из друга и при небольших отличиях начинают работать оптимальнее с разными наборами данных.
const root = document.body;
const resultElement = document.getElementById('result');
function traverse(node) {
const result = [];
// очередь для просмотра нод
const queue = [];
// куда мы сразу же кладем ноду, с которой будем начинать обход
queue.push(node);
// обходим, пока в стеке что-то есть
while(queue.length) {
// получаем ноду из стека
const currentNode = queue.pop();
// делаем всё, что нужно с нашей нодой (сейчас это просто перекладывание ноды в список просмотренных)
result.push(currentNode.localName);
// и кладем в очередь всех потомков нашей ноды.
// из-за того, что мы будем класть потомков в конец и доставать из начала, мы гарантированно просмотрим все ноды текущего уровня перед спуском на следующий
queue.push(...currentNode.children);
}
resultElement.innerHTML = result.join(' -> ');
}
traverse(root);