高級前端面試手寫扁平數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)Tree
前言
招聘季節(jié)一般都在金三銀四,或者金九銀十。最近在這五六月份,陸陸續(xù)續(xù)面試了十幾個高級前端。有一套考察算法的小題目。后臺返回一個扁平的數(shù)據(jù)結(jié)構(gòu),轉(zhuǎn)成樹。
我們看下題目:打平的數(shù)據(jù)內(nèi)容如下:
let arr = [
{id: 1, name: '部門1', pid: 0},
{id: 2, name: '部門2', pid: 1},
{id: 3, name: '部門3', pid: 1},
{id: 4, name: '部門4', pid: 3},
{id: 5, name: '部門5', pid: 4},
]
輸出結(jié)果
[
{
"id": 1,
"name": "部門1",
"pid": 0,
"children": [
{
"id": 2,
"name": "部門2",
"pid": 1,
"children": []
},
{
"id": 3,
"name": "部門3",
"pid": 1,
"children": [
// 結(jié)果 ,,,
]
}
]
}
]
我們的要求很簡單,可以先不用考慮性能問題。實現(xiàn)功能即可,回頭分析了面試的情況,結(jié)果使我大吃一驚。
10%的人沒思路,沒碰到過這種結(jié)構(gòu)
60%的人說用過遞歸,有思路,給他個筆記本,但就是寫不出來
20%的人在引導(dǎo)下,磕磕絆絆能寫出來
剩下10%的人能寫出來,但性能不是最佳
感覺不是在招聘季節(jié)遇到一個合適的人真的很難。
接下來,我們用幾種方法來實現(xiàn)這個小算法
什么是好算法,什么是壞算法
判斷一個算法的好壞,一般從執(zhí)行時間和占用空間來看,執(zhí)行時間越短,占用的內(nèi)存空間越小,那么它就是好的算法。對應(yīng)的,我們常常用時間復(fù)雜度代表執(zhí)行時間,空間復(fù)雜度代表占用的內(nèi)存空間。
時間復(fù)雜度
時間復(fù)雜度的計算并不是計算程序具體運行的時間,而是算法執(zhí)行語句的次數(shù)。
隨著n的不斷增大,時間復(fù)雜度不斷增大,算法花費時間越多。 常見的時間復(fù)雜度有
- 常數(shù)階O(1)
- 對數(shù)階O(log2 n)
- 線性階O(n)
- 線性對數(shù)階O(n log2 n)
- 平方階O(n^2)
- 立方階O(n^3)
- k次方階O(n^K)
- 指數(shù)階O(2^n)
計算方法
- 選取相對增長最高的項
- 最高項系數(shù)是都化為1
- 若是常數(shù)的話用O(1)表示
舉個例子:如f(n)=3*n^4+3n+300 則 O(n)=n^4
通常我們計算時間復(fù)雜度都是計算最壞情況。計算時間復(fù)雜度的要注意的幾個點
- 如果算法的執(zhí)行時間不隨n的增加而增長,假如算法中有上千條語句,執(zhí)行時間也不過是一個較大的常數(shù)。此類算法的時間復(fù)雜度是O(1)。 舉例如下:代碼執(zhí)行100次,是一個常數(shù),復(fù)雜度也是O(1)。
let x = 1;
while (x <100) {
x++;
}
- 有多個循環(huán)語句時候,算法的時間復(fù)雜度是由嵌套層數(shù)最多的循環(huán)語句中最內(nèi)層語句的方法決定的。舉例如下:在下面for循環(huán)當中,外層循環(huán)每執(zhí)行一次,內(nèi)層循環(huán)要執(zhí)行n次,執(zhí)行次數(shù)是根據(jù)n所決定的,時間復(fù)雜度是O(n^2)。
for (i = 0; i < n; i++){
for (j = 0; j < n; j++) {
// ...code
}
}
- 循環(huán)不僅與n有關(guān),還與執(zhí)行循環(huán)判斷條件有關(guān)。舉例如下:在代碼中,如果arr[i]不等于1的話,時間復(fù)雜度是O(n)。如果arr[i]等于1的話,循環(huán)不執(zhí)行,時間復(fù)雜度是O(0)。
for(var i = 0; i<n && arr[i] !=1; i++) {
// ...code
}空間復(fù)雜度
空間復(fù)雜度是對一個算法在運行過程中臨時占用存儲空間的大小。
計算方法:
- 忽略常數(shù),用O(1)表示
- 遞歸算法的空間復(fù)雜度=(遞歸深度n)*(每次遞歸所要的輔助空間)
計算空間復(fù)雜度的簡單幾點
僅僅只復(fù)制單個變量,空間復(fù)雜度為O(1)。舉例如下:空間復(fù)雜度為O(n) = O(1)。
let a = 1;
let b = 2;
let c = 3;
console.log('輸出a,b,c', a, b, c);
遞歸實現(xiàn),調(diào)用fun函數(shù),每次都創(chuàng)建1個變量k。調(diào)用n次,空間復(fù)雜度O(n*1) = O(n)。
function fun(n) {
let k = 10;
if (n == k) {
return n;
} else {
return fun(++n)
}
}
不考慮性能實現(xiàn),遞歸遍歷查找
主要思路是提供一個遞getChildren的方法,該方法遞歸去查找子集。 就這樣,不用考慮性能,無腦去查,大多數(shù)人只知道遞歸,就是寫不出來。。。
/**
* 遞歸查找,獲取children
*/
const getChildren = (data, result, pid) => {
for (const item of data) {
if (item.pid === pid) {
const newItem = {...item, children: []};
result.push(newItem);
getChildren(data, newItem.children, item.id);
}
}
}
/**
* 轉(zhuǎn)換方法
*/
const arrayToTree = (data, pid) => {
const result = [];
getChildren(data, result, pid)
return result;
}
從上面的代碼我們分析,該實現(xiàn)的時間復(fù)雜度為O(2^n)。
不用遞歸,也能搞定
主要思路是先把數(shù)據(jù)轉(zhuǎn)成Map去存儲,之后遍歷的同時借助對象的引用,直接從Map找對應(yīng)的數(shù)據(jù)做存儲
function arrayToTree(items) {
const result = []; // 存放結(jié)果集
const itemMap = {}; //
// 先轉(zhuǎn)成map存儲
for (const item of items) {
itemMap[item.id] = {...item, children: []}
}
for (const item of items) {
const id = item.id;
const pid = item.pid;
const treeItem = itemMap[id];
if (pid === 0) {
result.push(treeItem);
} else {
if (!itemMap[pid]) {
itemMap[pid] = {
children: [],
}
}
itemMap[pid].children.push(treeItem)
}
}
return result;
}
從上面的代碼我們分析,有兩次循環(huán),該實現(xiàn)的時間復(fù)雜度為O(2n),需要一個Map把數(shù)據(jù)存儲起來,空間復(fù)雜度O(n)
最優(yōu)性能
主要思路也是先把數(shù)據(jù)轉(zhuǎn)成Map去存儲,之后遍歷的同時借助對象的引用,直接從Map找對應(yīng)的數(shù)據(jù)做存儲。不同點在遍歷的時候即做Map存儲,有找對應(yīng)關(guān)系。性能會更好。
function arrayToTree(items) {
const result = []; // 存放結(jié)果集
const itemMap = {}; //
for (const item of items) {
const id = item.id;
const pid = item.pid;
if (!itemMap[id]) {
itemMap[id] = {
children: [],
}
}
itemMap[id] = {
...item,
children: itemMap[id]['children']
}
const treeItem = itemMap[id];
if (pid === 0) {
result.push(treeItem);
} else {
if (!itemMap[pid]) {
itemMap[pid] = {
children: [],
}
}
itemMap[pid].children.push(treeItem)
}
}
return result;
}
從上面的代碼我們分析,一次循環(huán)就搞定了,該實現(xiàn)的時間復(fù)雜度為O(n),需要一個Map把數(shù)據(jù)存儲起來,空間復(fù)雜度O(n)
小試牛刀
| 方法 | 1000(條) | 10000(條) | 20000(條) | 50000(條) |
|---|---|---|---|---|
| 遞歸實現(xiàn) | 154.596ms | 1.678s | 7.152s | 75.412s |
| 不用遞歸,兩次遍歷 | 0.793ms | 16.499ms | 45.581ms | 97.373ms |
| 不用遞歸,一次遍歷 | 0.639ms | 6.397ms | 25.436ms | 44.719ms |
從我們的測試結(jié)果來看,隨著數(shù)量的增大,遞歸的實現(xiàn)會越來越慢,基本成指數(shù)的增長方式。
以上就是高級前端面試手寫扁平數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)Tree的詳細內(nèi)容,更多關(guān)于扁平數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)Tree的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
TypeScript類型any never void和unknown使用場景區(qū)別
這篇文章主要為大家介紹了TypeScript類型any never void和unknown使用場景區(qū)別,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10
TypeScript使用noImplicitAny實戰(zhàn)解析
這篇文章主要為大家介紹了TypeScript使用noImplicitAny實戰(zhàn)解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08
Manipulation-TypeScript?DOM操作示例解析
這篇文章主要為大家介紹了DOM?Manipulation-TypeScript?DOM操作示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03
TypeScript數(shù)據(jù)結(jié)構(gòu)之隊列結(jié)構(gòu)Queue教程示例
這篇文章主要為大家介紹了TypeScript數(shù)據(jù)結(jié)構(gòu)之隊列結(jié)構(gòu)Queue教程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02
TypeScript類型編程中的extends和infer示例解析
這篇文章主要為大家介紹了TypeScript類型編程中的extends和infer示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08
TypeScript實現(xiàn)類型安全的EventEmitter
這篇文章主要為大家介紹了TypeScript實現(xiàn)類型安全的EventEmitter示例詳解有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03

