Рекурсивные обходы
Другим отличным применением рекурсии является рекурсивный обход.
Представьте, у нас есть компания. Структура персонала может быть представлена как объект:
let company = {
sales: [{
name: 'John',
salary: 1000
}, {
name: 'Alice',
salary: 600
}],
development: {
sites: [{
name: 'Peter',
salary: 2000
}, {
name: 'Alex',
salary: 1800
}],
internals: [{
name: 'Jack',
salary: 1300
}]
}
};
Другими словами, в компании есть отделы.
- Отдел может состоять из массива работников. Например, в отделе
sales
работают 2 сотрудника: Джон и Алиса. - Или отдел может быть разделён на подотделы, например, отдел
development
состоит из подотделов:sites
иinternals
. В каждом подотделе есть свой персонал. - Также возможно, что при росте подотдела он делится на подразделения (или команды).Например, подотдел
sites
в будущем может быть разделён на командыsiteA
иsiteB
. И потенциально они могут быть разделены ещё. Этого нет на картинке, просто нужно иметь это в виду.
Теперь, допустим, нам нужна функция для получения суммы всех зарплат. Как мы можем это сделать?
Итеративный подход не прост, потому что структура довольно сложная. Первая идея заключается в том, чтобы сделать цикл for
поверх объекта company
с вложенным циклом над отделами 1-го уровня вложенности. Но затем нам нужно больше вложенных циклов для итераций над сотрудниками отделов второго уровня, таких как sites
… А затем ещё один цикл по отделам 3-го уровня, которые могут появиться в будущем? Если мы поместим в код 3-4 вложенных цикла для обхода одного объекта, то это будет довольно некрасиво.
Давайте попробуем рекурсию.
Как мы видим, когда наша функция получает отдел для подсчёта суммы зарплат, есть два возможных случая:
- Либо это «простой» отдел с массивом – тогда мы сможем суммировать зарплаты в простом цикле.
- Или это объект с
N
подотделами – тогда мы можем сделатьN
рекурсивных вызовов, чтобы получить сумму для каждого из подотделов, и объединить результаты.
Случай (1), когда мы получили массив, является базой рекурсии, тривиальным случаем.
Случай (2), при получении объекта, является шагом рекурсии. Сложная задача разделяется на подзадачи для подотделов. Они могут, в свою очередь, снова разделиться на подотделы, но рано или поздно это разделение закончится, и решение сведётся к случаю (1).
Алгоритм даже проще читается в виде кода:
let company = { // тот же самый объект, сжатый для краткости
sales: [{name: 'John', salary: 1000}, {name: 'Alice', salary: 600 }],
development: {
sites: [{name: 'Peter', salary: 2000}, {name: 'Alex', salary: 1800 }],
internals: [{name: 'Jack', salary: 1300}]
}
};
// Функция для подсчёта суммы зарплат
function sumSalaries(department) {
if (Array.isArray(department)) { // случай (1)
return department.reduce((prev, current) => prev + current.salary, 0); // сумма элементов массива
} else { // случай (2)
let sum = 0;
for (let subdep of Object.values(department)) {
sum += sumSalaries(subdep); // рекурсивно вызывается для подотделов, суммируя результаты
}
return sum;
}
}
alert(sumSalaries(company)); // 6700
Код краток и прост для понимания (надеюсь?). В этом сила рекурсии. Она работает на любом уровне вложенности отделов.
Схема вызовов:
Принцип прост: для объекта {...}
используются рекурсивные вызовы, а массивы [...]
являются «листьями» дерева рекурсии, они сразу дают результат.
Обратите внимание, что в коде используются возможности, о которых мы говорили ранее:
- Метод
arr.reduce
из главы Методы массивов для получения суммы элементов массива. - Цикл
for(val of Object.values(obj))
для итерации по значениям объекта:Object.values
возвращает массив значений.
Рекурсивные структуры
Рекурсивная (рекурсивно определяемая) структура данных – это структура, которая повторяет саму себя в своих частях.
Мы только что видели это на примере структуры компании выше.
Отдел компании – это:
- Либо массив людей.
- Либо объект с отделами.
Для веб-разработчиков существуют гораздо более известные примеры: HTML- и XML-документы.
В HTML-документе HTML-тег может содержать:
- Фрагменты текста.
- HTML-комментарии.
- Другие HTML-теги (которые, в свою очередь, могут содержать фрагменты текста/комментарии или другие теги и т.д.).
Это снова рекурсивное определение.
Для лучшего понимания мы рассмотрим ещё одну рекурсивную структуру под названием «связанный список», которая в некоторых случаях может использоваться в качестве альтернативы массиву.
Связанный список
Представьте себе, что мы хотим хранить упорядоченный список объектов.
Естественным выбором будет массив:
let arr = [obj1, obj2, obj3];
…Но у массивов есть недостатки. Операции «удалить элемент» и «вставить элемент» являются дорогостоящими. Например, операция arr.unshift(obj)
должна переиндексировать все элементы, чтобы освободить место для нового obj
, и, если массив большой, на это потребуется время. То же самое с arr.shift()
.
Единственные структурные изменения, не требующие массовой переиндексации – это изменения, которые выполняются с конца массива: arr.push/pop
. Таким образом, массив может быть довольно медленным для больших очередей, когда нам приходится работать с его началом.
Или же, если нам действительно нужны быстрые вставка/удаление, мы можем выбрать другую структуру данных, называемую связанный список.
Элемент связанного списка определяется рекурсивно как объект с:
value
,next
– свойство, ссылающееся на следующий элемент связанного списка илиnull
, если это последний элемент.
Пример:
let list = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: {
value: 4,
next: null
}
}
}
};
Графическое представление списка:
Альтернативный код для создания:
let list = { value: 1 };
list.next = { value: 2 };
list.next.next = { value: 3 };
list.next.next.next = { value: 4 };
Здесь мы можем ещё лучше увидеть, что есть несколько объектов, каждый из которых имеет value
и next
, указывающий на соседа. Переменная list
является первым объектом в цепочке, поэтому, следуя по указателям next
из неё, мы можем попасть в любой элемент.
Список можно легко разделить на несколько частей и впоследствии объединить обратно:
let secondList = list.next.next;
list.next.next = null;
Для объединения:
list.next.next = secondList;
И, конечно, мы можем вставить или удалить элементы из любого места.
Например, для добавления нового элемента нам нужно обновить первый элемент списка:
let list = { value: 1 };
list.next = { value: 2 };
list.next.next = { value: 3 };
list.next.next.next = { value: 4 };
// добавление нового элемента в список
list = { value: "new item", next: list };
Чтобы удалить элемент из середины списка, нужно изменить значение next
предыдущего элемента:
list.next = list.next.next;
list.next
перепрыгнуло с 1
на значение 2
. Значение 1
теперь исключено из цепочки. Если оно не хранится где-нибудь ещё, оно будет автоматически удалено из памяти.
В отличие от массивов, нет перенумерации, элементы легко переставляются.
Естественно, списки не всегда лучше массивов. В противном случае все пользовались бы только списками.
Главным недостатком является то, что мы не можем легко получить доступ к элементу по его индексу. В простом массиве: arr[n]
является прямой ссылкой. Но в списке мы должны начать с первого элемента и перейти в next
N раз, чтобы получить N-й элемент.
…Но нам не всегда нужны такие операции. Например, нам может быть нужна очередь или даже двухсторонняя очередь – это упорядоченная структура, которая позволяет очень быстро добавлять/удалять элементы с обоих концов, но там не нужен доступ в середину.
Списки могут быть улучшены:
- Можно добавить свойство
prev
в дополнение кnext
для ссылки на предыдущий элемент, чтобы легко двигаться по списку назад. - Можно также добавить переменную
tail
, которая будет ссылаться на последний элемент списка (и обновлять её при добавлении/удалении элементов с конца). - …Возможны другие изменения: главное, чтобы структура данных соответствовала нашим задачам с точки зрения производительности и удобства.
Итого
Термины:
- Рекурсия – это термин в программировании, означающий вызов функцией самой себя. Рекурсивные функции могут быть использованы для элегантного решения определённых задач.Когда функция вызывает саму себя, это называется шагом рекурсии. База рекурсии – это такие аргументы функции, которые делают задачу настолько простой, что решение не требует дальнейших вложенных вызовов.
- Рекурсивно определяемая структура данных – это структура данных, которая может быть определена с использованием самой себя.Например, связанный список может быть определён как структура данных, состоящая из объекта, содержащего ссылку на список (или null).
list = { value, next -> list }
Деревья, такие как дерево HTML-элементов или дерево отделов из этой главы, также являются рекурсивными: они разветвляются, и каждая ветвь может содержать другие ветви.Как мы видели в примереsumSalary
, рекурсивные функции могут быть использованы для прохода по ним.
Любая рекурсивная функция может быть переписана в итеративную. И это иногда требуется для оптимизации работы. Но для многих задач рекурсивное решение достаточно быстрое и простое в написании и поддержке.