TypeScript實現二叉搜索樹

前言

樹是一種非順序數據結構,它用於存儲須要快速查找的數據。現實生活中也有許多用到樹的例子,好比:家譜、公司的組織架構圖等。javascript

本文將詳解二叉搜索樹並用TypeScript將其實現,歡迎各位感興趣的開發者閱讀本文。java

實現思路

二叉樹中的節點最多隻能有兩個子節點:一個是左側子節點, 另外一個是右側子節點。node

二叉搜索樹是二叉樹的一種,它與二叉樹的不一樣之處:git

  • 每一個節點的左子節點必定比自身小
  • 每一個節點的右子節點必定比自身大

本文主要講解樹的具體實現,不瞭解樹基礎知識的開發者請移步: 數據結構: 二叉查找樹github

二叉搜索樹的實現

根據二叉搜索樹的特色咱們可知: 每一個節點都有2個子節點,左子節點必定小於父節點,右子節點必定大於父節點。typescript

建立輔助類Node

首先,咱們須要一個類來描述二叉樹中的每一個節點。由二叉樹的特色可知,咱們建立的類必須包含以下屬性:bash

  • key 節點值
  • left 左子節點的引用
  • right 右子節點的引用

和鏈表同樣,咱們將經過指針來表示節點之間的關係,在雙向鏈表中每一個節點包含兩個指針, 一個指向下一個節點,兩一個指向上一個節點。樹也採用一樣的方式(使用兩個指針), 可是樹的指針一個指向左子節點,另外一個指向右子節點。數據結構

下圖描述了一個二叉樹, 節點與節點之間的連接就是指針。 架構

建立二叉搜索樹的基本結構

咱們採用與鏈表相同的作法,經過聲明一個變量來控制此數據結構的第一個節點(根節點), 在樹中此節點爲root。函數

接下來,咱們來分析下實現一個二叉搜索樹都須要實現的方法:

  • insert(key): 向樹中插入一個節點
  • search(key): 在樹中查找一個鍵。若是節點存在返回true, 不然返回false
  • min(): 返回樹中最小的值 / 鍵
  • max(): 返回書中最大的值 / 鍵
  • remove(key): 從樹中移除某個鍵
向樹中插入一個新的節點
  • 驗證插入操做是否爲特殊狀況: 根節點爲null, 若是根節點爲null則建立一個Node類的實例並將其賦值給root屬性,將root指向新建立的節點
  • 若是根節點不爲null, 則須要尋找合適的位置來插入子節點, 所以咱們須要實現一個輔助方法insertNode
  • insertNode方法接收兩個參數: 要從哪裏開始插,要插入節點的key
  • 因爲二叉搜索樹有一個特色是: 左子節點小於父節點, 右字節點大於父節點。因此咱們須要建立一個比對方法compareFn
  • compareFn方法接收兩個參數: 要比對的值,原值。若是小於則返回-1, 若是大於則返回1, 相等則返回0
  • 調用compareFn方法判斷要插入的鍵是否小於當前節點的鍵
  • 若是小於, 判斷當前節點的左子節點是否爲null, 若是爲null則建立Node節點將其指向左子節點, 不然從當前節點的左子節點位置開始遞歸調用insertNode方法,尋找合適的位置
  • 若是大於, 判斷當前節點的右子節點是否爲null, 若是爲null則建立Node節點將其指向右子節點,不然從當前節點的右子節點位置開始遞歸調用 insertNode方法, 尋找合適的位置

接下來,咱們經過一個例子來描述下上述過程。

咱們要將下述數據插入二叉樹中: 30 21 25 18 17 35 33 31

  • 首先,插入30,此時根節點爲null,建立一個Node節點將其指向root
  • 而後,插入21,此時比較21和30的大小,21 < 30。在30的左子節點插入, 30的左子節點爲null。所以直接插入
  • 而後,插入25 < 30,在30的左子節點插入,此時30的左子節點已經有了21,遞歸調用insertNode,比較25和21的大小,25 > 21,所以在21的右子節點插入
  • 依此類推,直至全部節點都插入。
搜索樹中的值

在樹中,有三種常常執行的搜索類型:

  • 搜索最小值
    • 上個例子,咱們將全部的節點插入到二叉樹後,咱們發現樹最左側的節點是這顆樹中最小的鍵
    • 所以咱們只須要從根節點一直沿着它的左子樹往下找就能夠找到最小節點了
  • 搜索最大值
    • 樹最右側的節點是這顆樹中最大的鍵
    • 所以咱們只須要從根節點一直沿着它的右子樹往下找就能夠找到最大節點了
  • 搜索特定的值
    • 首先聲明一個方法search, search方法接收一個參數:要查找的鍵,它須要一個輔助方法searchNode
    • searchNode接收兩個參數:要查找的節點,要查找的鍵,它能夠用來查找一棵樹或其任意子樹的一個特定的值
    • 首先須要驗證參數傳入的節點是否合法(不是null或undefined), 若是是說明要找的鍵沒找到,返回false
    • 調用compareFn方法比對要查找節點與當前節點的key進行大小比較
    • 若是要查找的節點小於當前節點的key,則遞歸調用searchNode方法,傳當前節點的左子節點和要查找的key
    • 若是要查找的節點大於當前節點的key,則遞歸調用searchNode方法,傳當前節點的右子節點和要抄着的key
    • 不然就是相等,則節點已找到,返回true

接下來,咱們經過一個例子來描述下搜索特定的值的過程。

