algorithm2026

8 阅读8分钟
function arrayToTree(arr, parentId = null) {
  const map = new Map();
  const result = [];
  
  // 第一次遍历:创建映射表
  for (const item of arr) {
    map.set(item.id, { ...item, children: [] });
  }
  
  // 第二次遍历:建立父子关系
  for (const item of arr) {
    const node = map.get(item.id);
    
    if (item.parentId === parentId) {
      // 根节点
      result.push(node);
    } else {
      // 子节点
      const parent = map.get(item.parentId);
      if (parent) {
        parent.children.push(node);
      }
    }
  }
  
  return result;
}

// 测试数据
const flatArray = [
  { id: 1, name: '根节点1', parentId: null },
  { id: 2, name: '子节点1-1', parentId: 1 },
  { id: 3, name: '子节点1-2', parentId: 1 },
  { id: 4, name: '根节点2', parentId: null },
  { id: 5, name: '子节点1-1-1', parentId: 2 },
  { id: 6, name: '子节点2-1', parentId: 4 }
];

console.log(JSON.stringify(arrayToTree(flatArray), null, 2));

数组转树执行过程详解

📊 输入数据分析

JavaScript
const flatArray = [
  { id: 1, name: '根节点1', parentId: null },
  { id: 2, name: '子节点1-1', parentId: 1 },
  { id: 3, name: '子节点1-2', parentId: 1 },
  { id: 4, name: '根节点2', parentId: null },
  { id: 5, name: '子节点1-1-1', parentId: 2 },
  { id: 6, name: '子节点2-1', parentId: 4 }
];

数据结构分析:

  • 2个根节点: id=1, id=4 (parentId为null)
  • 4个子节点: id=2,3,5,6
  • 层级关系: 1→2→5, 1→3, 4→6

🔄 第一次遍历:创建映射表

JavaScript
// 第一次遍历:创建映射表
for (const item of arr) {
  map.set(item.id, { ...item, children: [] });
}

执行过程:

JavaScript
// 处理 { id: 1, name: '根节点1', parentId: null }
map.set(1, { id: 1, name: '根节点1', parentId: null, children: [] });

// 处理 { id: 2, name: '子节点1-1', parentId: 1 }
map.set(2, { id: 2, name: '子节点1-1', parentId: 1, children: [] });

// 处理 { id: 3, name: '子节点1-2', parentId: 1 }
map.set(3, { id: 3, name: '子节点1-2', parentId: 1, children: [] });

// 处理 { id: 4, name: '根节点2', parentId: null }
map.set(4, { id: 4, name: '根节点2', parentId: null, children: [] });

// 处理 { id: 5, name: '子节点1-1-1', parentId: 2 }
map.set(5, { id: 5, name: '子节点1-1-1', parentId: 2, children: [] });

// 处理 { id: 6, name: '子节点2-1', parentId: 4 }
map.set(6, { id: 6, name: '子节点2-1', parentId: 4, children: [] });

第一次遍历后的Map状态:

JavaScript
Map {
  1 => { id: 1, name: '根节点1', parentId: null, children: [] },
  2 => { id: 2, name: '子节点1-1', parentId: 1, children: [] },
  3 => { id: 3, name: '子节点1-2', parentId: 1, children: [] },
  4 => { id: 4, name: '根节点2', parentId: null, children: [] },
  5 => { id: 5, name: '子节点1-1-1', parentId: 2, children: [] },
  6 => { id: 6, name: '子节点2-1', parentId: 4, children: [] }
}

🌳 第二次遍历:建立父子关系

JavaScript
// 第二次遍历:建立父子关系
for (const item of arr) {
  const node = map.get(item.id);
  
  if (item.parentId === parentId) {  // parentId = null
    result.push(node);
  } else {
    const parent = map.get(item.parentId);
    if (parent) {
      parent.children.push(node);
    }
  }
}

逐步执行过程:

步骤1: 处理 id=1
JavaScript
// item = { id: 1, name: '根节点1', parentId: null }
const node = map.get(1);  // 获取节点1
// item.parentId (null) === parentId (null) ✅
result.push(node);  // 添加到根节点数组

// result = [节点1]
步骤2: 处理 id=2
JavaScript
// item = { id: 2, name: '子节点1-1', parentId: 1 }
const node = map.get(2);  // 获取节点2
// item.parentId (1) === parentId (null) ❌
const parent = map.get(1);  // 获取父节点1
parent.children.push(node);  // 节点2添加到节点1的children

// 节点1.children = [节点2]
步骤3: 处理 id=3
JavaScript
// item = { id: 3, name: '子节点1-2', parentId: 1 }
const node = map.get(3);  // 获取节点3
// item.parentId (1) === parentId (null) ❌
const parent = map.get(1);  // 获取父节点1
parent.children.push(node);  // 节点3添加到节点1的children

// 节点1.children = [节点2, 节点3]
步骤4: 处理 id=4
JavaScript
// item = { id: 4, name: '根节点2', parentId: null }
const node = map.get(4);  // 获取节点4
// item.parentId (null) === parentId (null) ✅
result.push(node);  // 添加到根节点数组

