棧、隊列、鏈表等數據結構,都是順序數據結構。而樹是非順序數據結構。樹型結構是一類很是重要的非線性結構。直觀地,樹型結構是以分支關係定義的層次結構。javascript
樹在計算機領域中也有着普遍的應用,例如在編譯程序中,用樹來表示源程序的語法結構;在數據庫系統中,可用樹來組織信息;在分析算法的行爲時,可用樹來描述其執行過程等等。html
例如,(a)是隻有一個根結點的樹;(b)是有13個結點的樹,其中A是根,其他結點分紅3個互不相交的子集:T1={B,E,F,K,L},t2={D,H,I,J,M};T1,T2和T3都是根A的子樹,且自己也是一棵樹。java
結點擁有的子樹數稱爲結點的度(Degree)。例如,(b)中A的度爲3,C的度爲1,F的度爲0。度爲0的結點稱爲葉子(Leaf)或者終端結點。度不爲0的結點稱爲非終端結點或分支結點。node
樹的度是樹內各結點的度的最大值。(b)的樹的度爲3。結點的子樹的根稱爲該結點的孩子(Child)。相應的,該結點稱爲孩子的雙親(Parent)。同一個雙親的孩子之間互稱兄弟(Sibling)。結點的祖先是從根到該結點所經分支上的全部結點。反之,以某結點爲根的子樹中的任一結點都稱爲該結點的子孫。git
求樹的深度:看這篇:https://www.jianshu.com/p/9fc...程序員
二叉樹(Binary Tree)是另外一種樹型結構,它的特色是每一個結點至多隻有兩棵子樹(即二叉樹中不存在度大於2的結點),而且,二叉樹的子樹有左右之分(其次序不能任意顛倒。)github
若是i=1,則結點i是二叉樹的根,無雙親;若是i>1,則其雙親parent(i)是結點Math.floor(i/2)。
若是2i > n,則結點i無左孩子(結點i爲葉子結點);不然其左孩子LChild(i)是結點2i.
若是2i + 1 > n,則結點i無右孩子;不然其右孩子RChild(i)是結點2i + 1;
用一組連續的存儲單元依次自上而下,自左至右存儲徹底二叉樹上的結點元素,即將二叉樹上編號爲i的結點元素存儲在加上定義的一維數組中下標爲i-1的份量中。「0」表示不存在此結點。這種順序存儲結構僅適用於徹底二叉樹。由於,在最壞狀況下,一個深度爲k且只有k個結點的單支樹(樹中不存在度爲2的結點)卻須要長度爲2的n次方-1的一維數組。web
順序:[1, 2, 3, 4, 5, , 6, , , 7]算法
二叉樹的結點由一個數據元素和分別指向其左右子樹的兩個分支構成,則表示二叉樹的鏈表中的結點至少包含三個域:數據域和左右指針域。有時,爲了便於找到結點的雙親,還可在結點結構中增長一個指向其雙親結點的指針域。利用這兩種結構所得的二叉樹的存儲結構分別稱之爲二叉鏈表和三叉鏈表。 在含有n個結點的二叉鏈表中有n+1個空鏈域,咱們能夠利用這些空鏈域存儲其餘有用信息,從而獲得另外一種鏈式存儲結構---線索鏈表。數據庫
鏈式:{ data, left, right}
遍歷二叉樹(Traversing Binary Tree):是指按指定的規律對二叉樹中的每一個結點訪問一次且僅訪問一次。
二叉樹有深度遍歷和廣度遍歷, 深度遍歷有前序、 中序和後序三種遍歷方法。二叉樹的前序遍歷能夠用來顯示目錄結構等;中序遍歷能夠實現表達式樹,在編譯器底層頗有用;後序遍歷能夠用來實現計算目錄內的文件及其信息等。
二叉樹是很是重要的數據結構, 其中二叉樹的遍歷要使用到棧和隊列還有遞歸等,不少其它數據結構也都是基於二叉樹的基礎演變而來的。熟練使用二叉樹在不少時候能夠提高程序的運行效率,減小代碼量,使程序更易讀。
二叉樹不只是一種數據結構,也是一種編程思想。學好二叉樹是程序員進階的一個必然進程。
前序遍歷:訪問根–>遍歷左子樹–>遍歷右子樹;
中序遍歷:遍歷左子樹–>訪問根–>遍歷右子樹;
後序遍歷:遍歷左子樹–>遍歷右子樹–>訪問根;
廣度遍歷:按照層次一層層遍歷;
對該二叉樹進行深度和廣度遍歷爲:
前序遍歷:- + a b c / d e
中序遍歷:a + b * c - d / e
後序遍歷:a b c + d e / -
廣度遍歷:- + / a * d e b c
上述二叉樹(a+b*c)-d/e在js中能夠用對象的形式表示出來:
var tree = { value: "-", left: { value: '+', left: { value: 'a', }, right: { value: '*', left: { value: 'b', }, right: { value: 'c', } } }, right: { value: '/', left: { value: 'd', }, right: { value: 'e', } } }
深度遍歷也可稱爲深度優先遍歷(Depth-First Search,DFS),由於它老是優先往深處訪問。
先序遍歷
let result = []; let dfs = function (node) { if(node) { result.push(node.value); dfs(node.left); dfs(node.right); } } dfs(tree); console.log(result); // ["-", "+", "a", "*", "b", "c", "/", "d", "e"]
先序遞歸遍歷思路:
先遍歷根結點,將值存入數組,而後遞歸遍歷:先左結點,將值存入數組,繼續向下遍歷;直到(二叉樹爲空)子樹爲空,則遍歷結束;
而後再回溯遍歷右結點,將值存入數組,這樣遞歸循環,直到(二叉樹爲空)子樹爲空,則遍歷結束。
let dfs = function (nodes) { let result = []; let stack = []; stack.push(nodes); while(stack.length) { // 等同於 while(stack.length !== 0) 直到棧中的數據爲空 let node = stack.pop(); // 取的是棧中最後一個j result.push(node.value); if(node.right) stack.push(node.right); // 先壓入右子樹 if(node.left) stack.push(node.left); // 後壓入左子樹 } return result; } dfs(tree);
先序非遞歸遍歷思路:
中序遍歷
let result = []; let dfs = function (node) { if(node) { dfs(node.left); result.push(node.value); // 直到該結點無左子樹 將該結點存入結果數組 接下來並開始遍歷右子樹 dfs(node.right); } } dfs(tree); console.log(result); // ["a", "+", "b", "*", "c", "-", "d", "/", "e"]
中序遞歸遍歷的思路:
先遞歸遍歷左子樹,從最後一個左子樹開始存入數組,而後回溯遍歷雙親結點,再是右子樹,這樣遞歸循環。
function dfs(node) { let result = []; let stack = []; while(stack.length || node) { // 是 || 不是 && if(node) { stack.push(node); node = node.left; } else { node = stack.pop(); result.push(node.value); //node.right && stack.push(node.right); node = node.right; // 若是沒有右子樹 會再次向棧中取一個結點即雙親結點 } } return result; } dfs(tree); // ["a", "+", "b", "*", "c", "-", "d", "/", "e"]
一種利用回溯法思想的代碼:
看這裏:https://zhuanlan.zhihu.com/p/... 可是他的代碼有些問題。。。
非遞歸遍歷的思路:
將當前結點壓入棧,而後將左子樹當作當前結點,若是當前結點爲空,將雙親結點取出來,將值保存進數組,而後將右子樹當作當前結點,進行循環。
後序遍歷
result = []; function dfs(node) { if(node) { dfs(node.left); dfs(node.right); result.push(node.value); } } dfs(tree); console.log(result); // ["a", "b", "c", "*", "+", "d", "e", "/", "-"]
寫到這,深深的被遞歸折服。。。。服
先走左子樹,當左子樹沒有孩子結點時,將此結點的值放入數組中,而後回溯遍歷雙親結點的右結點,遞歸遍歷。
(含大量註釋代碼的)
function dfs(node) { let result = []; let stack = []; stack.push(node); while(stack.length) { // 不能用node.touched !== 'left' 標記‘left’作判斷, // 由於回溯到該結點時,遍歷右子樹已經完成,該結點標記被更改成‘right’ 若用標記‘left’判斷該if語句會一直生效致使死循環 if(node.left && !node.touched) { // 不要寫成if(node.left && node.touched !== 'left') // 遍歷結點左子樹時,對該結點作 ‘left’標記;爲了子結點回溯到該(雙親)結點時,便再也不訪問左子樹 node.touched = 'left'; node = node.left; stack.push(node); continue; } if(node.right && node.touched !== 'right') { // 右子樹同上 node.touched = 'right'; node = node.right; stack.push(node); continue; } node = stack.pop(); // 該結點無左右子樹時,從棧中取出一個結點,訪問(並清理標記) node.touched && delete node.touched; // 能夠不清理不影響結果 只是第二次對同一顆樹再執行該後序遍歷方法時,結果就會出錯啦由於你對這棵樹作的標記還留在這棵樹上 result.push(node.value); node = stack.length ? stack[stack.length - 1] : null; //node = stack.pop(); 這時當前結點再也不從棧中取(彈出),而是不改變棧數據直接訪問棧中最後一個結點 //若是這時當前結點去棧中取(彈出)會致使回溯時當該結點左右子樹都被標記過期 當前結點又變成從棧中取會漏掉對結點的訪問(存入結果數組中) } return result; // 返回值 } dfs(tree);
後序遍歷非遞歸遍歷思路:先把根結點和左樹推入棧,而後取出左樹,再推入右樹,取出,最後取根結點。
步驟:
廣度優先遍歷二叉樹(層序遍歷)是用隊列來實現的,廣度遍歷是從二叉樹的根結點開始,自上而下逐層遍歷;在同一層中,按照從左到右的順序對結點逐一訪問。
let result = []; let stack = [tree]; // 先將要遍歷的樹壓入棧 let count = 0; // 用來記錄執行到第一層 let bfs = function () { let node = stack[count]; if(node) { result.push(node.value); if(node.left) stack.push(node.left); if(node.right) stack.push(node.right); count++; bfs(); } } dfc(); console.log(result); // ["-", "+", "/", "a", "*", "d", "e", "b", "c"]
function bfs(node) { let result = []; let queue = []; queue.push(node); let pointer = 0; while(pointer < queue.length) { let node = queue[pointer++]; // // 這裏不使用 shift 方法(複雜度高),用一個指針代替 result.push(node.value); node.left && queue.push(node.left); node.right && queue.push(node.right); } return result; } bfs(tree); // ["-", "+", "/", "a", "*", "d", "e", "b", "c"]
另一種比較消耗性能的方法:不額外定義一個指針變量 pointer,使用數組的shift()方法,每次改變 queue 的數據(入棧、出棧),來讀取數據,直到棧 queue 中數據爲空,執行結束。(頻繁的改變數組,由於數組是引用類型,要改變它,要新開闢一個地址,因此比較消耗空間)
function bfs (node) { let result = []; let queue = []; queue.push(node); while(queue.length) { node = queue.shift(); result.push(node.value); // 不要忘記訪問 // console.log(node.value); node.left && queue.push(node.left); node.right && queue.push(node.right); } return result; } bfs(tree); // ["-", "+", "/", "a", "*", "d", "e", "b", "c"]
二叉樹與JavaScript
JavaScript與簡單算法
javascript實現數據結構: 樹和二叉樹,二叉樹的遍歷和基本操做
圖的基本算法(BFS和DFS)
搜索思想——DFS & BFS(基礎基礎篇)