以下圖所示爲一個二叉搜索樹,咱們須要判斷25是否在樹中:

  • 調用search方法,要查找的鍵爲25,調用searchNode方法,須要從根節點開始找,所以傳root和要查找的key
  • 首先,node不爲空,則繼續判斷key與根節點鍵的大小,25 < 30,遞歸它的左子樹
  • 而後,比較25和21的大小,25 > 21,遞歸它的右子樹
  • 此時,25 === 21,節點已找到,返回true
移除一個節點

移除樹中的節點remove,它接收一個參數key,它須要一個輔助方法removeNode,它接收兩個參數:節點,要移除的key。

removeNode實現思路以下:

  • 首先,判斷節點是否爲null,若是是則證實節點不存在返回null
  • 調用compareFn方法,比較要刪除的key與當前節點key的大小
  • 若是要刪除的key小於當前節點key,則遞歸調用removeNode方法,傳當前節點的左子節點和key
  • 若是要刪除的key大於當前節點key,則遞歸調用removeNode方法,傳當前節點的右子節點和key
  • 不然就是找到了要刪除的節點,刪除節點可能出現三種狀況:
    • 當前節點的左子節點和右子節點都爲null,此時咱們只須要將節點指向null來移除它
    • 當前節點包含一個左子節點或者右子節點,若是左子節點爲null,將當前節點指向當前節點的右子節點。若是右子節點爲null,將當前節點指向當前節點的左子節點。
    • 當前節點有兩個子節點,須要執行4個步驟:
      • 須要先找到它右邊子樹的最小節點
      • 用右子樹最小節點的鍵去更新當前節點的鍵
      • 更新完成後,此時樹中存在了多餘的鍵,須要調用removeNode方法將其刪除
      • 向其父節點更新節點的引用

二叉搜索樹的遍歷

遍歷一棵樹是指訪問樹的每一個節點並對它們進行某種操做的過程,訪問樹的全部節點有三種方式:

  • 中序遍歷
  • 先序遍歷
  • 後序遍歷

樹的遍歷採用遞歸的形式訪問,對遞歸不瞭解的開發者請移步:遞歸的理解與實現

中序遍歷

中序遍歷是一種上行順序訪問樹節點的遍歷方式,即從小到大的順序訪問全部節點。中序遍歷的一種應用場景就是對樹進行排序操做,接下來咱們來分析下中序遍歷的實現:

  • 聲明inOrderTraverse方法接收一個回調函數做爲參數,回調函數用來定義咱們對遍歷到的每一個節點進行的操做,中序遍歷採用遞歸實現,所以咱們須要一個輔助方法inOrderTraverseNode
  • inOrderTraverseNode方法接收兩個參數:節點,回調函數
  • 遞歸的基線條件node==null
  • 遞歸調用inOrderTraverseNode方法,傳當前節點的左子節點和回調函數
  • 調用回調函數
  • 遞歸調用inOrderTraverseNode方法,傳當前節點的右子節點和回調函數

接下來,咱們經過一個例子來描述下中序遍歷的過程

如上圖所示,藍色字標明瞭節點的訪問順序,橙色線條表示遞歸調用直至節點爲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)
複製代碼
先序遍歷

先序遍歷是以優先於後代節點的順序訪問每一個節點的。先序遍歷的一種應用是打印一個結構化的文檔,接下來咱們分析下先序遍歷的具體實現:

  • 聲明preOrderTraverse方法,接收的參數與中序遍歷同樣,它也須要一個輔助方法preOrderTraverseNode
  • preOrderTraverseNode方法接收的參數與中序遍歷同樣
  • 遞歸的基線條件: node == null
  • 調用回調函數
  • 遞歸調用preOrderTraverseNode方法,傳當前節點的左子節點和回調函數
  • 遞歸調用preOrderTraverseNode方法,傳當前節點的右子節點和回調函數

接下來,咱們經過一個例子來描述下先序遍歷的過程:

如上圖所示,藍色字標明瞭節點的訪問順序,橙色線表示遞歸調用直至節點爲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
複製代碼
後序遍歷

後序遍歷則是先訪問節點的後代節點,再訪問節點自己。後序遍歷的一種應用是計算一個目錄及其子目錄中全部文件所佔空間的大小,接下來咱們來分析下後序遍歷的實現:

  • 聲明postOrderTraverse方法,接收的參數與中序遍歷一致
  • 聲明postOrderTraverseNode方法,接收的參數與中序遍歷一致
  • 遞歸的基線條件爲node == null
  • 遞歸調用postOrderTraverseNode方法,傳當前節點的左子節點和回調函數
  • 遞歸調用postOrderTraverseNode方法,傳當前節點的右子節點和回調函數
  • 調用回調函數

接下來,咱們經過一個例子來描述下後序遍歷的執行過程。

如上圖所示,藍色字標示了節點的執行順序,橙色線表示遞歸調用直至節點爲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
複製代碼

實現代碼

前面咱們分析了二叉搜索樹的實現思路,接下來咱們就講上述思路轉換爲代碼。

建立輔助節點

  • 建立Node.ts文件
  • 建立Node類,根據節點的屬性爲類添加對應屬性
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

建立二叉樹的基本結構

  • 建立BinarySearchTree.ts文件
  • 聲明root節點和比對函數
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

執行結果以下:

寫在最後

  • 文中若有錯誤,歡迎在評論區指正,若是這篇文章幫到了你,歡迎點贊和關注😊
  • 本文首發於掘金,未經許可禁止轉載💌
相關文章
相關標籤/搜索