// result = [节点1, 节点4]
步骤5: 处理 id=5
JavaScript
// item = { id: 5, name: '子节点1-1-1', parentId: 2 }
const node = map.get(5);  // 获取节点5
// item.parentId (2) === parentId (null) ❌
const parent = map.get(2);  // 获取父节点2
parent.children.push(node);  // 节点5添加到节点2的children

// 节点2.children = [节点5]
步骤6: 处理 id=6
JavaScript
// item = { id: 6, name: '子节点2-1', parentId: 4 }
const node = map.get(6);  // 获取节点6
// item.parentId (4) === parentId (null) ❌
const parent = map.get(4);  // 获取父节点4
parent.children.push(node);  // 节点6添加到节点4的children

// 节点4.children = [节点6]

🎯 最终结果

树形结构:

JavaScript
[
  {
    id: 1,
    name: '根节点1',
    parentId: null,
    children: [
      {
        id: 2,
        name: '子节点1-1',
        parentId: 1,
        children: [
          {
            id: 5,
            name: '子节点1-1-1',
            parentId: 2,
            children: []
          }
        ]
      },
      {
        id: 3,
        name: '子节点1-2',
        parentId: 1,
        children: []
      }
    ]
  },
  {
    id: 4,
    name: '根节点2',
    parentId: null,
    children: [
      {
        id: 6,
        name: '子节点2-1',
        parentId: 4,
        children: []
      }
    ]
  }
]

可视化树形结构:

根节点1 (id: 1)
├── 子节点1-1 (id: 2)
│   └── 子节点1-1-1 (id: 5)
└── 子节点1-2 (id: 3)

根节点2 (id: 4)
└── 子节点2-1 (id: 6)

📊 执行统计

处理统计:

  • 总节点数: 6个
  • 根节点数: 2个 (id: 1, 4)
  • 子节点数: 4个 (id: 2, 3, 5, 6)
  • 最大深度: 3层 (1→2→5)

Map操作统计:

  • set操作: 6次 (第一次遍历)
  • get操作: 10次 (第二次遍历中的node获取 + parent获取)

数组操作统计:

  • result.push: 2次 (添加根节点)
  • children.push: 4次 (添加子节点)

🔍 关键点总结

  1. Map的作用: 提供O(1)的节点查找,避免嵌套循环

  2. 两次遍历的必要性:

    • 第一次确保所有节点都存在于Map中
    • 第二次建立引用关系时不会出现找不到父节点的情况
  3. 引用传递: Map中存储的是对象引用,修改children时直接影响最终结果

  4. 顺序无关性: 由于使用Map映射,原数组的顺序不影响最终树形结构

这就是整个数组转树的完整执行过程!

JavaScript 引用传递详解

🔍 核心概念

在 JavaScript 中,对象是通过引用传递的,这意味着变量存储的不是对象本身,而是指向对象在内存中位置的"地址"。

📊 内存模型图解

基本类型 vs 引用类型

JavaScript
// 基本类型 - 值传递
let a = 5;
let b = a;  // 复制值
b = 10;
console.log(a); // 5 (不受影响)

// 引用类型 - 引用传递  
let obj1 = { name: 'Alice' };
let obj2 = obj1;  // 复制引用地址
obj2.name = 'Bob';
console.log(obj1.name); // 'Bob' (受影响!)

内存示意图

基本类型:
a[内存地址1: 5]
b[内存地址2: 10]  // 独立的内存空间

引用类型:
obj1[引用地址][内存中的对象: {name: 'Bob'}]
obj2[相同引用地址]// 指向同一个对象

🌳 数组转树中的引用传递

关键代码分析

JavaScript
// 第一次遍历:创建映射表
for (const item of arr) {
  map.set(item.id, { ...item, children: [] });
}

// 第二次遍历:建立父子关系
for (const item of arr) {
  const node = map.get(item.id);        // ← 获取对象引用
  
  if (item.parentId === parentId) {
    result.push(node);                  // ← 将引用添加到result
  } else {
    const parent = map.get(item.parentId); // ← 获取父节点引用
    if (parent) {
      parent.children.push(node);       // ← 修改父节点的children
    }
  }
}

🔬 详细执行过程

步骤1: 创建对象并存储引用

JavaScript
// 假设处理 { id: 1, name: '根节点1', parentId: null }
const newObj = { id: 1, name: '根节点1', parentId: null, children: [] };
map.set(1, newObj);

// 内存状态:
// map: { 1  [引用A] }
// [引用A]  { id: 1, name: '根节点1', parentId: null, children: [] }

步骤2: 获取引用并添加到result

JavaScript
const node = map.get(1);  // node 获得 [引用A]
result.push(node);        // result 中存储 [引用A]

// 内存状态:
// map: { 1 → [引用A] }
// result: [ [引用A] ]
// [引用A] → { id: 1, name: '根节点1', parentId: null, children: [] }

步骤3: 修改children属性

