代碼量超過了文字,慎讀前端
前面部分是流水帳,不感興趣的同窗能夠直接略過😄node
記得剛入行前端的時候,我很是排斥使用框架,全部的功能,都要本身用原生寫,那時候好有激情啊......,固然也是到處碰壁,其中某個管理系統中的一個樹形表格折磨了我好久,後端的同窗直接甩給我一個相似這樣的結構:面試
const list = [
{ id: 3, name: 'cc', parentId: 1, sort: 2 },
{ id: 4, name: 'dd', parentId: 1, sort: 1 },
{ id: 5, name: 'ee', parentId: 2, sort: 4 },
{ id: 6, name: 'ff', parentId: 3, sort: 3 },
{ id: 7, name: 'gg', parentId: 2, sort: 0 },
{ id: 8, name: 'hh', parentId: 4, sort: 1 },
{ id: 9, name: 'ii', parentId: 1, sort: 3 },
{ id: 1, name: 'aa', parentId: 0, sort: 1 },
{ id: 2, name: 'bb', parentId: 0, sort: 0 },
]
複製代碼
而後需求大概是這個樣子算法
要達到這種效果,首先得將這個列表轉換成樹形結構,而後遍歷這個樹形結構生成表格,過程當中還須要算出子節點的深度等等後端
當時思考良久而答案始終不可得🤔,因而百度一番首次接觸了遞歸(竟然還能搜到😊):數組
function fn(data, pid) {
var result = [], temp;
for (var i = 0; i < data.length; i++) {
if (data[i].pid == pid) {
var obj = {"text": data[i].name,"id": data[i].id};
temp = fn(data, data[i].id);
if (temp.length > 0) {
obj.children = temp;
}
result.push(obj);
}
}
return result;
}
複製代碼
而後我根據個人狀況將上面的寫法改造了一番終於將功能實現了,當時實現完後是滿滿的成就感吶!框架
不久以後,我回頭再看這個轉換,能不能不使用遞歸進行轉換呢?當時瞭解遞歸時也知道遞歸的性能不怎麼樣,而且後端甩給個人列表也有些大,我測試每次轉換大概要消耗大幾十毫秒,因此這成了我內心的一道坎,得翻過去啊,因而🤔函數
function toTree (tree, topId) {
tree.forEach(function (child) {
child.children = tree.filter(function (cd) {
return cd.parentId === child.id
})
})
return tree.filter(child => child.parentId === topId)
}
複製代碼
太爽了,當時看到這樣的代碼,起碼得興奮一夜,況且仍是我本身想出來的😄。上面的原理就是,利用數組是一維的,同時利用引用類型的特性,直接改變節點的屬性值,而後根據topId過濾一下就能獲得轉換好的樹。這很好,避開了遞歸,而後我將先後2種方式比較了一下,效率差很少提高20幾倍左右(應該沒記錯,幾年了)。性能
又過了不久,印證了一句話,too young too simple,我偶然從某篇文章(不記得了)中瞭解到了深度優先和廣度優先、時間複雜度和空間複雜度這幾個詞,因而開啓了新世界。測試
上面的作法,看似簡潔,但在時間複雜度上的表現彷佛太弱了,每一次循環都要進行一次filter操做,返回時又是一次循環,這是不必的。
接下來我將以往的一些相關的經驗總結分享一下......,
繼承上面的一丟丟思路,通過個人改良版本,原理仍是利用了引用類型的特性,用一個map經過節點的id
保存每個節點的children
引用,而後一邊遍歷一邊更新children
,最後清空map。
function convertListToTree (list, topId = 0) {
let result
let map = {}
list.forEach(child => {
const { id, parentId } = child
const children = map[parentId] = map[parentId] || []
if (!result && parentId === topId) {
result = children
}
children.push(child)
child.children = map[id] || (map[id] = [])
})
map = null
return result
}
複製代碼
上面的代碼量多了一些,但仔細看會發現,整個轉換過程只經歷了一次循環O(n)
,相比以前的方法,優化了不少倍。但僅僅是轉換彷佛不太夠,可能有時候除了轉換還須要排序等其它操做。以排序來講,若是能儘可能減小時間複雜度,那確定最好不過了,這裏能夠在拿到全部children以後直接利用map來完成,遍歷map,而後對每一個children數組sort操做,如今將代碼再改造一下
function convertListToTree(list, callBack = c => c, options = {}) {
const {
topId = 0,
idKey = 'id',
parentIdKey = 'parentId',
sortKey
} = options
let result
let map = {}
list.forEach(child => {
const { [idKey]: id, [parentIdKey]: parentId } = child
const children = map[parentId] = map[parentId] || []
if (!result && parentId === topId) {
result = children
}
children.push( callBack(child) || child )
child.children = map[id] || (map[id] = [])
})
sortKey && Object.keys(map).forEach(i => {
if (map[i].length > 1) {
map[i].sort((a, b) => a[sortKey] - b[sortKey])
}
})
map = null
return result
}
複製代碼
如今對最開始的list
進行轉換
const tree = convertListToTree(list, node => {
node.someKey = 1
}, { sortKey: 'sort' })
複製代碼
搞定了列表轉樹,但若是遍歷樹呢?
深度優先搜索算法(英語:Depth-First-Search,DFS)是一種用於遍歷或搜索樹或圖的算法。沿着樹的深度遍歷樹的節點,儘量深的搜索樹的分支。當節點v的所在邊都己被探尋過,搜索將回溯到發現節點v的那條邊的起始節點。 這一過程一直進行到已發現從源節點可達的全部節點爲止。-----維基百科
按照深度優先搜索,上圖的遍歷順序爲: a b e f c d g h
廣度優先搜索算法(英語:Breadth-First-Search,縮寫爲BFS),又譯做寬度優先搜索,或橫向優先搜索,是一種圖形搜索算法。簡單的說,BFS是從根節點開始,沿着樹的寬度遍歷樹的節點。若是全部節點均被訪問,則算法停止。-----維基百科
按照廣度優先搜索,上圖的遍歷順序爲: a b c d e f g h
都很容易理解,接下來,先將他們簡單實現
深度優先遞歸方式 原理就是每碰到有children的節點遞歸調用一下
function recursiveEachByDfs (tree, cb) {
let i = 0
let node
while (node = tree[i++]) {
cb(node)
if (node.children && node.children.length) {
recursiveEachByDfs(node.children, cb)
}
}
}
複製代碼
深度優先非遞歸方式 原理就是利用棧,後進先出,每次碰到children,就將children壓入棧頂
function eachByDfs([...stack], cb) {
while (stack.length) {
// 出棧
const node = stack.shift()
cb(node)
if (node.children && node.children.length) {
// 入棧
stack.unshift(...node.children)
}
}
}
複製代碼
廣度優先遞歸方式 原理就是,遍歷當前層的節點時,將遇到children存入數組中,做爲tree傳入下一個調用,終止條件爲沒有下一層節點時
function recursiveEachByBfs(tree, cb) {
const nextLevels = []
// 遍歷當前層同時拿到下一層節點
let i = 0
let node
while (node = tree[i++]) {
cb(node)
nextLevels.push(...node.children)
}
// 遞歸下一層全部節點
if (nextLevels.length) {
recursiveEachByBfs(nextLevels, cb)
}
}
複製代碼
廣度優先非遞歸方式 原理就是利用隊列,先進先出,讓碰到children去排隊,就像食堂排隊打飯同樣
function eachByBfs([...queue], cb) {
while (queue.length) {
// 出隊
const node = queue.shift()
cb(node)
if (node.children && node.children.length) {
// 入隊
queue.push(...node.children)
}
}
}
複製代碼
如今我們就能經過上面任意一種方式遍歷樹形結構了,但僅僅是這樣確定是不夠的。根據我以往碰到的需求,至少還可能須要
首先,我們先大概分析一下上述功能:
continue
仍是break
那接下來,咱們將上面的4個基本方法都改造一下,所有支持上述功能,而後供君挑選。
relations[0]
: 節點深度relations[1]
: 全部父節點,按深度順序排列只須要通過函數參數傳遞一下
function recursiveEachByDfs(tree, cb, relations = [0, []]) {
let i = 0
let node
while (node = tree[i++]) {
const behavior = cb(node, ...relations)
if (behavior === 'break') {
break
}
if (behavior === 'continue') {
continue
}
if (node.children) {
const [level, parents] = relations
recursiveEachByDfs(node.children, cb, [level + 1, parents.concat(node)])
}
}
}
// 搜索根節點id爲0的分支,最深搜索3層
recursiveEachByDfs(tree, (node, level, parents) => {
if (level > 2) {
return 'continue'
}
if (level === 0 && node.id !== 0) {
return 'continue'
}
console.log(node, level, parents)
})
複製代碼
既然用到了棧,那就能夠往棧中加點料,原理就是在每一次入棧時,將入棧子節點們的父節點和深度信息(就叫信息對象吧,不限於這些東西)等一塊兒放到棧頂,以後在每次出棧時判斷若是不是節點類型的值,就說明這是接下來遍歷的子節點們的父節點以及深度信息,用一張圖來描述(基於本章第二張圖):
function eachByDfs([...stack], cb) {
let relations = [0, []]
// 將信息對象壓入棧頂
stack.unshift(relations)
while (stack.length) {
let node = stack.shift()
if (Array.isArray(node)) {
relations = node
node = stack.shift()
}
const behavior = cb(node, ...relations)
if (behavior === 'break') {
break
}
if (behavior === 'continue') {
continue
}
if (node.children && node.children.length) {
// 若是此時棧頂是信息對象,說明當前節點是它的父節點中子節點的最後一個了
if (stack.length && !Array.isArray(stack[0])) {
stack.unshift(relations)
}
const [level, parents] = relations
stack.unshift([level + 1, parents.concat(node)], ...node.children)
}
}
}
// 搜索根節點id爲0的分支,最深搜索3層
eachByDfs(tree, (node, level, parents) => {
if (level > 2) {
return 'continue'
}
if (level === 0 && node.id !== 0) {
return 'continue'
}
console.log(node, level, parents)
})
複製代碼
首次進入函數時,初始化一個信息對象,以後,將每一層的節點被統一放到數組中(將問題統一,就像處理第一層節點同樣),除了第一層節點的其它層的節點可能有不一樣的父節點,因此須要像插隊同樣,在每一個節點的children以前插入一個信息對象
function recursiveEachByBfs(tree, cb) {
const nextLevels = []
let relations = [0, []]
if (!Array.isArray(tree[0])) {
// 首次進入,放入父節點和深度信息
tree.unshift(relations)
}
let i = 0
let node
while (node = tree[i++]) {
if (Array.isArray(node)) {
relations = node
continue
}
const behavior = cb(node, ...relations)
if (behavior === 'break') {
break
}
if (behavior === 'continue') {
continue
}
const [level, parents] = relations
// 在每一個節點的子節點以前插入信息對象
nextLevels.push([level + 1, parents.concat(node)], ...node.children)
}
// 遞歸下一層全部節點
if (nextLevels.length) {
recursiveEachByBfs(nextLevels, cb)
}
}
// 搜索根節點id爲0的分支,最深搜索3層
recursiveEachByBfs(tree, (node, level, parents) => {
if (level > 2) {
return 'continue'
}
if (level === 0 && node.id !== 0) {
return 'continue'
}
console.log(node, level, parents)
})
複製代碼
思路和深度優先非遞歸相似,只不過這裏換成來隊列,但處理起來比棧稍微簡單點,在每一個節點的children以前插入一個信息對象......
function eachByBfs([...queue], cb) {
let relations = [0, []]
queue.unshift(relations)
while (queue.length) {
let node = queue.shift()
if (Array.isArray(node)) {
relations = node
node = queue.shift()
}
const behavior = cb(node, ...relations)
if (behavior === 'break') {
break
}
if (behavior === 'continue') {
continue
}
if (node.children && node.children.length) {
const [level, parents] = relations
queue.push([level + 1, parents.concat(node)], ...node.children)
}
}
}
// 搜索根節點id爲0的分支,最深搜索3層
eachByBfs(tree, (node, level, parents) => {
if (level > 2) {
return 'continue'
}
if (level === 0 && node.id !== 0) {
return 'continue'
}
console.log(node, level, parents)
})
複製代碼
如今,基本的功能都實現了,主要是以往作的項目都過小了,至今沒發現什麼更復雜的場景,若是有,那就繼續改造......,思路都差很少
這個我只在刷面試題的時候碰到......,這裏簡單實現一下,思路都是同樣的,知道怎麼遍歷就不是問題了😄,僅供參考,統一的思路就是將對象的每一個屬性拆解成一個信息對象,對象中包含key、value、parent等等等等,而後和處理數組的方式差很少,有以下粒子
const symbolName = Symbol()
const obj = {
a: {
b: {
c: 1
},
d: [{ j: 1 }, { [symbolName]: 222 }]
},
e: {
f: 3
},
g: {
h: {
i: 4
}
}
}
複製代碼
深度優先遞歸
const recursiveEachObjByDfs = (obj, cb) => {
Reflect.ownKeys(obj).forEach(key => {
const value = obj[key]
cb({ key, value, parent: obj })
if (typeof value === 'object') {
recursiveEachObjByDfs(value, cb)
}
})
}
recursiveEachObjByDfs(obj, console.log)
複製代碼
深度優先非遞歸
const eachObjByDfs = (obj, cb) => {
const stack = Reflect.ownKeys(obj).map(key => ({key, value: obj[key], parent: obj}))
while (stack.length) {
const options = stack.shift()
cb(options)
if (typeof options.value === 'object') {
stack.unshift(...Reflect.ownKeys(options.value).map(key => {
return {
key,
value: options.value[key],
parent: options.value
}
}))
}
}
}
eachObjByDfs(obj, console.log)
複製代碼
廣度優先遞歸
const recursiveEachObjByBfs = (obj, cb) => {
const currentLevels = Reflect.ownKeys(obj).map(key => ({key, value: obj[key], parent: obj}))
const each = levels => {
const nextLevels = []
let i = 0
let options
while (options = levels[i++]) {
cb(options)
if (typeof options.value === 'object') {
nextLevels.push(...Reflect.ownKeys(options.value).map(key => {
return {
key,
value: options.value[key],
parent: options.value
}
}))
}
}
if (nextLevels.length) {
each(nextLevels)
}
}
each(currentLevels)
}
recursiveEachObjByBfs(obj, console.log)
複製代碼
廣度優先非遞歸
const eachObjByBfs = (obj, cb) => {
const queue = Reflect.ownKeys(obj).map(key => ({key, value: obj[key], parent: obj}))
while (queue.length) {
const options = queue.shift()
cb(options)
if (typeof options.value === 'object') {
queue.push(...Reflect.ownKeys(options.value).map(key => {
return {
key,
value: options.value[key],
parent: options.value
}
}))
}
}
}
eachObjByBfs(obj, console.log)
複製代碼
寫得很差,莫計較,獻給須要的同窗,若是文章中有什麼錯誤或者有什麼能夠改進的地方,能夠下方留言咯。