🧑💻 寫在開頭
點贊 + 收藏 === 學會🤣🤣🤣
扁平數組轉樹形結構 (Array To Tree)
核心痛點
處理“數組轉樹”最直觀的思路是使用遞歸配合雙重循環:遍歷數組中的每一項,再次遍歷數組尋找其子節點。
這種做法的時間複雜度為 O(n2)O(n2)當數據量 nn較小時(如幾十條菜單),性能損耗尚可忽略。但當數據量達到數千條(如組織架構、行政區劃)時,計算次數呈指數級增長,會導致瀏覽器主線程阻塞,頁面卡頓。
解決方案:Map 映射法
為了解決性能瓶頸,我們需要將時間複雜度降低至 O(n)O(n)核心思路利用 JavaScript 對象是**引用類型(Reference Type)**的特性,結合 空間換時間 的策略:
-
第一次遍歷:構建一個以 id 為鍵,節點對象為值的 Map(或對象字典)。這一步可以讓我們在 O(1)O(1)的時間內獲取任意節點的引用。
-
第二次遍歷:通過 parentId 直接從 Map 中查找父節點引用,並將當前節點掛載到父節點的 children 屬性下。
代碼實現
/**
* 扁平數組轉樹形結構 - O(n)
* @param {Array} items 扁平數組
* @param {Object} config 配置項,兼容不同的字段名
* @returns {Array} 樹形結構
*/
function arrayToTree(items, config = {}) {
const { id = 'id', pid = 'parentId', children = 'children' } = config;
const result = [];
const itemMap = new Map();
// 1. 初始化 Map,並處理數據深淺拷貝問題
// 這一步確保了後續操作的是新對象,不污染原數據
for (const item of items) {
// 擴展運算符實現淺拷貝,初始化 children 數組
itemMap.set(item[id], { ...item, [children]: [] });
}
// 2. 二次遍歷,利用引用地址組裝樹
for (const item of items) {
const idValue = item[id];
const pidValue = item[pid];
const treeItem = itemMap.get(idValue);
// 嘗試獲取父節點引用
const parent = itemMap.get(pidValue);
if (parent) {
// 如果父節點存在,利用引用特性,直接 push 到父節點的 children 中
// 此時 Map 中的父節點對象和 result 中的對象指向同一塊內存地址
parent[children].push(treeItem);
} else {
// 如果找不到父節點,説明是根節點(Root)
// 注意:這裏兼容了 parentId 為 null、0 或不存在的情況
result.push(treeItem);
}
}
return result;
}
深度解析
該算法僅對數組進行了兩次遍歷,總操作次數為 2n2n因此時間複雜度穩定在 O(n)O(n)關鍵在於 parent[children].push(treeItem) 這一步。在 JavaScript 堆內存中,treeItem 和 parent 都是獨立的對象。當我們把 treeItem 放入 parent 的數組時,實際上是存儲了 treeItem 的內存地址。
無論層級多深,我們都無需遞歸查找,因為 Map 充當了“索引表”,讓我們能直接定位到任何節點的內存地址並進行掛載。
樹形結構轉扁平數組 (Tree To Array)
核心場景
- 全量搜索:在樹中查找符合條件的節點。
- 表格渲染:將樹形數據展示為平鋪的表格。
- 數據清洗:去除樹結構中的空 children 或無效字段。
解決方案:迭代法 (Iterative Approach)
雖然遞歸(Recursion)寫法代碼簡潔,但在極端層級深度下(如深度超過 10000 層),遞歸調用棧(Call Stack)可能溢出。
為了代碼的健壯性,推薦使用廣度優先遍歷 (BFS) 或 深度優先遍歷 (DFS) 的迭代寫法。這裏展示基於棧(Stack)的 DFS 寫法,因為它邏輯清晰且性能優異。
代碼實現
/**
* 樹形結構轉扁平數組 - 迭代法 (DFS)
* @param {Array} tree 樹形數據
* @returns {Array} 扁平數組
*/
function treeToArray(tree) {
const result = [];
// 使用擴展運算符淺拷貝,避免修改原數組引用
// stack 作為任務棧,模擬遞歸調用的過程
const stack = [...tree];
while (stack.length > 0) {
// 彈出棧頂元素(後進先出,模擬深度優先)
const node = stack.pop();
// 解構分離 children 和其他屬性
// 目的:扁平化後的節點通常不需要 children 字段,且防止循環引用
const { children, ...rest } = node;
// 將處理後的純淨節點推入結果集
result.push(rest);
// 如果存在子節點,將它們推入棧中待後續處理
if (children && children.length > 0) {
// 注意:為了保證順序(如果需要),可以反轉 children 後入棧,或者使用 shift() 做 BFS
// 這裏使用 push + pop 實現 DFS
stack.push(...children);
}
}
return result;
}
注意點:Immutability (不可變性)
在上述代碼中,const { children, ...rest } = node 是至關重要的一步。
- 斷開引用:我們不希望扁平化後的對象依然保留龐大的 children 樹狀結構,這會造成不必要的內存佔用。
- 數據安全:如果直接 delete node.children,會修改原始傳入的樹數據。在 Vue/React 等響應式框架中,這種副作用(Side Effect)會導致不可預知的視圖更新異常。通過解構賦值產生新對象,保證了函數的純度(Pure Function)。
總結
在前端工程化開發中,處理複雜數據結構是檢驗工程師基本功的試金石。
-
數組轉樹:核心在於利用 Hash Map 建立索引,將 O(n2)O(n2)的嵌套查找優化為 O(n)O(n)的引用掛載。這是典型的“空間換時間”算法思想。
- 樹轉數組:核心在於利用 棧(Stack) 或 隊列(Queue) 將遞歸邏輯轉化為迭代循環,避免棧溢出風險,並嚴格遵守數據不可變原則。
理解內存引用(Memory Reference)和算法複雜度,是寫出高性能、強健壯性代碼的關鍵。