数组扁平化 flat
问题
手写实现数组扁平化(flat),支持指定展开深度。
答案
数组扁平化是将多维数组转换为一维数组(或指定深度)的操作。
基础实现
递归实现
function flat<T>(arr: (T | T[])[], depth = 1): T[] {
if (depth <= 0) {
return arr.slice() as T[];
}
const result: T[] = [];
for (const item of arr) {
if (Array.isArray(item)) {
result.push(...flat(item, depth - 1));
} else {
result.push(item);
}
}
return result;
}
// 测试
const nested = [1, [2, [3, [4, [5]]]]];
console.log(flat(nested, 1)); // [1, 2, [3, [4, [5]]]]
console.log(flat(nested, 2)); // [1, 2, 3, [4, [5]]]
console.log(flat(nested, Infinity)); // [1, 2, 3, 4, 5]
reduce 实现
function flatReduce<T>(arr: (T | T[])[], depth = 1): T[] {
return depth > 0
? arr.reduce<T[]>((acc, item) => {
if (Array.isArray(item)) {
acc.push(...flatReduce(item, depth - 1));
} else {
acc.push(item);
}
return acc;
}, [])
: (arr.slice() as T[]);
}
concat + apply(简洁版)
// 只能展开一层
function flatOnce<T>(arr: (T | T[])[]): T[] {
return ([] as T[]).concat(...arr);
}
// 或者用 apply
function flatOnceApply<T>(arr: (T | T[])[]): T[] {
return Array.prototype.concat.apply([], arr) as T[];
}
// 测试
console.log(flatOnce([1, [2, 3], [4, [5]]])); // [1, 2, 3, 4, [5]]
完全展平(无限深度)
递归方式
function flatDeep<T>(arr: unknown[]): T[] {
const result: T[] = [];
for (const item of arr) {
if (Array.isArray(item)) {
result.push(...flatDeep<T>(item));
} else {
result.push(item as T);
}
}
return result;
}
// 简洁版
function flatDeepConcise<T>(arr: unknown[]): T[] {
return arr.reduce<T[]>((acc, item) => {
return acc.concat(Array.isArray(item) ? flatDeepConcise(item) : (item as T));
}, []);
}
迭代方式(栈)
function flatIterative<T>(arr: unknown[]): T[] {
const stack = [...arr];
const result: T[] = [];
while (stack.length) {
const item = stack.pop();
if (Array.isArray(item)) {
// 展开数组,保持顺序
stack.push(...item);
} else {
result.push(item as T);
}
}
// 因为是 pop,需要反转
return result.reverse();
}
使用 flat 字符串转换(技巧)
// 仅适用于简单类型数组
function flatByString(arr: number[]): number[] {
return arr
.toString()
.split(',')
.filter(Boolean)
.map(Number);
}
// 测试
console.log(flatByString([1, [2, [3, [4]]]])); // [1, 2, 3, 4]
// 缺点:只能处理数字,无法处理字符串、对象等
Generator 实现
function* flatGenerator<T>(arr: unknown[], depth = Infinity): Generator<T> {
for (const item of arr) {
if (Array.isArray(item) && depth > 0) {
yield* flatGenerator<T>(item, depth - 1);
} else {
yield item as T;
}
}
}
// 使用
const arr = [1, [2, [3, [4]]]];
console.log([...flatGenerator<number>(arr)]); // [1, 2, 3, 4]
console.log([...flatGenerator<number>(arr, 1)]); // [1, 2, [3, [4]]]
扩展功能
带过滤的扁平化
function flatFilter<T>(
arr: unknown[],
predicate: (item: T) => boolean,
depth = Infinity
): T[] {
const result: T[] = [];
function traverse(items: unknown[], d: number) {
for (const item of items) {
if (Array.isArray(item) && d > 0) {
traverse(item, d - 1);
} else if (predicate(item as T)) {
result.push(item as T);
}
}
}
traverse(arr, depth);
return result;
}
// 示例:扁平化并过滤偶数
const arr = [1, [2, [3, [4, [5]]]]];
console.log(flatFilter<number>(arr, (n) => n % 2 === 0)); // [2, 4]
带映射的扁平化(flatMap)
function flatMap<T, U>(
arr: T[],
callback: (item: T, index: number, array: T[]) => U | U[]
): U[] {
const result: U[] = [];
arr.forEach((item, index) => {
const mapped = callback(item, index, arr);
if (Array.isArray(mapped)) {
result.push(...mapped);
} else {
result.push(mapped);
}
});
return result;
}
// 示例
const sentences = ['Hello World', 'Flat Map'];
console.log(flatMap(sentences, (s) => s.split(' ')));
// ['Hello', 'World', 'Flat', 'Map']
对象数组扁平化(展开嵌套属性)
interface TreeNode {
id: number;
name: string;
children?: TreeNode[];
}
function flattenTree(tree: TreeNode[]): Omit<TreeNode, 'children'>[] {
const result: Omit<TreeNode, 'children'>[] = [];
function traverse(nodes: TreeNode[]) {
for (const node of nodes) {
const { children, ...rest } = node;
result.push(rest);
if (children) {
traverse(children);
}
}
}
traverse(tree);
return result;
}
// 示例
const tree: TreeNode[] = [
{
id: 1,
name: 'root',
children: [
{ id: 2, name: 'child1' },
{
id: 3,
name: 'child2',
children: [{ id: 4, name: 'grandchild' }],
},
],
},
];
console.log(flattenTree(tree));
// [{ id: 1, name: 'root' }, { id: 2, name: 'child1' }, ...]
性能对比
// 性能测试
const createNestedArray = (depth: number): unknown[] => {
if (depth === 0) return [1];
return [1, createNestedArray(depth - 1)];
};
const testArr = createNestedArray(100);
console.time('递归');
flat(testArr, Infinity);
console.timeEnd('递归');
console.time('迭代');
flatIterative(testArr);
console.timeEnd('迭代');
console.time('原生 flat');
(testArr as number[]).flat(Infinity);
console.timeEnd('原生 flat');
| 方法 | 时间复杂度 | 空间复杂度 | 优点 |
|---|---|---|---|
| 递归 | ,d 为深度 | 代码简洁 | |
| 迭代(栈) | 避免栈溢出 | ||
| reduce | 函数式风格 | ||
| Generator | 惰性求值 |
常见面试问题
Q1: 实现 Array.prototype.flat
答案:
declare global {
interface Array<T> {
myFlat(depth?: number): T[];
}
}
Array.prototype.myFlat = function <T>(this: T[], depth = 1): T[] {
const result: T[] = [];
const flatten = (arr: unknown[], d: number) => {
for (const item of arr) {
if (Array.isArray(item) && d > 0) {
flatten(item, d - 1);
} else {
result.push(item as T);
}
}
};
flatten(this, depth);
return result;
};
// 测试
console.log([1, [2, [3]]].myFlat()); // [1, 2, [3]]
console.log([1, [2, [3]]].myFlat(2)); // [1, 2, 3]
Q2: flat 和 flatMap 的区别?
答案:
const arr = [1, 2, 3];
// flat:仅扁平化
console.log([[1], [2], [3]].flat()); // [1, 2, 3]
// flatMap:先 map 再 flat(1)
console.log(arr.flatMap((x) => [x, x * 2])); // [1, 2, 2, 4, 3, 6]
// 等价于
console.log(arr.map((x) => [x, x * 2]).flat()); // [1, 2, 2, 4, 3, 6]
// flatMap 只能展开一层
console.log(arr.flatMap((x) => [[x]])); // [[1], [2], [3]]
Q3: 如何处理空位?
答案:
// 原生 flat 会跳过空位
const sparse = [1, , [2, , 3]];
console.log(sparse.flat()); // [1, 2, 3]
// 手写实现
function flatSkipHoles<T>(arr: unknown[], depth = 1): T[] {
const result: T[] = [];
const flatten = (items: unknown[], d: number) => {
for (let i = 0; i < items.length; i++) {
// 跳过空位
if (!(i in items)) continue;
const item = items[i];
if (Array.isArray(item) && d > 0) {
flatten(item, d - 1);
} else {
result.push(item as T);
}
}
};
flatten(arr, depth);
return result;
}
Q4: 递归深度过大怎么办?
答案:
// 使用迭代方式避免栈溢出
function flatSafe<T>(arr: unknown[]): T[] {
const stack: unknown[] = [...arr];
const result: T[] = [];
while (stack.length) {
const item = stack.shift(); // 使用 shift 保持顺序
if (Array.isArray(item)) {
stack.unshift(...item);
} else {
result.push(item as T);
}
}
return result;
}
// 或者使用尾递归优化(需要支持 TCO 的环境)
function flatTCO<T>(arr: unknown[], result: T[] = []): T[] {
for (const item of arr) {
if (Array.isArray(item)) {
flatTCO(item, result);
} else {
result.push(item as T);
}
}
return result;
}
Q5: 如何实现一个支持指定深度的 flat?Infinity 怎么处理?
答案:
核心思路是通过 depth 参数控制递归展开的层数。每递归一层,depth 减 1,当 depth === 0 时停止展开,直接将元素原样放入结果数组。Infinity 作为特殊值,由于 Infinity - 1 仍然是 Infinity,因此可以实现无限展开,与 Array.prototype.flat(Infinity) 行为一致。
/**
* 支持指定深度的 flat 实现
* @param arr - 待扁平化的数组
* @param depth - 展开深度,默认为 1,传 Infinity 表示完全展平
*/
function flat<T>(arr: (T | T[])[], depth: number = 1): T[] {
// depth 为 0 时不再展开,直接返回浅拷贝
if (depth <= 0) {
return arr.slice() as T[];
}
const result: T[] = [];
for (const item of arr) {
if (Array.isArray(item)) {
// depth - 1 控制递归深度;Infinity - 1 === Infinity,天然支持无限展开
result.push(...flat(item, depth - 1));
} else {
result.push(item);
}
}
return result;
}
// 测试:对标 Array.prototype.flat 的行为
const nested = [1, [2, [3, [4, [5]]]]];
// 默认展开 1 层
console.log(flat(nested)); // [1, 2, [3, [4, [5]]]]
console.log(nested.flat()); // [1, 2, [3, [4, [5]]]](原生行为一致)
// 指定深度
console.log(flat(nested, 2)); // [1, 2, 3, [4, [5]]]
console.log(flat(nested, 3)); // [1, 2, 3, 4, [5]]
// depth 为 0,不展开
console.log(flat(nested, 0)); // [1, [2, [3, [4, [5]]]]]
// Infinity 完全展平
console.log(flat(nested, Infinity)); // [1, 2, 3, 4, 5]
console.log(nested.flat(Infinity)); // [1, 2, 3, 4, 5](原生行为一致)
要点
depth参数默认值为1,与原生Array.prototype.flat()保持一致Infinity - 1 === Infinity是 JavaScript 的特性,无需对Infinity做特殊分支处理depth <= 0时返回数组的浅拷贝(arr.slice()),而非原数组引用,与原生行为一致
Q6: 除了递归,还有哪些方式实现数组扁平化?各有什么优缺点?
答案:
主要有以下 5 种实现方式,适用场景和局限性各不相同:
1. toString + split(仅适合纯数字数组)
function flatByString(arr: number[]): number[] {
return arr
.toString() // "1,2,3,4,5"
.split(',')
.filter(Boolean)
.map(Number);
}
console.log(flatByString([1, [2, [3, [4]]]])); // [1, 2, 3, 4]
// 致命缺陷:无法处理字符串、对象、布尔值等
// flatByString(['a', ['b']]) → [NaN, NaN] ❌
2. reduce + concat(递归变体)
function flatReduce<T>(arr: (T | T[])[], depth: number = 1): T[] {
return depth > 0
? arr.reduce<T[]>(
(acc, item) =>
acc.concat(
Array.isArray(item) ? flatReduce(item, depth - 1) : item
),
[]
)
: (arr.slice() as T[]);
}
console.log(flatReduce([1, [2, [3]]], Infinity)); // [1, 2, 3]
3. 栈/队列迭代(避免栈溢出)
function flatStack<T>(arr: unknown[]): T[] {
const stack = [...arr];
const result: T[] = [];
while (stack.length) {
const item = stack.pop();
if (Array.isArray(item)) {
stack.push(...item); // 展开放回栈中
} else {
result.push(item as T);
}
}
return result.reverse(); // pop 导致逆序,需要反转
}
console.log(flatStack<number>([1, [2, [3, [4]]]])); // [1, 2, 3, 4]
4. Generator 惰性求值
function* flatGen<T>(arr: unknown[], depth: number = Infinity): Generator<T> {
for (const item of arr) {
if (Array.isArray(item) && depth > 0) {
yield* flatGen<T>(item, depth - 1);
} else {
yield item as T;
}
}
}
console.log([...flatGen<number>([1, [2, [3]]])]); // [1, 2, 3]
5. 展开运算符循环检测
function flatSpread<T>(arr: unknown[]): T[] {
let result = [...arr];
// 只要数组中还有嵌套数组,就继续展开一层
while (result.some(Array.isArray)) {
result = ([] as unknown[]).concat(...result);
}
return result as T[];
}
console.log(flatSpread<number>([1, [2, [3, [4]]]])); // [1, 2, 3, 4]
对比总结:
| 方式 | 支持 depth | 支持所有类型 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|---|---|
toString + split | 不支持 | 仅数字 | 代码极简 | 只能处理数字,实用性极低 | ||
reduce + concat | 支持 | 支持 | 函数式风格,支持 depth | 本质还是递归,深层嵌套会栈溢出 | ||
| 栈/队列迭代 | 不支持(完全展平) | 支持 | 避免栈溢出,适合极深嵌套 | 不支持指定深度,需要 reverse | ||
| Generator | 支持 | 支持 | 惰性求值,支持 depth | 语法较新,性能略低于普通递归 | ||
| 展开运算符循环 | 不支持(完全展平) | 支持 | 直观易懂 | 每次循环创建新数组,性能最差 |
面试建议
- 面试手写首选 递归实现(简洁、支持 depth),并能口述迭代方案作为优化
toString + split方案只在面试中作为"你还知道哪些方式"的补充提及即可,要主动指出其局限性- 如果面试官追问"极深嵌套怎么办",切换到栈迭代方案