JavaScript
// 处理子节点 { id: 2, name: '子节点1-1', parentId: 1 }
const childNode = map.get(2);   // childNode 获得 [引用B]
const parent = map.get(1);      // parent 获得 [引用A] (同一个引用!)
parent.children.push(childNode); // 修改 [引用A] 指向的对象

// 内存状态:
// map: { 1[引用A], 2[引用B] }
// result: [ [引用A] ]  // 注意:result中的引用A指向的对象已被修改!
// [引用A] → { id: 1, name: '根节点1', parentId: null, children: [[引用B]] }
// [引用B] → { id: 2, name: '子节点1-1', parentId: 1, children: [] }

🎯 实际演示代码

完整演示

JavaScript
function demonstrateReference() {
  const map = new Map();
  const result = [];
  
  // 创建对象
  const obj1 = { id: 1, name: '节点1', children: [] };
  const obj2 = { id: 2, name: '节点2', children: [] };
  
  // 存储到Map
  map.set(1, obj1);
  map.set(2, obj2);
  
  console.log('=== 初始状态 ===');
  console.log('obj1:', obj1);
  console.log('map.get(1):', map.get(1));
  console.log('obj1 === map.get(1):', obj1 === map.get(1)); // true - 同一个引用
  
  // 添加到result
  result.push(map.get(1));
  
  console.log('\n=== 添加到result后 ===');
  console.log('result[0] === obj1:', result[0] === obj1); // true - 同一个引用
  console.log('result[0] === map.get(1):', result[0] === map.get(1)); // true
  
  // 修改children
  const parent = map.get(1);
  const child = map.get(2);
  parent.children.push(child);
  
  console.log('\n=== 修改children后 ===');
  console.log('obj1.children:', obj1.children); // 已被修改
  console.log('result[0].children:', result[0].children); // 同样被修改
  console.log('map.get(1).children:', map.get(1).children); // 同样被修改
  
  console.log('\n=== 验证引用关系 ===');
  console.log('所有引用指向同一对象:', 
    obj1 === result[0] && 
    result[0] === map.get(1) && 
    map.get(1) === parent
  ); // true
}

demonstrateReference();

输出结果

=== 初始状态 ===
obj1: { id: 1, name: '节点1', children: [] }
map.get(1): { id: 1, name: '节点1', children: [] }
obj1 === map.get(1): true

=== 添加到result后 ===
result[0] === obj1: true
result[0] === map.get(1): true

=== 修改children后 ===
obj1.children: [ { id: 2, name: '节点2', children: [] } ]
result[0].children: [ { id: 2, name: '节点2', children: [] } ]
map.get(1).children: [ { id: 2, name: '节点2', children: [] } ]

=== 验证引用关系 ===
所有引用指向同一对象: true

🔄 引用传递的优势

1. 内存效率

JavaScript
// 不需要复制整个对象,只传递引用
const largeObject = { /* 包含大量数据 */ };
const reference1 = largeObject;  // 只复制引用,不复制数据
const reference2 = largeObject;  // 只复制引用,不复制数据

2. 实时同步

JavaScript
function arrayToTree(arr) {
  const map = new Map();
  const result = [];
  
  // 所有变量都指向同一个对象实例
  for (const item of arr) {
    const node = { ...item, children: [] };
    map.set(item.id, node);
    
    if (!item.parentId) {
      result.push(node);  // result中存储引用
    }
  }
  
  for (const item of arr) {
    if (item.parentId) {
      const parent = map.get(item.parentId);  // 获取同一引用
      const child = map.get(item.id);         // 获取同一引用
      parent.children.push(child);            // 修改会影响result中的对象
    }
  }
  
  return result;  // 返回的对象已经包含了所有修改
}

⚠️ 注意事项和陷阱

1. 意外修改

JavaScript
const tree = arrayToTree(data);
const firstNode = tree[0];

// 危险!修改会影响原树结构
firstNode.name = '修改后的名称';
console.log(tree[0].name); // '修改后的名称' - 被意外修改

2. 深拷贝需求

JavaScript
// 如果需要独立的副本
function getIndependentCopy(tree) {
  return JSON.parse(JSON.stringify(tree));  // 深拷贝
}

const independentTree = getIndependentCopy(originalTree);
independentTree[0].name = '修改';  // 不会影响原树

3. 循环引用问题

JavaScript
// 如果存在循环引用
const obj = { name: 'test' };
obj.self = obj;  // 循环引用

// JSON.stringify(obj);  // 报错:Converting circular structure to JSON

📚 总结

在数组转树的算法中,引用传递的关键作用:

  1. Map存储引用map.set(id, nodeObject) 存储的是对象引用
  2. result存储引用result.push(node) 添加的是同一个引用
  3. 修改同步: 通过 parent.children.push(child) 修改对象时,所有引用该对象的地方都会看到变化
  4. 内存效率: 避免了对象的重复创建和复制

这就是为什么我们可以在第二次遍历中修改 parent.children,而最终返回的 result 数组中的对象会自动包含这些修改的原因!