樹
樹是一種非順序數據結構,一種分層數據的抽象模型,它對於存儲需要快速查找的數據非常有用。
一個樹結構包含一系列存在父子關係的節點。每個節點都有一個父節點(除了頂部的第一個 節點)以及零個或多個子節點:
樹大概包含以下幾種結構/屬性:
- 節點
- 根節點
- 內部節點:非根節點、且有子節點的節點
- 外部節點/頁節點:無子節點的節點
- 子樹:就是大大小小節點組成的樹
- 深度:節點到根節點的節點數量
- 高度:樹的高度取決於所有節點深度中的最大值
- 層級:也可以按照節點級別來分層
二叉樹和二叉搜索樹
二叉樹中的節點最多只能有兩個子節點:一個是左側子節點,另一個是右側子節點。這些定 義有助於我們寫出更高效的向/從樹中插人、查找和刪除節點的算法。二叉樹在計算機科學中的 應用非常廣泛。
二叉搜索樹(BST)是二叉樹的一種,但是它只允許你在左側節點存儲(比父節點)小的值, 在右側節點存儲(比父節點)大(或者等於)的值。上圖中就展現了一棵二叉搜索樹。
不同於之前的鏈表和集合,在樹中節點被稱爲"鍵",而不是"項"。
下圖展現了二叉搜索樹數據結構的組織方式:
使用 JavaScript 實現一個 BinarySearchTree 類:
class Node {
constructor(key) {
this.key = key
this.left = null
this.right = null
}
}
class BinarySearchTree {
constructor() {
this.root = null
}
insert(key) {
const newNode = new Node(key)
const insertNode = (node, newNode) => {
if (newNode.key < node.key) {
if (node.left === null) {
node.left = newNode
} else {
insertNode(node.left, newNode)
}
} else {
if (node.right === null) {
node.right = newNode
} else {
insertNode(node.right, newNode)
}
}
}
if (!this.root) {
this.root = newNode
} else {
insertNode(this.root, newNode)
}
}
}
二叉搜索樹類的使用:
const tree = new BinarySearchTree()
tree.insert(11)
tree.insert(7)
tree.insert(5)
tree.insert(3)
tree.insert(9)
tree.insert(8)
tree.insert(10)
tree.insert(13)
tree.insert(12)
tree.insert(14)
tree.insert(20)
tree.insert(18)
tree.insert(25)
最終構建的樹如下圖:
樹的遍歷
遍歷一棵樹是指訪問樹的每個節點並對它們進行某種操作的過程。但是我們應該怎麼去做呢?應該從樹的頂端還是底端開始呢?從左開始還是從右開始呢?
訪問樹的所有節點有三種方式:中序、先序、後序。
中序遍歷
中序遍歷是一種以上行順序訪問 BST 所有節點的遍歷方式,也就是以從最小到最大的順序訪 問所有節點。中序遍歷的一種應用就是對樹進行排序操作。我們來看它的實現:
inOrderTraverse(callback) {
const inOrderTraverseNode = (node, callback) => {
if (node !== null) {
inOrderTraverseNode(node.left, callback)
callback(node.key)
inOrderTraverseNode(node.right, callback)
}
}
inOrderTraverseNode(this.root, callback)
}
inOrderTraverse方法接收一個回調函數作爲參數,回調函數用來定義我們對遍歷到的每個節點進行的操作,這也叫作訪問者模式。
在之前展示的樹上執行下面的方法:
tree.inOrderTraverse(value => { console.log(value) })
下面的結果將會在控制檯上輸出(每個數字將會輸出在不同的行):
3 5 6 7 8 9 10 11 12 13 14 15 18 20 25
下面的圖描繪了 inOrderTraverse 方法的訪問路徑:
先序遍歷
先序遍歷是以優先於後代節點的順序訪問每個節點的。先序遍歷的一種應用是打印一個結構化的文檔。
preOrderTraverse(callback) {
const preOrderTraverseNode = (node, callback) => {
if (node !== null) {
callback(node.key)
preOrderTraverseNode(node.left, callback)
preOrderTraverseNode(node.right, callback)
}
}
preOrderTraverseNode(this.root, callback)
}
下面的圖描繪了 preOrderTraverse 方法的訪問路徑:
後序遍歷
後序遍歷則是先訪問節點的後代節點,再訪問節點本身。後序遍歷的一種應用是計算一個目錄和它的子目錄中所有文件所佔空間的大小。
我們來看它的實現:
postOrderTraverse(callback) {
const postOrderTraverseNode = (node, callback) => {
if (node !== null) {
postOrderTraverseNode(node.left, callback)
postOrderTraverseNode(node.right, callback)
callback(node.key)
}
}
postOrderTraverseNode(this.root, callback)
}
下面的圖描繪了 postOrderTraverse方法的訪問路徑:
三種遍歷訪問順序的不同:
- 先序遍歷:節點本身 => 左側子節點 => 右側子節點
- 中序遍歷:左側子節點 => 節點本身 => 右側子節點
- 後序遍歷:左側子節點 => 節點本身 => 右側子節點
搜索樹中的值
在樹中,有三種經常執行的搜索類型:
- 最小值
- 最大值
- 搜索特定的值
搜索最小值和最大值
我們使用下面的樹作爲示例:
實現方法:
min(node) {
const minNode = node => {
return node ? (node.left ? minNode(node.left) : node) : null
}
return minNode(node || this.root)
}
max(node) {
const maxNode = node => {
return node ? (node.right ? maxNode(node.right) : node) : null
}
return maxNode(node || this.root)
}
搜索一個特定的值
search(key) {
const searchNode = (node, key) => {
if (node === null) return false
if (node.key === key) return node
return searchNode((key < node.key) ? node.left : node.right, key)
}
return searchNode(root, key)
}
移除一個
remove(key) {
const removeNode = (node, key) => {
if (node == null) {
return null;
}
if (key == node.key) {
// 沒有子節點(子樹)
if (node.left == null && node.right == null) {
return null;
}
// 只有右子節點(子樹)
else if (node.left == null) {
return node.right;
}
// 只有左子節點(子樹)
else if (node.right == null) {
return node.left;
}
// 有兩個子節點(子樹)
else {
var tempNode = this.min(node.right);
node.key = tempNode.key;
node.right = this.removeNode(node.right, tempNode.key);
return node;
}
} else if (key < node.key) {
node.left = this.removeNode(node.left, key);
return node;
} else {
node.right = this.removeNode(node.right, key);
return node;
}
}
return removeNode(this.root, key)
}