«Расплющивание» массива

Дан массив:

const arr = [1, [2, [3, [4,5]]]]

Необходимо написать функцию, которая принимает в аргументах многомерный массив неограниченной вложенности и возвращает одномерный массив, состоящий из элементов со всех уровней вложенности исходного массива.

Примеры:

flat([1, [2, [3, [4,5]]]]); // => [1, 2, 3, 4, 5]
flat([1, [2, [3, [4,5,[6,[7]]]]]]); // => [1, 2, 3, 4, 5, 6, 7]

Теория и алгоритм

Многомерный массив можно представить как дерево: узлы — это массивы, а «листья» — это не-массивные значения (числа, строки, объекты и т.д.). Задача «сделать плоский массив» означает выполнить обход этого дерева слева направо и собрать все листья в один линейный список.

На практике чаще всего используется обход в глубину (DFS):

  • Если встречается массив, требуется зайти внутрь и обработать элементы по порядку.
  • Если встречается не-массив, требуется добавить значение в результат.

Базовая схема (псевдокод) выглядит так:

walk(value):
  если value — массив:
    для каждого элемента item в value:
      walk(item)
  иначе:
    добавить value в result

По сложности:

  • По времени все корректные реализации работают за O(n), где n — общее количество элементов на всех уровнях (каждый элемент посещается один раз).
  • По памяти: результат занимает O(n); дополнительная память зависит от подхода, рекурсия требует глубины стека O(d), итеративный стек также хранит до O(d) (и иногда больше, в зависимости от формы вложенности).

Практические детали и ошибки

Частая ошибка — «раскрыть» массив только на один уровень (например, через поверхностное объединение), хотя по условию требуется раскрывать до конца, независимо от глубины. Это означает, что проверка и обработка должны повторяться, пока внутри не останутся только не-массивные элементы.

Ещё одна ошибка — неверная проверка «это массив или нет». Для этой задачи корректная проверка — Array.isArray(value), так как:

  • typeof [] возвращает "object", поэтому typeof не подходит.
  • Строки и обычные объекты не должны раскрыться как массив, иначе будет ломаться смысл задачи.

Полезно заранее определить, что считается элементом:

  • Если внутри встречаются null и undefined, они должны попадать в результат как обычные значения (если не задано иное).
  • Если внутри встречаются функции или объекты, они также должны попадать в результат как есть (если не задано иное).

Небольшая таблица выбора подхода:

ПодходПлюсыМинусыКогда уместен
РекурсияСамая простая для пониманияВозможен переполненный стек при большой глубинеУчебные задачи, обычные входные данные
Итеративно (стек)Устойчиво к большой глубинеКод сложнее рекурсииОчень глубокие структуры, «защитный» код
ГенераторУдобно отделять обход и потреблениеМожет быть рекурсивным и иметь те же рискиЗнакомство с yield, большие наборы данных
flat(Infinity)Самое короткое решениеНужна поддержка среды или полифилСовременные браузеры/Node.js, без требований к старым

Кратко: задача решается обходом вложенной структуры; наиболее понятный вариант — рекурсивный DFS, наиболее устойчивый к огромной глубине — итеративный вариант со стеком, а в современных средах самый короткий путь — arr.flat(Infinity) (если доступен).