前面介紹過的都是線性的資料結構,本文將介紹一種非線性資料結構——樹,它對於儲存需要快速查詢的資料非常有用。樹是一種一對多的資料結構,樹這種資料結構在生活中經常看到,如 組織結構圖
圖中每個元素我們叫做節點,即樹(Tree)可以理解為是n(n>=0)個節點的有限集合。當n=0時稱為空樹。
基本概念
樹這種資料結構跟現實生活中的樹很相似,樹中的元素叫節點,其中連線相鄰節點之間具有層級關係的叫做父子關係。
比如下面這幅圖,A 節點就是 B 節點的父節點,B 節點是 A 節點的子節點。B、C、D 這三個節點的父節點是同一個節點,所以它們之間互稱為兄弟節點。父節點為同一層的節點稱為堂兄弟節點,也就是圖中的B、C、D、K、L,及G、H、I、J。我們把沒有父節點的節點叫做根節點,也就是圖中的節點 E。我們把沒有子節點的節點叫做葉子節點或者葉節點,比如圖中的 G、H、I、J、K、L 都是葉子節點。
樹還有三個比較相似的概念:高度(Height)、深度(Depth)、層(Level)。
節點的高度:節點到葉子節點的最長路徑、邊的個數
節點的深度:跟節點到這個節點的邊的個數
節點的層數:節點的深度 + 1
樹的高度:跟節點的高度
這三個概念的定義比較容易混淆,描述起來也比較空洞。我舉個例子說明一下,你一看應該就能明白。
如圖所示高度是從下往上遞增,深度是上往下遞增,層數是深度加 1。
二叉樹
樹結構多種多樣,不過我們最常用還是二叉樹。
二叉樹,顧名思義,每個節點最多有兩個叉,也就是兩個子節點,分別是左子節點和右子節點。不過,二叉樹並不要求每個節點都有兩個子節點,有的節點只有左子節點,有的節點只有右子節點。我畫的這幾個都是二叉樹。
因為二叉樹每個節點最多隻有兩個子節點,所以既可以用陣列來儲存,也可以用連結串列來儲存。
先來看比較簡單、直觀的鏈式儲存法。從圖中你應該可以很清楚地看到,每個節點有三個欄位,其中一個儲存資料,另外兩個是指向左右子節點的指標。我們只要知道根節點,就可以通過左右子節點的指標,把整棵樹都串起來,這種儲存方式我們比較常用。大部分二叉樹程式碼都是通過這種結構來實現的。
基於陣列的順序儲存法。把根節點儲存在下標 i = 1 的位置,那左子節點儲存在下標 2 * i = 2 的位置,右子節點儲存在 2 * i + 1 = 3 的位置。以此類推,B 節點的左子節點儲存在 2 * i = 2 * 2 = 4 的位置,右子節點儲存在 2 * i + 1 = 2 * 2 + 1 = 5 的位置。
如圖所示,如果節點 X 儲存在陣列中下標為 i 的位置,下標為 2 * i 的位置儲存的就是左子節點,下標為 2 * i + 1 的位置儲存的就是右子節點。反過來,下標為 i/2 的位置儲存就是它的父節點。通過這種方式,我們只要知道根節點儲存的位置(一般情況下,為了方便計運算元節點,根節點會儲存在下標為 1 的位置),這樣就可以通過下標計算,把整棵樹都串起來。
建立二叉樹
觀察上面的圖我們可以知道,二叉樹實際就是一個遞迴的過程,不斷的左子樹、右子樹,直到該節點沒有左子樹或者右子樹。遞迴需要一個臨界點來結束遞迴,不然會死迴圈,從圖中可以知道樹終止遞迴其實就是沒有左子樹、右子樹,也就是葉子節點,所以我們需要把葉子節點補上,用 # 來表示 如:
1 const arr = ['A','B','D','#','#','E','#','#','C','F','#', '#', 'G', '#', '#']
步驟:
- 先建立跟節點
- 遞迴建立左子樹
- 遞迴建立右子樹
先序遍歷構建
1 /* 2 * @Description: 3 * @Version: 1.0 4 * @Autor: longbs 5 * 先序構建 6 */ 7 8 class Node { 9 constructor (data = '#') { 10 this.data = data 11 this.lNode = null 12 this.rNode = null 13 } 14 } 15 16 class BiTree { 17 root = null 18 nodeList = [] 19 constructor (nodeList) { 20 this.root = new Node() 21 this.nodeList = nodeList 22 } 23 createNode (node) { 24 const data = this.nodeList.shift() 25 if (data === '#') return 26 node.data = data 27 // 下一個元素是不是空節點, 如果不是建立左節點 28 if (this.nodeList[0] !== '#') { 29 node.lNode = new Node(data) 30 } 31 this.createNode(node.lNode) 32 33 // 下一個元素是不是空節點, 如果不是建立右節點 34 if (this.nodeList[0] !== '#') { 35 node.rNode = new Node(data) 36 } 37 this.createNode(node.rNode) 38 39 } 40 } 41 42 const arr = ['A','B','D','#','#','E','#','#','C','F','#', '#', 'G', '#', '#'] 43 const bi = new BiTree(arr) 44 bi.createNode(bi.root) 45 console.log(bi.root)
層級遍歷構建
還可以一層一層的來構建,如先建立跟節點,在建立下一層的左子樹、右子樹,在繼續建立左子樹的下一層(左右子樹)。
步驟:
- 先建立跟節點
- 建立左子樹
- 建立右子樹
- 重複2、3過程
通常這種層次的問題可以使用佇列來解決,先將跟節點入隊,把佇列中的隊首出隊,將這個出隊相關的節點入隊,這樣迴圈,一直到佇列為空。
1 /* 2 * @Description: 3 * @Version: 1.0 4 * @Autor: longbs 5 * 層次構建 6 */ 7 class Node { 8 constructor (data = '#') { 9 this.data = data 10 this.lNode = null 11 this.rNode = null 12 } 13 } 14 15 class BiTreeByLevel { 16 root = null 17 constructor () { 18 this.root = null 19 } 20 createNode (nodeList) { 21 let queue = [] 22 if (!this.root) { 23 let nodeValue = nodeList.shift() 24 this.root = new Node(nodeValue) 25 queue.push(this.root) 26 } 27 while (queue.length) { 28 // 將佇列隊首出隊,這個是樹的跟節點或者子樹的跟節點 29 let head= queue.shift() 30 // 找到相關的在入隊 31 let nodeValue = nodeList.shift() 32 // 構建左節點 33 if (nodeValue !== '#') { 34 head.lNode = new Node(nodeValue) 35 queue.push(head.lNode) 36 } 37 // 右節點 38 nodeValue = nodeList.shift() 39 if (nodeValue !== '#') { 40 head.rNode = new Node(nodeValue) 41 queue.push(head.rNode) 42 } 43 } 44 } 45 } 46 47 let arr = ['A','B','C','D','E','F','G','#','#','#','#','#','#','#','#'] 48 let bi = new BiTreeByLevel(arr) 49 bi.createNode(arr) 50 console.log(bi.root)
今天先到這裡吧,後面把二叉樹先序、中序、後序、層次遍歷,查詢二叉樹、紅黑樹補上。