title: JS樹結(jié)構(gòu)數(shù)據(jù)的遍歷
date: 2022-04-14
description: 針對項目中出現(xiàn)樹形結(jié)構(gòu)數(shù)據(jù)的時候,我們怎樣去操作他
開始
項目中我們會經(jīng)常出現(xiàn)對樹形結(jié)構(gòu)的遍歷、查找和轉(zhuǎn)換的場景,比如說DOM樹、族譜、社會機構(gòu)、組織架構(gòu)、權(quán)限、菜單、省市區(qū)、路由、標簽等等。那針對這些場景和數(shù)據(jù),我們又如何去遍歷和操作,有什么方式或者技巧可以簡化我們的實現(xiàn)思路。下面我們將針對常規(guī)出現(xiàn)的場景去總結(jié)一下我們的遍歷方式
樹結(jié)構(gòu)操作
遍歷
1、樹結(jié)構(gòu)介紹
樹的特點
1、每個節(jié)點都只有有限個子節(jié)點或無子節(jié)點;
2、沒有父節(jié)點的節(jié)點稱為根節(jié)點;
3、每一個非根節(jié)點有且只有一個父節(jié)點;
4、除了根節(jié)點外,每個子節(jié)點可以分為多個不相交的子樹;
5、樹里面沒有環(huán)路
下面的圖片表示一顆樹

在下面的JS中我們由多棵樹組成我們的數(shù)據(jù)
const tree = [
{
id: '1',
label: 'tree1',
children: [
{
id: '1-1',
label: 'tree1-1',
children: [
{
id: '1-1-1',
label: 'tree1-1-1',
}
]
},
{
id: '1-2',
label: 'tree1-2',
}
]
},
{
id: '2',
label: 'tree2',
children: [
{
id: '2-1',
label: 'tree2-1',
}
]
},
{
id: '3',
label: 'tree3'
}
]
在這數(shù)據(jù)中我們?nèi)绾卧u判數(shù)據(jù)是否為葉節(jié)點(也就是最后一級),我們每個節(jié)點都會存在children屬性,如果不存在children屬性或者children不是一個數(shù)組或者children為數(shù)組且長度為0我們則認為他是一個葉節(jié)點
2、遍歷方式介紹
我們針對樹結(jié)構(gòu)的操作離不開遍歷,遍歷的話又分為廣度優(yōu)先遍歷、深度優(yōu)先遍歷。其中深度優(yōu)先遍歷可以通過遞歸和循環(huán)的方式實現(xiàn),而廣度優(yōu)先遍歷的話是非遞歸的
- 深度優(yōu)先遍歷(DFS=Depth First Search)
- 先序遍歷(Pre-Order Traversal)
- 后續(xù)遍歷(Post-Order Traversal)
- 二叉樹中還有中序遍歷
- 廣度優(yōu)先遍歷(BFS=Breath First Search)
3、廣度優(yōu)先遍歷
從上往下對每一層依次訪問,在每一層中,從左往右(也可以從右往左)訪問結(jié)點,訪問完一層就進入下一層,直到?jīng)]有結(jié)點可以訪問為止。即訪問樹結(jié)構(gòu)的第n+1層前必須先訪問完第n層。
簡單的說,BFS是從根節(jié)點開始,沿著樹的寬度遍歷樹的節(jié)點。如果所有節(jié)點均被訪問,則算法中止。
所以我們的實現(xiàn)思路是,維護一個隊列,隊列的初始值為樹結(jié)構(gòu)根節(jié)點組成的列表,重復執(zhí)行以下步驟直到隊列為空:
取出隊列中的第一個元素,進行訪問相關(guān)操作,然后將其后代元素(如果有)全部追加到隊列最后。
function bfsTreeEach(tree, func){
let node, nodes = tree.slice()
while(node = nodes.shift()){
func(node)
if(node.children && node.children.length){
nodes.push(...node.children)
}
}
}
4、深度優(yōu)先遍歷
深度優(yōu)先搜索算法(英語:Depth-First-Search,DFS)是一種用于遍歷或搜索樹或圖的算法。這個算法會盡可能深的搜索樹的分支。當節(jié)點v的所在邊都己被探尋過,搜索將回溯到發(fā)現(xiàn)節(jié)點v的那條邊的起始節(jié)點。這一過程一直進行到已發(fā)現(xiàn)從源節(jié)點可達的所有節(jié)點為止。如果還存在未被發(fā)現(xiàn)的節(jié)點,則選擇其中一個作為源節(jié)點并重復以上過程,整個進程反復進行直到所有節(jié)點都被訪問為止
遞歸實現(xiàn)方式
1、先序遍歷
訪問子樹的時候,先訪問根再訪問根的子樹
function dfsPreTreeRecursion(tree, func){
let nodes = tree.slice()
for(let i = 0; i < nodes.length; i++){
let node = nodes[i]
func(node)
if(node.children && node.children.length){
dfsPreTreeRecursion(node.children ,func)
}
}
}
2、后序遍歷
訪問子樹的時候,先訪問子樹再訪問根
function dfsPostTreeRecursion(tree, func){
let nodes = tree.slice()
for(let i = 0; i < nodes.length; i++){
let node = nodes[i]
if(node.children && node.children.length){
dfsPostTreeRecursion(node.children ,func)
}
func(node)
}
}
循環(huán)實現(xiàn)方式
1、先序遍歷
先序遍歷與廣度優(yōu)先循環(huán)實現(xiàn)類似,要維護一個隊列,不同的是子節(jié)點不追加到隊列最后,而是加到隊列最前面
function dfsPreTreeEach(tree, func){
let node, nodes = tree.slice()
while(node = nodes.shift()){
func(node)
if(node.children && node.children.length){
nodes.unshift(...node.children)
}
}
}
2、后序遍歷
后序遍歷就略微復雜一點,我們需要不斷將子樹擴展到根節(jié)點前面去,執(zhí)行列表遍歷,并且通過一個臨時對象維護一個id列表,當遍歷到某個節(jié)點如果它沒有子節(jié)點或者它本身已經(jīng)存在于我們的臨時id列表,則執(zhí)行訪問操作,否則繼續(xù)擴展子節(jié)點到當前節(jié)點前面
function dfsPostTreeEach(tree, func){
let node, nodes = tree.slice();
let temp = new Map()
while (node = nodes[0]) {
let childLength = node.children ? node.children.length : 0
if(childLength && !temp.has(node.id)){
nodes.unshift(...node.children)
}
if(!childLength || temp.has(node.id)){
func(node)
nodes.shift()
temp.delete(node.id)
}
temp.set(node.id)
}
}
總結(jié)
對于樹結(jié)構(gòu)的遍歷操作,其實遞歸是最基礎(chǔ),也是最容易理解的。遞歸本身就是循環(huán)的思想,所以可以用循環(huán)來改寫遞歸,以上的方式在項目中已經(jīng)廊括了大部分的場景了,我們在日常開發(fā)中可以根據(jù)場景或者需要去選擇我們的遍歷方式,或者基于此對他進行調(diào)整和優(yōu)化,至于每種方式的空間復雜度和時間復雜度我們在這個地方就不去嘗試了,各位感興趣可以自己去驗證。
參考
圖文詳解兩種算法:深度優(yōu)先遍歷(DFS)和廣度優(yōu)先遍歷(BFS)