上一篇:JS數據結構與算法_集合&字典javascript
學習樹離不開遞歸。java
遞歸是一種解決問題的方法,它解決問題的各個小部分,直到解決最初的大問題。遞歸一般涉及函數調用自身。
通俗的解釋:年級主任須要知道某個年級的數學成績的平均值,他無法直接獲得結果;年級主任須要問每一個班的數學老師,數學老師須要問班上每一個同窗;而後再沿着學生-->老師-->主任這條線反饋,才能獲得結果。遞歸也是如此,本身沒法直接解決問題,將問題給下一級,下一級若沒法解決,再給下一級,直到有結果再依次向上反饋。node
咱們常見的使用遞歸解決的問題,以下:算法
// 斐波拉契數列 function fibo(n) { if (n === 0 || n === 1) return n; // 邊界 return fibo(n - 1) + fibo(n - 2); } // 階乘 function factorial(n) { if (n === 0 || n === 1) return 1; // 邊界 return facci(n - 1) * n; }
他們有共同的特色,也是遞歸的特色:segmentfault
以斐波拉契數列舉例,下面是n=6
時斐波拉契數列的計算過程。緩存
咱們能夠發現,這裏面存在許多重複的計算,數列越大重複計算越多。數據結構
如何避免呢?利用緩存,將fib(n)
計算後的值存儲,後面使用時,若存在直接取用,不存在則計算閉包
(1)緩存Memoizer
函數
const fibo_memo = function() { const temp = {0: 0, 1: 1}; // 須要用閉包緩存 return function fib(n) { if (!(n in temp)) { // 緩存中無對應數據時,向下計算查找 temp[n] = fib(n - 1) + fib(n - 2); } return temp[n]; } }()
(2)遞推法(動態規劃)post
動態規劃並不屬於高效遞歸,可是也是有效解決問題的一個方法。
動態規劃:從底部開始解決問題,將全部小問題解決掉,而後合併成一個總體解決方案,從而解決掉整個大問題;
遞歸:從頂部開始將問題分解,經過解決掉全部分解的小問題來解決整個問題;
使用動態規劃解決斐波那契數列
function fibo_dp(n) { let current = 0; let next = 1; for(let i = 0; i < n; i++) { [current, next] = [next, current + next]; } return current; }
(3)效率對比
const arr = Array.from({length: 40}, (_, i) => i); // 普通 console.time('fibo'); arr.forEach((e) => { fibo(e); }); console.timeEnd('fibo'); // 緩存 console.time('fibo_memo'); arr.forEach((e) => { fibo_memo(e); }); console.timeEnd('fibo_memo'); // 動態規劃 console.time('fibo_dp'); arr.forEach((e) => { fibo_dp(e); }); console.timeEnd('fibo_dp'); // 打印結果【40】 fibo: 1869.665ms fibo_memo: 0.088ms fibo_dp: 0.326ms // 當打印到【1000】時,普通的已溢出 fibo_memo: 0.370ms fibo_dp: 16.458ms
總結:從上面的對比結果可知,使用緩存的性能最佳
一個樹結構包含一系列存在父子關係的節點。每一個節點都有一個父節點(除了頂部的第一個
節點)以及零個或多個子節點:
關於數的深度和高度的問題,不一樣的教材有不一樣的說法,具體能夠參考樹的高度和深度以及結點的高度和深度這篇文章
BST
二叉樹是樹的一種特殊狀況,每一個節點最多有有兩個子女,分別稱爲該節點的左子女和右子女,就是說,在二叉樹中,不存在度大於2的節點。
二叉搜索樹(BST
)是二叉樹的一種,可是它只容許你在左側節點存儲(比父節點)小的值, 在右側節點存儲(比父節點)大(或者等於)的值。
上圖展現的即是二叉搜索數
x^(i-1)
個節點2^k-1
個節點滿二叉樹:深度爲k的滿二叉樹,是有2^k-1
個節點的二叉樹,每一層都達到了能夠容納的最大數量的節點
insert(key)
: 向樹中插入一個新的鍵;inOrderTraverse
: 經過中序遍歷方式遍歷全部節點preOrderTraverse
: 經過先序遍歷方式遍歷全部節點postOrderTraverse
: 經過後序遍歷方式遍歷全部節點getMin
: 返回樹中最小的值/鍵getMax
: 返回樹中最大的值/鍵find(key)
: 在樹中查找一個鍵,若是節點存在則返回該節點不存在則返回null
;remove(key)
: 從樹中移除某個鍵BST
的實現// 基類 class BinaryTreeNode { constructor(data) { this.key = data; this.left = null; this.right = null; } }
下圖展示了二叉搜索樹數據結構的組織方式:
//二叉查找樹(BST)的類 class BinarySearchTree { constructor() { this.root = null; // 根節點 } insert(){} // 插入節點 preOrderTraverse(){} // 先序遍歷 inOrderTraverse(){} // 中序遍歷 postOrderTraverse(){} // 後序遍歷 search(){} // 查找節點 getMin(){} // 查找最小值 getMax(){} // 查找最大值 remove(){} // 刪除節點 }
insert
某個值到樹中,必須依照二叉搜索樹的規則【每一個節點Key值惟一,最多有兩個節點,且左側節點值<父節點值<右側節點值】
不一樣狀況具體操做以下:
null
,直接賦值插入節點給根節點;null
,按照BST
規則找到left/right
爲null
的位置並賦值insert(key) { const newNode = new BinaryTreeNode(key); if (this.root !== null) { this.insertNode(this.root, newNode); } else { this.root = newNode; } } insertNode(node, newNode) { if (newNode.key < node.key) { if (node.left === null) {// 左側 node.left = newNode; } else { this.insertNode(node.left, newNode); } } else { if (node.right === null) {// 右側 node.right = newNode; } else { this.insertNode(node.right, newNode); } } }
下圖爲在已有BST
的基礎上插入值爲6的節點,步驟以下:
樹的遍歷,核心爲遞歸:根節點須要找到其每個子孫節點,可是並不知道這棵樹有多少層。所以,它找到其子節點,子節點也不知道,依次向下找,直到葉節點。
訪問樹的全部節點有三種方式:中序、先序和後序。下面依次介紹
(1)中序遍歷
中序遍歷是一種以上行順序訪問BST全部節點的遍歷方式,也就是以從最小到最大的順序訪問全部節點。中序遍歷的一種應用就是<u>對樹進行排序操做</u>
inOrderTraverse(callback) { this.inOrderTraverseNode(this.root, callback); } inOrderTraverseNode(node, callback) { if (node !== null) { this.inOrderTraverseNode(node.left, callback); callback(node.key); this.inOrderTraverseNode(node.right, callback); } }
下面的圖描繪了中序遍歷方法的訪問路徑:
(2)先序遍歷
先序遍歷是以優先於後代節點的順序訪問每一個節點的。先序遍歷的一種應用是<u>打印一個結構化的文檔</u>
preOrderTraverse(callback) { this.preOrderTraverseNode(this.root, callback); } preOrderTraverseNode(node, callback) { if (node !== null) { callback(node.key); this.preOrderTraverseNode(node.left, callback); this.preOrderTraverseNode(node.right, callback); } }
下面的圖描繪了先序遍歷方法的訪問路徑:
(3)後序遍歷
後序遍歷則是先訪問節點的後代節點,再訪問節點自己。後序遍歷的一種應用是<u>計算一個目錄和它的子目錄中全部文件所佔空間的大小</u>
postOrderTraverse(callback) { this.postOrderTraverseNode(this.root, callback); } postOrderTraverseNode(node, callback) { if (node !== null) { this.postOrderTraverseNode(node.left, callback); this.postOrderTraverseNode(node.right, callback); callback(node.key); } }
下面的圖描繪了後序遍歷方法的訪問路徑:
(1)最值
觀察下圖,咱們能夠很是直觀的發現左下角爲最小值,右下角爲最大值
具體代碼實現以下
getMin() { const ret = this.getMinNode(); return ret && ret.key; } getMinNode(node = this.root) { if (node) { while (node && node.left !== null) { node = node.left; } } return node; } getMax() { const ret = this.getMaxNode(); return ret && ret.key; } getMaxNode(node = this.root) { if (node) { while (node && node.right !== null) { node = node.right; } } return node; }
(2)find()方法
遞歸找到與目標key
值相同的節點,並返回;具體實現以下:
find(key) { return this.findNode(this.root, key); } findNode(node, key) { if (node === null) { return null; } if (key < node.key) { return this.findNode(node.left, key); } if (key > node.key) { return this.findNode(node.right, key); } return node; }
remove()
方法移除節點是這一類方法中最爲複雜的操做,首先須要找到目標key
值對應的節點,而後根據不一樣的目標節點類型須要有不一樣的操做
remove(key) { return this.removeNode(this.root, key); } removeNode(node, key) { if (node === null) { return null; } if (key < node.key) { // 目標key小於當前節點key,繼續向左找 node.left = this.removeNode(node.left, key); return node; } if (key > node.key) { // 目標key小於當前節點key,繼續向右找 node.right = this.removeNode(node.right, key); return node; } // 找到目標位置 if (node.left === null && node.right === null) { // 目標節點爲葉節點 node = null; return node; } if (node.right === null) { // 目標節點僅有左側節點 node = node.left; return node; } if (node.left === null) { // 目標節點僅有右側節點 node = node.right; return node; } // 目標節點有兩個子節點 const tempNode = this.getMinNode(node.right); // 右側最小值 node.key = tempNode.key; node.right = this.removeNode(node.right, node.key); return node; }
目標節點爲葉節點圖例:子節點賦值爲null
,並將目標節點指向null
目標節點爲僅有左側子節點或右側子節點圖例:將目標節點的父節點指向子節點
目標節點有兩個子節點:根據BST
的構成規則,以目標節點右側樹最小值替換從新鏈接