樹是一種非順序數據結構,它用於存儲須要快速查找的數據。現實生活中也有許多用到樹的例子,好比:家譜、公司的組織架構圖等。javascript
本文將詳解二叉搜索樹並用TypeScript將其實現,歡迎各位感興趣的開發者閱讀本文。java
二叉樹中的節點最多隻能有兩個子節點:一個是左側子節點, 另外一個是右側子節點。node
二叉搜索樹是二叉樹的一種,它與二叉樹的不一樣之處:git
本文主要講解樹的具體實現,不瞭解樹基礎知識的開發者請移步: 數據結構: 二叉查找樹github
根據二叉搜索樹的特色咱們可知: 每一個節點都有2個子節點,左子節點必定小於父節點,右子節點必定大於父節點。typescript
首先,咱們須要一個類來描述二叉樹中的每一個節點。由二叉樹的特色可知,咱們建立的類必須包含以下屬性:bash
和鏈表同樣,咱們將經過指針來表示節點之間的關係,在雙向鏈表中每一個節點包含兩個指針, 一個指向下一個節點,兩一個指向上一個節點。樹也採用一樣的方式(使用兩個指針), 可是樹的指針一個指向左子節點,另外一個指向右子節點。數據結構
下圖描述了一個二叉樹, 節點與節點之間的連接就是指針。 架構
咱們採用與鏈表相同的作法,經過聲明一個變量來控制此數據結構的第一個節點(根節點), 在樹中此節點爲root。函數
接下來,咱們來分析下實現一個二叉搜索樹都須要實現的方法:
接下來,咱們經過一個例子來描述下上述過程。
咱們要將下述數據插入二叉樹中: 30 21 25 18 17 35 33 31
在樹中,有三種常常執行的搜索類型:
接下來,咱們經過一個例子來描述下搜索特定的值的過程。
以下圖所示爲一個二叉搜索樹,咱們須要判斷25是否在樹中:
移除樹中的節點remove,它接收一個參數key,它須要一個輔助方法removeNode,它接收兩個參數:節點,要移除的key。
removeNode實現思路以下:
遍歷一棵樹是指訪問樹的每一個節點並對它們進行某種操做的過程,訪問樹的全部節點有三種方式:
樹的遍歷採用遞歸的形式訪問,對遞歸不瞭解的開發者請移步:遞歸的理解與實現
中序遍歷是一種上行順序訪問樹節點的遍歷方式,即從小到大的順序訪問全部節點。中序遍歷的一種應用場景就是對樹進行排序操做,接下來咱們來分析下中序遍歷的實現:
接下來,咱們經過一個例子來描述下中序遍歷的過程
如上圖所示,藍色字標明瞭節點的訪問順序,橙色線條表示遞歸調用直至節點爲null而後執行回調函數。 具體的執行過程以下:
11=>7=>5=>3
ode:3,
left:undefined
callback(node.key) 3
right:undefined
allback(node.key) 5
node:5,
right:6
left:undefined
callback(node.key) 6
right: undefined
callback(node.key) 7
node:7,
right:9
left:8
left:undefined
callback(node.key) 8
right:undefined
callback(node.key) 9
right:10
left:undefined
callback(node.key) 10
right:undefined
allback(node.key) 11
node:11,
right:15
left:13
left:12
left:undefined
callback(node.key) 12
right:undefined
callback(node.key) 13
right:14
left:undefined
callback(node.key) 14
right:undefined
...... ...
right:25
left:undefined 25
callback(node.key)
複製代碼
先序遍歷是以優先於後代節點的順序訪問每一個節點的。先序遍歷的一種應用是打印一個結構化的文檔,接下來咱們分析下先序遍歷的具體實現:
接下來,咱們經過一個例子來描述下先序遍歷的過程:
如上圖所示,藍色字標明瞭節點的訪問順序,橙色線表示遞歸調用直至節點爲null而後執行回調函數。具體的執行過程以下:
node:11
callback(node.key) 11
node.left=7
ode:7
callback(node.key) 7
node.left=5
ode:5
callback(node.key) 5
ode.left=3
node:3
callback(node.key) 3
node.left=undefined,node.right=undefined => node:5
node:5
node.right = 6
callback(node.key) 6
node:6
node.left=undefined,node.right=undefined => node:7
node:7
node.right = 9
callback(node.key) 9
...... ...
callback(node.key) 25
複製代碼
後序遍歷則是先訪問節點的後代節點,再訪問節點自己。後序遍歷的一種應用是計算一個目錄及其子目錄中全部文件所佔空間的大小,接下來咱們來分析下後序遍歷的實現:
接下來,咱們經過一個例子來描述下後序遍歷的執行過程。
如上圖所示,藍色字標示了節點的執行順序,橙色線表示遞歸調用直至節點爲null而後執行回調函數。
具體的執行過程以下:
11=>7=>5=>3
node:3
left:undefined
right:undefined
callback(node.key) 3
node:5
right:6
ode:6
left:undefined
right:undefined
callback(node.key) 6
node:5
callback(node.key) 5
node:7
right: 9
node:9
left:8
node:8
left:undefined
right:undefined
callback(node.key) 8
node:9
right:10
node:10
left:undefined
right:undefined
callback(node.key) 10
node:9
callback(node.key) 9
node:7
callback(node.key) 7
... ...
callback(node.key) 11
複製代碼
前面咱們分析了二叉搜索樹的實現思路,接下來咱們就講上述思路轉換爲代碼。
export class Node<K> {
public left: Node<K> | undefined;
public right: Node<K> | undefined;
constructor(public key: K) {
this.left = undefined;
this.right = undefined;
}
toString() {
return `${this.key}`;
}
}
複製代碼
完整代碼請移步: Node.ts
protected root: Node<T> | undefined;
constructor(protected compareFn: ICompareFunction<T> = defaultCompare) {
this.root = undefined;
};
複製代碼
export type ICompareFunction<T> = (a: T, b: T) => number;
export function defaultCompare<T>(a:T, b:T) {
if (a === b){
return Compare.EQUALS;
}else if(a > b) {
return Compare.BIGGER_THAN;
}else {
return Compare.LESS_THAN;
}
}
複製代碼
insert(key: T){
if (this.root == null){
// 若是根節點不存在則直接新建一個節點
this.root = new Node(key);
}else {
// 在根節點中找合適的位置插入子節點
this.insertNode(this.root,key);
}
}
protected insertNode(node: Node<T>, key: T) {
// 新節點的鍵小於當前節點的鍵,則將新節點插入當前節點的左邊
// 新節點的鍵大於當前節點的鍵,則將新節點插入當前節點的右邊
if (this.compareFn(key,node.key) === Compare.LESS_THAN){
if (node.left == null){
// 當前節點的左子樹爲null直接插入
node.left = new Node(key);
}else {
// 從當前節點(左子樹)向下遞歸,找到null位置將其插入
this.insertNode(node.left,key);
}
}else{
if (node.right == null){
// 當前節點的右子樹爲null直接插入
node.right = new Node(key);
}else {
// 從當前節點(右子樹)向下遞歸,找到null位置將其插入
this.insertNode(node.right,key);
}
}
}
複製代碼
// 搜索特定節點的值
search(key: T){
return this.searchNode(<Node<T>>this.root, key);
}
private searchNode(node: Node<T>, key: T): boolean | Node<T>{
if (node == null){
return false;
}
if (this.compareFn(key, node.key) === Compare.LESS_THAN){
// 要查找的key在節點的左側
return this.searchNode(<Node<T>>node.left, key);
} else if(this.compareFn(key, node.key) === Compare.BIGGER_THAN){
// 要查找的key在節點的右側
return this.searchNode(<Node<T>>node.right, key);
} else{
// 節點已找到
return true;
}
}
// 獲取樹的最小節點
min(){
return this.minNode(<Node<T>>this.root);
}
protected minNode(node: Node<T>): Node<T>{
let current = node;
while (current != null && current.left != null){
current = current.left;
}
return current;
}
// 獲取樹的最大節點
max(){
return this.maxNode(<Node<T>>this.root);
}
private maxNode(node: Node<T>){
let current = node;
while (current != null && current.right != null){
current = current.right;
}
return current;
}
複製代碼
remove(key: T){
this.removeNode(<Node<T>>this.root,key);
}
protected removeNode(node: Node<T> | null, key: T){
// 正在檢測的節點爲null,即鍵不存在於樹中
if (node == null){
return null;
}
// 不爲null,須要在樹中找到要移除的鍵
if (this.compareFn(key,node.key) === Compare.LESS_THAN){ // 目標key小於當前節點的值則沿着樹的左邊找
node.left = <Node<T>>this.removeNode(<Node<T>>node.left, key);
return node;
} else if (this.compareFn(key,node.key) === Compare.BIGGER_THAN){ // 目標key大於當前節點的值則沿着樹的右邊找
node.right = <Node<T>>this.removeNode(<Node<T>>node.right, key);
return node;
} else{
// 鍵等於key,須要處理三種狀況
if (node.left == null && node.right == null){ // 移除一個葉節點,即該節點沒有左、右子節點
// 將節點指向null來移除它
node = null;
return node;
}
if (node.left == null){ // 移除一個左側子節點的節點
// node有一個右側子節點,所以須要把對它的引用改成對它右側子節點的引用
node = <Node<T>>node.right;
// 返回更新後的節點
return node;
} else if(node.right == null){ // 移除一個右側子節點的節點
// node有一個左側子節點,所以須要把對它的引用改成對它左側子節點的引用
node = node.left;
// 返回更新後的節點
return node;
}
// 移除有兩個子節點的節點
const aux = this.minNode(node.right); // 當找到了要移除的節點後,須要找到它右邊子樹最小的節點,即它的繼承者
node.key = aux.key; // 用右側子樹最小的節點的鍵去更新node的鍵
// 更新完node的鍵後,樹中存在了兩個相同的鍵,所以須要移除多餘的鍵
node.right = <Node<T>>this.removeNode(node.right, aux.key) // 移除右側子樹中的最小節點
return node; // 返回更新後的節點
}
}
複製代碼
接下來咱們實現中序、先序、後序遍歷
inOrderTraverse(callback: Function){
this.inOrderTraverseNode(<Node<T>>this.root,callback);
}
private inOrderTraverseNode(node: Node<T>,callback: Function){
if (node !=null){
this.inOrderTraverseNode(<Node<T>>node.left,callback);
callback(node.key);
this.inOrderTraverseNode(<Node<T>>node.right,callback);
}
}
複製代碼
preOrderTraverse(callback: Function){
this.preOrderTraverseNode(<Node<T>>this.root, callback);
}
private preOrderTraverseNode(node: Node<T>, callback: Function){
if (node != null){
callback(node.key);
this.preOrderTraverseNode(<Node<T>>node.left, callback);
this.preOrderTraverseNode(<Node<T>>node.right, callback);
}
}
複製代碼
postOrderTraverse(callback: Function){
this.postOrderTraverseNode(<Node<T>>this.root, callback);
}
private postOrderTraverseNode(node: Node<T>, callback: Function) {
if (node != null){
this.postOrderTraverseNode(<Node<T>>node.left, callback);
this.postOrderTraverseNode(<Node<T>>node.right, callback);
callback(node.key);
}
}
複製代碼
完整代碼請移步: BinarySearchTree.ts
前面咱們實現了二叉搜索樹以及它的基本方法,接下來咱們來測試下上述代碼是否都能正常執行。
const binarySearchTree = new BinarySearchTree();
binarySearchTree.insert(11);
binarySearchTree.insert(7);
binarySearchTree.insert(15);
binarySearchTree.insert(5);
binarySearchTree.insert(3);
binarySearchTree.insert(9);
binarySearchTree.insert(8);
binarySearchTree.insert(10);
binarySearchTree.insert(13);
binarySearchTree.insert(12);
binarySearchTree.insert(14);
binarySearchTree.insert(20);
binarySearchTree.insert(18);
binarySearchTree.insert(25);
binarySearchTree.insert(6);
// 測試中序遍歷函數
const printNode = (value) => console.log(value);
console.log("中序遍歷");
binarySearchTree.inOrderTraverse(printNode);
// 測試先序遍歷
console.log("先序遍歷");
binarySearchTree.preOrderTraverse(printNode);
// 測試後序遍歷
console.log("後序遍歷");
binarySearchTree.postOrderTraverse(printNode);
// 測試獲取最小值函數
console.log("樹的最小值",binarySearchTree.min());
// 測試獲取最大值函數
console.log("樹的最大值",binarySearchTree.max());
// 測試搜索節點函數
console.log("8在二叉樹中",binarySearchTree.search(8));
console.log("100在二叉樹中",binarySearchTree.search(100));
// 測試節點刪除
console.log("刪除節點3");
binarySearchTree.remove(3);
binarySearchTree.inOrderTraverse(printNode);
複製代碼
完整代碼請移步: BinarySearchTreeTest.js
執行結果以下: