«Расплющивание» массива
Дан массив:
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) (если доступен).