最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
列表转树:高频前端面试真题
时间:2026-07-10 11:21:01 编辑:袖梨 来源:一聚教程网
列表转树:高频前端面试题
列表转树:为什么它是前端高频题
后台返回的数据通常是扁平的,但前端渲染菜单、组织架构、评论回复时,需要的是树形结构。列表转树(list2tree)就是把这样的数据:

const flatList = [{ id: 1, name: '一级菜单A', parentId: 0 },{ id: 2, name: '一级菜单B', parentId: 0 },{ id: 3, name: '二级A-1', parentId: 1 },{ id: 4, name: '二级B-1', parentId: 2 },{ id: 5, name: '三级A-1-1', parentId: 3 }];
转成这样的嵌套结构:
一级菜单A└── 二级A-1└── 三级A-1-1一级菜单B└── 二级B-1
这里的关键字段是 parentId:它记录了每个节点的父节点是谁。parentId === 0 表示这个节点没有父节点,也就是根节点。
基础实现:用 Map 建立父子关系
最直观的思路是两次遍历:
- 第一次遍历:用
Map把每个id映射到节点对象,同时给每个节点初始化一个children数组; - 第二次遍历:根据
parentId把当前节点挂到父节点的children里。
function list2Tree(list) {const map = new Map();const tree = [];// 第一步:建立 id -> 节点的映射list.forEach(item => {map.set(item.id, { ...item, children: [] });});// 第二步:把每个节点挂到父节点下list.forEach(item => {const current = map.get(item.id);const parent = map.get(item.parentId);if (parent) {parent.children.push(current);}if (item.parentId === 0) {tree.push(current);}});return tree;}
用 Map 的好处是查找父节点的时间复杂度是 O(1),整体两次遍历下来,时间复杂度是 O(n),比嵌套循环查找父节点高效得多。
更优雅的写法:用 reduce 一次聚合
在写 list2tree 之前,先回顾一下 reduce 这个方法。它是数组的高阶函数之一,作用是把一个数组"归约"成单个值。基本签名长这样:
arr.reduce((accumulator, currentItem, currentIndex, originalArray) => {// 返回新的 accumulator}, initialValue);
accumulator:累积值,也就是上一次回调返回的结果;currentItem:当前遍历到的元素;currentIndex:当前元素的索引;originalArray:原数组本身;initialValue:累积值的初始值,可选,但通常建议显式传入。
看个最简单的例子,求和:
const nums = [1, 2, 3, 4];const sum = nums.reduce((acc, cur) => acc + cur, 0);console.log(sum); // 10
这里的 acc 一开始是 0,然后依次变成 1、3、6、10。每次回调把当前元素加到累积值上,最后返回总和。
reduce 的强大之处在于,累积值不一定是数字,也可以是对象、数组、Map 等任何数据结构。这让它非常适合"把数组聚合成另一个数据结构"的场景,比如把列表聚合成树。
如果熟悉 reduce,可以把上面的 list2tree 逻辑写得更紧凑:
function list2Tree(list) {const nodeMap = list.reduce((map, item) => {map[item.id] = { ...item, children: [] };return map;}, {});return list.reduce((tree, item) => {const node = nodeMap[item.id];const parent = nodeMap[item.parentId];if (item.parentId === 0) {tree.push(node);} else {parent.children.push(node);}return tree;}, []);}
两种写法的核心思路完全一致,区别只在于用 forEach 还是用 reduce。reduce 更适合"把数组聚合成另一个数据结构"的场景;forEach 则更容易阅读,适合对代码可读性要求更高的项目。
列表转树与递归的关系
list2tree 本身没有显式递归,但它处理的问题本质上是树形的。每个节点知道自己的孩子,孩子又知道自己的孩子,这就形成了和递归遍历相同的结构。
这也是为什么面试官喜欢考这道题:它看起来是数组操作,实际上考察的是对"树形结构"和"哈希映射"的理解。写出 O(n) 的解法,说明你已经能把"找父节点"这个重复操作优化成一次查找。
进入 AI 部分:用 Prompt 做 NLP 翻译任务
列表转树练的是基础数据结构能力,而 NLP 翻译任务练的是如何把大模型能力封装成可复用的工程模块。
这节课的项目结构和之前情感分析、信息提取的示例保持一致:
| 文件 | 职责 |
|---|---|
client.mjs | 提供 LLM Client 对象,隔离配置 |
completions.mjs | 封装调用逻辑,提供 getCompletions 方法 |
index.mjs / main.mjs | 业务入口,写具体 Prompt 和调用流程 |
client.mjs:把配置隔离出去
import 'dotenv/config';import OpenAI from 'openai';const client = new OpenAI({apiKey: process.env.DASHSCOPE_API_KEY,baseURL: process.env.DASHSCOPE_BASE_URL,model: process.env.DASHSCOPE_MODEL});export default client;
把 API Key、Base URL、模型名放到环境变量里,代码本身不暴露敏感信息。这是从"能跑"到"能上线"的第一步。
completions.mjs:把调用逻辑封装成函数
import client from './client.mjs';export async function getCompletions(prompt) {const response = await client.chat.completions.create({model: process.env.DASHSCOPE_MODEL,messages: [{ role: 'user', content: prompt }]});return response.choices[0].message.content;}
getCompletions 只做一件事:接收一个 Prompt,返回模型输出。后续情感分析、翻译、总结都可以复用它。
翻译任务的多种形态
这节课重点讲了翻译(Translation),它是 NLP 中最常见、也最容易落地的任务之一。但翻译不是简单地把 A 语言变成 B 语言,它有很多细分形态。
1. 中英互译
const prompt = `将以下三个反引号中的中文翻译成西班牙语:```您好,我想订购一个搅拌机。````;
2. 语种识别
const prompt = `请告诉我以下文本是什么语种:```Combien coûte le lampadaire?````;
3. 多目标语言翻译
const prompt = `请将以下文本分别翻译成中文、英文、法语和西班牙语:```I want to order a basketball.````;
4. 语气转换
const prompt = `请将以下文本翻译成中文,分别展示正式语气和非正式语气:```Would you like to order a pillow?````;
同一个英文句子,翻译结果会根据语气场景变化。这说明 Prompt 不仅要说明"翻成什么语言",还要说明"用什么风格"。
实战:批量识别语种并翻译
课程里的 index.mjs 实现了一个通用翻译器:先识别输入文本的语种,再把它翻译成中文和英文。
const user_messages = ["La performance du système est plus lente que d'habitude.","Mi monitor tiene píxeles que no se iluminan.","Il mio mouse non funziona","Mój klawisz Ctrl jest zepsuty","我的屏幕在闪烁"];const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));const main = async () => {for (let message of user_messages) {await sleep(1000);const prompt1 = `告诉我以下文本是什么语种,直接输出语种,如法语,无需输出标点符号:```${message}````;const lang = await getCompletions(prompt1);console.log(lang, "n");const prompt2 = `请将以下文本分别翻译成中文和英文,并写成中文:xxx英文:xxx的格式:```${message}````;const result = await getCompletions(prompt2);console.log(result, "================");}};main();
这里有两个值得注意的点:
- 输出格式约束:翻译结果要求写成"中文:xxx / 英文:xxx"的固定格式,方便后续代码解析;
- 批量调用时加入延迟:
sleep(1000)避免短时间内高频请求触发限流。
大模型接口调用的意义:从"会用"到"能封装"
这节课让我意识到,调用大模型和调用普通 HTTP 接口的本质区别不大,但工程化思路上有很大不同。
普通接口的输入输出通常是结构化的:传什么参数、返回什么字段,文档写得很清楚。而大模型接口的输入是"自然语言 Prompt",输出也是自然语言。这就需要我们多做一层"约束":
- 用角色设定控制回答风格;
- 用 Few-shot 示例固定输出格式;
- 用 JSON / 固定分隔符让输出可被程序解析;
- 把调用逻辑封装成
getCompletions这样的函数,方便复用。
当这些封装做得足够好时,我们就可以像搭积木一样,把"翻译"、"总结"、"情感分析"这些能力组合成更复杂的业务系统。
我现在怎么理解这节课
这节课把两个看起来不相关的内容放在了一起:前端面试题 list2tree 和 AI 翻译任务。
list2tree 让我重新理解了"把扁平结构变成树形结构"这件事。它考察的不是你会不会写递归,而是你能不能通过哈希映射把 O(n²) 的嵌套查找优化成 O(n)。这种"用空间换时间"的思路,在面试和工程里都很常见。
而 NLP 翻译任务让我看到,AI 能力的落地不只是"调一个 API",而是要把 Prompt 设计、调用封装、输出解析、批量调度这些环节串起来。client.mjs 管配置,completions.mjs 管调用,业务文件专注写 Prompt——这种分层结构,就是从小 demo 走向可维护项目的关键。
两部分的共同点在于:好的代码结构,能让复杂问题变得可管理。无论是把数组转成树,还是把大模型能力封装成业务接口,最终目的都是让下一层调用者不必关心实现细节。