TypeScript實現數組和樹的相互轉換
這段時間重新撿起了數據結構和算法,發(fā)現里面的樹和圖是真的掉頭發(fā)。本文基于一個面試題,詳細分析如何實現數組和樹的相互轉換。
前言
樹或者圖是個比較抽象的概念,并不存在這樣的數據類型。我們看一下樹的結構,一層嵌套一層,同層次可能還會有多個節(jié)點,這種結構的數據可以使用{}對象來表示。數組就比較簡單了,因此數組和樹的轉換可以理解為數組和對象之間的轉換,只是需要轉換的數組和對象都是比較特殊的數據。為了更好的看清楚轉換過程,本文采用ts的語法,使用js的話沒有類型約束,看起來就更容易暈了。
數組轉換為樹
我們先來一個簡單點的,將數組轉換為樹。

結合樹,我們看一下數組的結構:BC是A的子節(jié)點,DE是B的子節(jié)點,F是C的子節(jié)點。想要實現這個效果,我們先來捋一下實現思路:
- 遍歷數組
- 將數組項轉換為樹的葉子節(jié)點,并使用Map來維持id與節(jié)點直接的關系,便于快速查找
- 根據parentId查找到可能存在的父節(jié)點,并建立葉子節(jié)點和父節(jié)點之間的關系
- 找到根節(jié)點,最終返回的也就是根節(jié)點
文字描述可能不太具體,我們看一下實現代碼:
// 定義數組項的數據類型,包含id、name、parentId基本屬性
interface ArrayItem {
id: number,
name: string,
parentId: number
}
// 定義樹節(jié)點的數據類型,包含id、name、可能存在的子節(jié)點
interface TreeNode {
id: number,
name: string,
children?: TreeNode[], // 葉子節(jié)點沒有子節(jié)點
}
function array2Tree(arr: ArrayItem[]): TreeNode | null {
// 用于id和TreeNode的映射,map<key, value>,可通過id快速查找到樹節(jié)點,時間復雜度為O(1)
const treeNode: Map<number, TreeNode> = new Map();
let root = null; // 樹根節(jié)點
arr.forEach(item => {
const {id, name, parentId} = item; // 解構賦值
// 定義樹節(jié)點tree node,并使用Map維持id與節(jié)點之間的關系
const node: TreeNode = {id, name}
treeNode.set(id, node);
?
// 使用parentId找出parentNode
const parentNode = treeNode.get(parentId);
if (parentNode) {
if (parentNode.children == null) {
parentNode.children = [];
}
// 找到父節(jié)點后,將當前數組項轉換的節(jié)點添加到子節(jié)點列表中
parentNode.children.push(node)
}
?
// 在第一次循環(huán)中找到根節(jié)點,我們假定id=0的表示根節(jié)點
if (parentId === 0) {
root = node;
}
})
return root;
}
const tree = array2Tree(arrs); // 需要定義arrs
console.log(tree);
代碼是使用ts完成的,清晰的展示了樹節(jié)點數據類型的定義,在轉換過程中數據類型的變化。直接運行ts代碼是看不出來打印的結果的,我們可以新建一個vue-ts的項目,或者嫌麻煩的話可以直接使用官網提供的運行環(huán)境:playground
實現效果:

其實就是一個對象,對象的層次就像是樹一樣展開。
樹轉換為數組
同樣也是這個例子,只不過是需要將樹轉換為數組了。

數組和樹在數據結構上最大的差異就是樹沒有parentId的屬性,如果一個節(jié)點有子節(jié)點,那么該節(jié)點的id就應該是其子節(jié)點的parentId。老規(guī)矩,先來捋一下思路:
- 遍歷樹節(jié)點,使用廣度優(yōu)先。樹的遍歷可分為廣度優(yōu)先和深度優(yōu)先,廣度優(yōu)先表示橫向遍歷,深度優(yōu)先表示縱向遍歷
- 將遍歷出的樹節(jié)點添加到隊列中,實現先進先出。隊列并不是js中的數據類型,但是我們可以使用數組來實現隊列
- 將樹節(jié)點轉換為數組項
- 根據節(jié)點的父子關系,設置數組項的parentId,并將數組項添加到數組中
- 為了查找遍歷,可使用Map來維持父節(jié)點和子節(jié)點的關系
實現代碼:
// 定義數據結構
...
function tree2Array(root: TreeNode): ArrayItem[] {
// 定義子節(jié)點和父節(jié)點的映射關系,map<子節(jié)點,父節(jié)點>
const childToParent: Map<TreeNode, TreeNode> = new Map();
const arr: ArrayItem[] = []; // 返回的數組
// 廣度優(yōu)先遍歷樹,使用隊列保存節(jié)點
const queue: TreeNode[] = [];
queue.unshift(root); // 入隊,從頭部入隊
while(queue.length) {
const curNode = queue.pop(); // 出隊,從尾部出隊,保證先進先出
if (curNode == null) break; // 循環(huán)結束,使用==包括了null和undefined的情況
const {id, name, children = []} = curNode; // 結構賦值,葉子節(jié)點沒有children屬性,所以默認為空數組
const parentNode = childToParent.get(curNode); // 獲取當前節(jié)點的父節(jié)點
const parentId = parentNode?.id || 0; // 獲取父節(jié)點id,根節(jié)點設置為0
const item = {id, name, parentId}; // 定義數組項
arr.push(item); // 插入到數組中
// 繼續(xù)遍歷子節(jié)點,并且子節(jié)點入隊
children.forEach(child => {
// 如果有子節(jié)點,則把子節(jié)點和當前節(jié)點通過Map保持關系
childToParent.set(child, curNode);
// 入隊
queue.unshift(child);
})
}
return arr;
}
const array = tree2Array(obj); // 這個obj對象就是數組轉換為樹打印出的結果
console.log(array)
運行結果:

可以發(fā)現,結果符合預期。
總結
總結一下,數組和樹之間相互轉換一開始還挺蒙的,但是看完代碼發(fā)現,其實也并不復雜。
- 數組轉換為樹:從數組的第一項開始,根據parentId,找到每一項的歸屬;若找不到,則表示當前數組項為根節(jié)點
- 樹轉換為數組:樹的每一個節(jié)點都可能會有很多子節(jié)點,按照廣度優(yōu)先,從根節(jié)點開始遍歷,將每一個節(jié)點以及其子節(jié)點都添加到隊列中,按照先進先出的原則,逐一將節(jié)點都轉換為數組項并添加到數組中
- 在實現的過程中,可結合圖例,一步一步分析,最終實現結果
以上就是TypeScript實現數組和樹的相互轉換的詳細內容,更多關于TypeScript數組轉樹的資料請關注腳本之家其它相關文章!
相關文章
詳談js中標準for循環(huán)與foreach(for in)的區(qū)別
下面小編就為大家?guī)硪黄斦刯s中標準for循環(huán)與foreach(for in)的區(qū)別。小編覺得挺不錯的,現在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-11-11
原生JavaScript實現頁面滾動監(jiān)聽的方法步驟
滾動監(jiān)聽事件一般網頁中的返回頂部按鈕都是通過滾動監(jiān)聽事件來實現的,本文給大家介紹了原生JavaScript實現頁面滾動監(jiān)聽的方法步驟,文中通過代碼示例講解的非常詳細,對大家的學習或工作有一定的幫助,需要的朋友可以參考下2025-03-03

