需求背景
在需要存儲樹結構的情況下,一般由于使用的關系型數據庫(如 MySQL),是以類似表格的扁平化方式存儲數據。因此不會直接將樹結構存儲在數據庫中
代表了如下的樹狀結構:
{
id: 1,
pid: 0,
data: 'a',
children: [
{id: 2, pid: 1, data: 'b'},
{id: 3, pid: 1, data: 'c'},
]
}
??
const list = [
{ pid: null, id: 1, data: "1" },
{ pid: 1, id: 2, data: "2-1" },
{ pid: 1, id: 3, data: "2-2" },
{ pid: 2, id: 4, data: "3-1" },
{ pid: 3, id: 5, data: "3-2" },
{ pid: 4, id: 6, data: "4-1" },
];
方法一
遞歸解法:該方法簡單易懂,從根節點出發,每一輪迭代找到 pid 為當前節點 id 的節點,作為當前節點的 children,遞歸進行。
function listToTree(
list,
pid = null,
{ idName = "id", pidName = "pid", childName = "children" } = {}
) {
return list.reduce((root, item) => {
// 遍歷每一項,如果該項與當前 pid 匹配,則遞歸構建該項的子樹
if (item[pidName] === pid) {
const children = listToTree(list, item[idName]);
if (children.length) {
item[childName] = children;
}
return [...root, item];
}
return root;
}, []);
}
//時間復雜度為 O(n^2)
方法二
迭代法:利用對象在 js 中是引用類型的原理。第一輪遍歷將所有的項,將項的 id 與項自身在字典中建立映射,為后面的立即訪問做好準備。 由于操作的每一項都是對象,結果集 root 中的每一項和字典中相同 id 對應的項實際上指向的是同一塊數據。后續的遍歷中,直接對字典進行操作,操作同時會反應到 root 中。
function listToTree(
list,
rootId = null,
{ idName = "id", pidName = "pid", childName = "children" } = {}
) {
const record = {}; // 用空間換時間,用于將所有項的 id 及自身記錄到字典中
const root = [];
list.forEach((item) => {
record[item[idName]] = item; // 記錄 id 與項的映射
item[childName] = [];
});
list.forEach((item) => {
if (item[pidName] === rootId) {
root.push(item);
} else {
// 由于持有的是引用,record 中相關元素的修改,會在反映在 root 中。
record[item[pidName]][childName].push(item);
}
});
return root;
}
//時間復雜度為 O(n)
方法二變體一
在解法二的基礎上,將兩輪迭代合并成一輪迭代。采用邊迭代邊構建的方式:
function listToTree(
list,
rootId = null,
{ idName = "id", pidName = "pid", childName = "children" } = {}
) {
const record = {}; // 用空間換時間,用于將所有項的 id 及自身記錄到字典中
const root = [];
list.forEach((item) => {
const id = item[idName];
const parentId = item[pidName];
// 如果該項不在 record 中,則放入 record。如果該項已存在 (可能由別的項構建 pid 加入),則合并該項和已存在的數據
record[id] = !record[id] ? item : { ...item, ...record[id] };
const treeItem = record[id];
if (parentId === rootId) {
// 如果是根元素,則加入結果集
root.push(treeItem);
} else {
// 如果父元素不存在,則初始化父元素
if (!record[parentId]) {
record[parentId] = {};
}
// 如果父元素沒有 children, 則初始化
if (!record[parentId][childName]) {
record[parentId][childName] = [];
}
record[parentId][childName].push(treeItem);
}
});
return root;
}
//時間復雜度為 O(n)
方法二變體二
record 字典僅記錄 id 與 children 的映射關系,代碼更精簡:
function listToTree(
list,
rootId = null,
{ idName = "id", pidName = "pid", childName = "children" } = {}
) {
const record = {}; // 用空間換時間,僅用于記錄 children
const root = [];
list.forEach((item) => {
const newItem = Object.assign({}, item); // 如有需要,可以復制 item ,可以不影響 list 中原有的元素。
const id = newItem[idName];
const parentId = newItem[pidName];
// 如果當前 id 的 children 已存在,則加入 children 字段中,否則,初始化 children
// item 與 record[id] 引用同一份 children,后續迭代中更新 record[parendId] 就會反映到 item 中
newItem[childName] = record[id] ? record[id] : (record[id] = []);
if (parentId === rootId) {
root.push(newItem);
} else {
if (!record[parentId]) {
record[parentId] = [];
}
record[parentId].push(newItem);
}
});
return root;
}
//時間復雜度為 O(n)
總結
● 遞歸法:在數據量增大的時候,性能會急劇下降。好處是可以在構建樹的過程中,給節點添加層級信息。
● 迭代法:速度快。但如果想要不影響源數據,需要在 record 中存儲一份復制的數據,且無法在構建的過程中得知節點的層級信息,需要構建完后再次深度優先遍歷獲取。
● 迭代法變體一:按需創建 children,可以避免空的 children 列表。
時間復雜度
代碼的執行時間 T(n)與每行代碼的執行次數 n 成正比,人們把這個規律總結成這么一個公式:T(n) = O(f(n));
大O時間復雜度并不具體表示代碼真正的執行時間,而是表示代碼執行時間隨數據規模增長的變化趨勢,所以,也叫作漸進時間復雜度,簡稱時間復雜度。
● 只關注循環執行次數最多的一段代碼
function total(n) { // 1
var sum = 0; // 2
for (var i = 0; i < n; i++) { // 3
sum += i; // 4
} //5
} //6
//只有第 3 行和第 4 行是執行次數最多的,分別執行了 n 次,那么忽略常數項,所以此段代碼的時間復雜度就是 O(n)。
● 加法法則:總復雜度等于量級最大的那段代碼的復雜度。
function total(n) {
// 第一個 for 循環
var sum1 = 0;
for (var i = 0; i < n; i++) {
for (var j = 0; j < n; j++) {
sum1 = sum1 + i + j;
}
}
// 第二個 for 循環
var sum2 = 0;
for(var i=0;i<1000;i++) {
sum2 = sum2 + i;
}
// 第三個 for 循環
var sum3 = 0;
for (var i = 0; i < n; i++) {
sum3 = sum3 + i;
}
}
//取最大量級,所以整段代碼的時間復雜度為 O(n2)。
● 乘法法則:嵌套代碼的復雜度等于嵌套內外代碼復雜度的乘積
function f(i) {
var sum = 0;
for (var j = 0; j < i; j++) {
sum += i;
}
return sum;
}
function total(n) {
var res = 0;
for (var i = 0; i < n; i++) {
res = res + f(i); // 調用 f 函數
}
}
// O(n2)。
function total1(n) {
var sum = 0;
var i = 1;
while (i <= n) {
sum += i;
i = i * 2;
}
}
function total2(n) {
var sum = 0;
for (var i = 1; i <= n; i = i * 2) {
sum += i;
}
}
//2x = n => x = log2n => O(logn)
??
function total(n) {
var sum = 0;
for (var i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
function total(n) {
var sum = n * (n + 1) / 2
return sum;
}
注:來源于小哥的分享