フラット配列はツリー構造に変換されます.これは、バックグラウンド管理システムを実行するときにも頻繁に使用される機能です.インタビュー中にも頻繁に使用されます.今日はそれを実装し、Nuggets から 2 つの記事を引用します, ありがとう.
1. 良いアルゴリズムとは? 悪いアルゴリズムとは?
アルゴリズムの品質を判断するには、一般的に执行时间
合計で言え占用空间
ば、実行時間が短く、占有するメモリ スペースが小さいほど、優れたアルゴリズムです。それに対応して、実行時間を表すために時間計算量を使用し、占有されたメモリ空間を表すために空間計算量を使用することがよくあります。
時間の複雑さ
時間計算量の計算は、プログラムの特定の実行時間を計算することではなく、アルゴリズムがステートメントを実行する回数を計算することです。
時間の複雑さが続く につれてn
、より多くのアルゴリズムが存在します。一般的な時間の複雑さは增大
增大
花费时间
- 一定の順序
O(1)
- 対数次数
O(log2 n)
- 線形順序
O(n)
- 線形対数次数
O(n log2 n)
- 平方オーダー
O(n^2)
- 立方次数
O(n^3)
- k次
O(n^K)
- 指数順
O(2^n)
計算方法
- 相対成長率が最も高い項目を選択してください
- 最高項の係数を1に減らす
- 定数ならO(1)で表す
例: f(n)=3*n^4+3n+300 の場合、O(n)=n^4
通常、時間の複雑さを計算するのは、最悪のケースを計算することです。時間計算量を計算する際の注意点
- アルゴリズムの実行時間
不随n
が增加
ゼロ の場合增长
、アルゴリズムにステートメントがある場合上千条
、実行時間は 1 にすぎません较大的常数
。このようなアルゴリズムの時間計算量は ですO(1)
。例は次のとおりです。コードは 100 回実行されますが、これは定数であり、複雑さも同様ですO(1)
。
let x = 1;
while (x <100) {
x++;
}
多个循环语
アルゴリズムの時間計算量は、嵌套层数最多
ループ ステートメント内の最内层
ステートメントのメソッドによって決まる場合があります。例を次に示します。次の for ループでは、外层循环
各実行を1 回実行する一次
必要があり、実行回数は n に従って決定され、時間計算量は です。内层循环
n
O(n^2)
for (i = 0; i < n; i++){
for (j = 0; j < n; j++) {
// ...code
}
}
- ループは単なる
n
ループではなく、判断条件
ループの実行に関するものです。例を次に示します。コードでは、arr[i]
等しくない場合1
、時間計算量はO(n)です。arr[i]
等しい場合1
、ループは実行されず、時間計算量は ですO(0)
。
for(var i = 0; i<n && arr[i] !=1; i++) {
// ...code
}
スペースの複雑さ
スペースの複雑さは、操作中にアルゴリズムによって一時的に占有されるストレージ スペースのサイズです。
計算方法:
- 定数を無視し、O(1) で表現する
- 再帰アルゴリズムのスペースの複雑さ = (再帰の深さ n) * (各再帰に必要な補助スペース)
スペースの複雑さを計算するためのいくつかの簡単なポイント
- 単一の変数のみがコピーされ、スペースの複雑さは O(1) です。例は次のとおりです。空間の複雑度は O(n) = O(1) です。
let a = 1;
let b = 2;
let c = 3;
console.log('输出a,b,c', a, b, c);
- 再帰的な実装で、fun 関数を呼び出し、毎回変数 k を作成します。n 回の呼び出し、スペースの複雑さ O(n*1) = O(n)。
function fun(n) {
let k = 10;
if (n == k) {
return n;
} else {
return fun(++n)
}
}
テストデータ:
const array = [
{ id: 1, parentId: 0, name: "菜单1" },
{ id: 2, parentId: 0, name: "菜单2" },
{ id: 3, parentId: 0, name: "菜单3" },
{ id: 4, parentId: 1, name: "菜单4" },
{ id: 5, parentId: 1, name: "菜单5" },
{ id: 6, parentId: 2, name: "菜单6" },
{ id: 7, parentId: 4, name: "菜单7" },
{ id: 8, parentId: 7, name: "菜单8" },
{ id: 9, parentId: 8, name: "菜单9" },
{ id: 10, parentId: 9, name: "菜单10" },
{ id: 11, parentId: 10, name: "菜单11" },
{ id: 12, parentId: 11, name: "菜单12" },
{ id: 13, parentId: 12, name: "菜单13" },
{ id: 14, parentId: 13, name: "菜单14" },
];
2. フラット配列をツリー構造に変換する
1. パフォーマンスが良くなく (1W のデータには 18 秒が必要)、実装は比較的単純です: 再帰的方法
/**
* 方法一:简单递归
* @param { Array } data 数据源
* @param { Array } result 输出结果
* @param { Number | String } parentId 根id
*/
const getChildren = (data, result = [], parentId) => {
for (const item of data) {
if (item.parentId === parentId) {
const newItem = { ...item, children: [] };
result.push(newItem);
getChildren(data, newItem.children, item.id);
}
}
return result;
};
const res2 = getChildren(array, [], 0);
console.log("res2", res2);
/**
* 方法二:递归实现
* @param { Array } list 数组
* @param { String } parentId 父级 id
* @param { Object } param2 可配置参数
*/
const generateTree = (
list,
parentId = 0,
{ idName = "id", parentIdName = "parentId", childName = "children" } = {}
) => {
if (!Array.isArray(list)) {
throw new Error("type only Array");
// new Error("type only Array");
return list;
}
return list.reduce((pre, cur) => {
// 找到parentId 的子节点之后,递归找子节点的下一级节点
if (cur[parentIdName] === parentId) {
const children = generateTree(list, cur[idName]);
if (children?.length) {
cur[childName] = children;
}
return [...pre, cur];
}
return pre;
}, []);
};
const result = generateTree(array, 0);
2.非再帰的な方法を使用して、パフォーマンスはOKです
オブジェクトは参照機能を保存します. 毎回現在のノードの id がキーとして使用され、対応するノードの参照情報が保存されます. 配列をトラバースするとき、objMap の子情報が毎回更新されるため、すべてのノードとそれらの子ノードは objMap に保持されます. 最も重要なことは、配列を 1 回トラバースする だけでよく、時間の計算量は O(n) です。この方法を使用すると、1W データの計算時間はわずか 60ms です。
/**
* 方法三:不用递归的简单循环
* @param { Array } 源数据
*/
const arrayToTree = (items) => {
const result = []; // 结果集
const itemMap = {};
// 先转成map存储
for (const item of items) {
itemMap[item.id] = { ...item, children: [] };
}
for (const item of items) {
const id = item.id;
const parentId = item.parentId;
const treeItem = itemMap[id];
if (parentId === 0) {
result.push(treeItem);
} else {
if (!itemMap[parentId]) {
itemMap[parentId] = { children: [] };
}
itemMap[parentId].children.push(treeItem);
}
}
return result;
};
const res3 = arrayToTree(array);
console.log("res3", res3);
/**
* 方法四:非递归实现 (映射 + 引用)
* 前提:每一项都有parentId,根元素
* @param { Array } list 数组
* @param { String } rootId 根元素Id
* @param { Object } param2 可配置参数
*/
const generateTree2 = (
list,
rootId = 0,
{ idName = "id", parentIdName = "parentId", childName = "childern" } = {}
) => {
if (!Array.isArray(list)) {
new Error("type only Array");
return list;
}
const objMap = {}; //暂存数组以 id 为 key的映射关系
const result = []; // 结果
for (const item of list) {
const id = item[idName];
const parentId = item[parentIdName];
// 该元素有可能已经放入map中,(找不到该项的parentId时 会先放入map
objMap[id] = !objMap[id] ? item : { ...item, ...objMap[id] };
const treeItem = objMap[id]; // 找到映射关系那一项(注意这里是引用)
if (parentId === rootId) {
// 已经到根元素则将映射结果放进结果集
result.push(treeItem);
} else {
// 若父元素不存在,初始化父元素
if (!objMap[parentId]) {
objMap[parentId] = [];
}
// 若无该根元素则放入map中
if (!objMap[parentId][childName]) {
objMap[parentId][childName] = [];
}
objMap[parentId][childName].push(treeItem);
}
}
return result;
};
const res = generateTree2(array);
console.log("res", res);
Daxie の元のアドレス 1: 1 週間のデータ、タイル配列からツリー構造https://juejin.cn/post/6988901231674523661#comment
Daxie の元の住所 2: 10 を超える高度なフロントエンドにインタビューした後、(フラットなデータ構造から Tree まで) 書くことさえできませんでした #comment