二分搜尋樹
那麼接下來就來實現一下這個樹:二分搜尋樹也是二叉樹,和二叉樹長的一樣,就是有個特點,每個節點的值比他的左子樹的值大,比他的右子樹的值小。如下圖所示:
// 宣告節點建構函式 當前節點的值,左節點,右節點
class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
}
}
// 二分搜尋樹建構函式
class BST {
constructor() {
this.root = null
this.size = 0
}
getSize() {
return this.size
}
isEmpty() {
return this.size === 0
}
addNode(v) {
// 每次新增子節點後,更新樹
this.root = this._addChild(this.root, v)
}
_addChild(node, v) {
if (!node) {
this.size++
return new Node(v)
}
if (node.value > v) {
console.log(`在${node.value}節點,新增左節點${v}`)
node.left = this._addChild(node.left, v)
} else if (node.value < v) {
console.log(`在${node.value}節點,新增右節點${v}`)
node.right = this._addChild(node.right, v)
}
return node
}
}
let bst = new BST()
// 第一個節點
bst.addNode(10)
// 後續節點
bst.addNode(8)
bst.addNode(6)
bst.addNode(3)
bst.addNode(7)
bst.addNode(9)
bst.addNode(12)
bst.addNode(11)
bst.addNode(15)
bst.addNode(14)
console.log(bst)
複製程式碼
執行結果如下:
左節點值是8 右節點值12 中間節點值是10。
再展開左節點看一下
右邊就不再展開贅述了
二分搜尋樹--遍歷
二叉樹遍歷 分為深度遍歷(先序遍歷、中序遍歷、後序遍歷,三種遍歷的區別在於何時訪問節點), 廣度遍歷(一層層地遍歷)
先序遍歷
繞不開的遞迴又出現了,想起有一天右邊同事妹子很開心的和我說她知道了遞迴的終極奧義:“遞迴的終極奧義就是:不要想遞迴是怎麼具體一步步實現的”
那我先來實現一下先序遍歷
class BST {
...
// 新增先序遍歷實現,其實就是很簡單的幾行程式碼
preTraversal() {
this._pre(this.root)
}
_pre(node) {
if (node) {
// 訪問節點的值
console.log(node.value)
// 遞迴左右子樹
this._pre(node.left)
this._pre(node.right)
}
}
}
// 用上面生成的bst例項執行一下,結果如下圖
bst.preTraversal()
複製程式碼
那麼這個結果是如何生成的呢?
- 先是列印10,這個毫無爭議 然後 this._pre(node.left),this._pre(node.right)這兩個方法看似兩行,其實左子樹沒有遍歷完結的話是不會去遍歷右子樹的
- 此時this._pre(node.left)中引數是如下圖部分,同樣,會對這部分執行那3行程式碼,首先會列印8 , 然後以8那個節點作為根節點,去遍歷左右子樹
- 上面第三步驟時候,列印6之後,先遍歷左子樹,後遍歷右子樹。而此時的遍歷左子樹只是列印3。於是要去遍歷6的右子樹,也就是列印7。
- 列印7之後,本例中,作為節點8的左節點已經遍歷完畢。遍歷8的右節點,也就是列印9,之後8的右節點也遍歷完畢。
- 再往回退,列印9之後,也就是10節點的左節點已經全部遍歷完畢。 所以列印的結果是 10 8 6 3 7 9
- 同樣的邏輯此時該去遍歷10節點的右節點了。依次列印12 11 15 14 ,所以最終結果就是 10 8 6 3 7 9 12 11 15 14
一步步的推導遞迴的具體實現後,還真的覺的上面所說遞迴的奧義那句話總結的是很有意思的。
中序遍歷
class BST {
...
// 新增中序遍歷實現,其實就是很簡單的幾行程式碼
midTraversal() {
this._mid(this.root)
}
_mid(node) {
if (node) {
// 1語句 後面講解時候說'1語句'就指代下面這句
this._mid(node.left)
// 2語句
console.log(node.value)
// 3語句
this._mid(node.right)
}
}
}
// 用上面生成的bst例項執行一下,結果如下圖
bst.midTraversal()
複製程式碼
- 上面的逐條分析之後,中序遍歷就很容易理解了。上面的迴圈體主要三條語句,在第一次執行 2語句之前,我們要想一下,此時的引數node,是什麼?換個問法,在引數node是樹中的哪個節點的時候,才會第一次的執行2語句? 我來截圖吧
- 現在回想一下,當node引數是這個3節點的時候,回退一下,他的上一步執行函式中node是什麼?我來截圖
-
那麼再回退一下,列印出7之後,也就是node.left 是6這個節點的遞迴執行完畢了,此時此刻的node是8,然後執行console.log(node.value),再去執行3語句,此時此刻的3語句的引數是node.left,也就是9節點,對這個9節點,依次執行123語句,9節點沒有子節點,所以只是列印出9 ,至此,整個8節點遍歷完畢。
-
列印出了9之後, 8節點完全遍歷完畢了,8節點作為node.left,那麼此時的node是10節點,執行2語句,列印出10 ,那麼後續的,相信不用我再說了吧
-
這就是為什麼二分排序樹的中序遍歷的結果是排序好的。
後序遍歷
class BST {
...
// 新增後序遍歷實現,其實就是很簡單的幾行程式碼
backTraversal() {
this._back(this.root)
}
_back(node) {
if (node) {
// 1語句 後面講解時候說'1語句'就指代下面這句
this._back(node.left)
// 2語句
this._back(node.right)
// 3語句
console.log(node.value)
}
}
}
// 用上面生成的bst例項執行一下,結果如下圖
bst.backTraversal()
複製程式碼
- 遞迴的奧義:就是不要想遞迴是怎麼一步步具體實現的。
- 後續遍歷與之前遍歷方式的區別是的3語句是列印,那麼能否根據上面的前序推導和中序推導加上奧義,直接看著樹的圖,寫出後續遍歷的結果呢?
廣度遍歷
未完待續
(以上參考掘金小冊,融入自己的實操和思考,因為是收費小冊,參考的地址原文沒辦法貼出來,yck老師很贊,建議大家都去看看他的小冊)