資料結構:一文看懂二叉搜尋樹 (JavaScript)

叫我詹躲躲發表於2021-11-20

貓咪寵物商店價目表優惠活動公眾號推送首圖@凡科快圖.png

二叉搜尋樹介紹

  1. 二叉搜尋樹是一種節點值之間具有一定數量級次序的二叉樹,對於樹中每個節點:
  2. 若其左子樹存在,則其左子樹中每個節點的值都不大於該節點值;
  3. 若其右子樹存在,則其右子樹中每個節點的值都不小於該節點值。

滿足條件

  1. 若左子樹不為空,則左子樹上左右節點的值都小於根節點的值;
  2. 若它的右子樹不為空,則它的右子樹上所有的節點的值都大於根節點的值;
  3. 它的左右子樹也要分別是二叉搜尋樹;

查詢節點過程是,比較元素值是否相等,相等則返回,不相等則判斷大小情況,迭代查詢左、右子樹,直到找到相等的元素,或子節點為空,返回節點不存在。

兩種特殊的二叉樹

完全二叉樹,所有節點儘量填滿樹的每一層,上一層填滿後還有剩餘節點的話,則由左向右儘量填滿下一層。

每一層只有一個節點的二叉樹。

Node節點例項

let Node = function (key) {
    this.key = key;
    this.left = null;
    this.right = null;
}
this.roots = null;

例項一個節點

let node = new Node()
console.log(node)
//{ key: undefined, left: null, right: null }

1.二叉樹插入

1.1Node節點例項

//Node節點例項
function Node(key) {
    this.key = key;
    this.left = null;
    this.right = null;
}

1.2二叉樹物件

function BinarySearchTree() {
    this.roots = null;
    this.insert = insert
}

1.3 節點插入(三種情況)

由於二叉搜尋樹的特殊性質確定了二叉搜尋樹中每個元素只可能出現一次,所以在插入的過程中如果發現這個元素已經存在於二叉搜尋樹中,就不進行插入。否則就查詢合適的位置進行插入。

1.3.1 第一種情況:_root為空

直接插入,return true;

let insert = function (key) {
    let newNode = new Node(key)
    if (this.roots === null) {
        this.roots = newNode
    }
}

1.3.2 第二種情況:要插入的元素已經存在

如上面所說,如果在二叉搜尋樹中已經存在該元素,則不再進行插入,直接return false;不再讓它插入;

function insertNode(node, newNode) {
    if(newNode.key === node.key){
        return false
    }
}
節點插入測試

可以看到節點2沒有重複插入;

let BST = new BinarySearchTree();
BST.insert(2)
BST.insert(2)
BST.insert(7)
BST.insert(3)
BST.insert(1)
console.log(BST)

1.3.3 第三種情況:能夠找到合適位置

function insertNode(node, newNode) {
    if(newNode.key === node.key){
        return false
    }
    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)
        }
    }
}

1.3.4節點插入完整版

//Node節點例項
function Node(key) {
    this.key = key;
    this.left = null;
    this.right = null;
}
//二叉搜尋樹
function BinarySearchTree() {
    this.roots = null;
    this.insert = insert
}
let insert = function (key) {
    let newNode = new Node(key)
    if (this.roots === null) {
        this.roots = newNode
    } else {
        insertNode(this.roots, newNode)
    }
}
function insertNode(node, newNode) {
    if(newNode.key === node.key){
        return false
    }
    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)
        }
    }
}

let BST = new BinarySearchTree();
BST.insert(2)
BST.insert(6)
BST.insert(7)
BST.insert(3)
BST.insert(1)
console.log(BST)

2.二叉樹遍歷(四種)

2.1 二叉樹遍歷型別

2.2 前序遍歷

所謂的前序遍歷就是先訪問根節點,再訪問左節點,最後訪問右節點。

那麼遍歷順序就是:8->4->2->6->12->9->15

實現

//先序遍歷是以優先於後代節點的順序訪問每一個節點。
let preOrderTraverse = function (callback) {
    preOrderTraverseNode(this.roots, callback)
}
function preOrderTraverseNode(node, callback) {
    if (node!==null) {
        callback(node.key)
        preOrderTraverseNode(node.left, callback)
        preOrderTraverseNode(node.right, callback)
    }
}
let BST = new BinarySearchTree();
let tree =[8,4,2,6,12,9,15]
tree.forEach(v=>{
    BST.insert(v)
})
BST.preOrderTraverse(key=>console.log(key)) //8,4,2,6,12,9,15

2.3 中序遍歷

所謂的中序遍歷就是先訪問左節點,再訪問根節點,最後訪問右節點。

那麼順序是:2->4->6->8->9->12->15

實現

//中序遍歷是一種以從最小到最大的順序訪問所有節點的遍歷方式
let inOrderTraverse = function (callback) {
    inOrderTraverseNode(this.roots, callback)
}
function inOrderTraverseNode(node, callback) {
    if (node !== null) {
        inOrderTraverseNode(node.left, callback)
        callback(node.key)
        inOrderTraverseNode(node.right, callback)
    }
}
let BST = new BinarySearchTree();
let tree = [8, 4, 2, 6, 12, 9, 15]
tree.forEach(v => {
    BST.insert(v)
})
BST.inOrderTraverse(key => console.log(key)) //2->4->6->8->9->12->15

2.4 後序遍歷

所謂的後序遍歷就是先訪問左節點,再訪問右節點,最後訪問根節點。

那麼順序是:2->6->4->9->15->12->8

實現

//後序遍歷是先訪問節點的後代節點,再訪問節點本身
let postOrderTraverse = function (callback) {
    postOrderTraverseNode(this.roots, callback)
}
function postOrderTraverseNode(node, callback) {
    if (node!==null) {
        postOrderTraverseNode(node.left, callback)
        postOrderTraverseNode(node.right, callback)
        callback(node.key)
    }
}

2.5層序遍歷

二叉樹的層序遍歷,即二叉樹的廣度遍歷,先遍歷根節點的相鄰節點,再一次遍歷相鄰節點的子節點。廣度遍歷通常藉助佇列來實現。用佇列來儲存當前層的節點數,遍歷當前層的節點,將當前層的節點依次推入陣列subRes[],再將當前節點的左右子節點推入佇列中,進入下一層進行遍歷,直到遍歷完整棵樹,即完成到二叉樹的層序遍歷。


圖片來自leetcode題解:BFS 的使用場景總結:層序遍歷、最短路徑問題

非常的精彩,點贊!!!

實現

let levelOrder = function (root) {
    if (!root) return []
    let res = [] //結果最外層陣列
    let queue = [root]
    while (queue.length > 0) {
        var len = queue.length//當前層的節點數目
        var subRes = []
        for (var i = 0; i < len; i++) {
            var node = queue.shift() //節點出列
            subRes.push(node.val) //將當前層的節點值加入subRes陣列中
            //將下一層節點計入佇列中
            if (node.left) {
                queue.push(node.left)
            }
            if (node.right) {
                queue.push(node.right)
            }
        }
        res.push(subRes)
    }
    return res
};

具體的可以檢視 leetcode 這道題的題解:102. 二叉樹的層序遍歷

3 二叉樹搜尋

在 JavaScript 中我們可以通過 hasOwnProperty 來檢測指定 key 在物件是否存在,現在我們在二叉搜尋中實現一個類似的方法,傳入一個值 value 判斷是否在二叉搜尋樹中存在 。

3.1 四種情況

  1. 先判斷傳入的 node 是否為 null,如果為 null 就表示查詢失敗,返回 false。
  2. 找到了節點,返回 true。
  3. 要找的節點,比當前節點小,在左側節點繼續查詢。
  4. 要找的節點,比當前節點大,在右側節點繼續查詢。

實現

let search = function (key) {
    searchNode(this.roots, key)
}
function searchNode(node, key) {
    //查詢失敗,返回 false
    if (node === null) {
        return false;
    }
    //比當前節點小,在左側節點繼續查詢
    if (node.key > key) {
        searchNode(node.left, key)
    }
    //比當前節點大,在右側節點繼續查詢
    if (node.key < key) {
        searchNode(node.right, key)
    }
    return true;
}

4. 查詢最小節點

查詢最小值,往二叉樹的左側查詢,直到該節點 left 為 null 沒有左側節點,證明其是最小值。

實現

//最小值
function min() {
    return minNode(this.roots) || null
}
function minNode(node) {
    console.log(node)
    while (node !== null && node.left !== null) {
        node = node.left
    }
    return node.key
}

let BST = new BinarySearchTree();
let tree = [8, 4, 2, 6, 12, 9, 15]
tree.forEach(v => {
    BST.insert(v)
})
console.log(BST.min())  //2

5.查詢最大節點

查詢最大值,往二叉樹的右側查詢,直到該節點 right 為 null 沒有右側節點,證明其是最大值。

實現

//最大值
let max = function () {
    return maxNode(this.roots) || null
}
function maxNode(node) {
    while (node !== null && node.right !== null) {
        node = node.right
    }
    return node.key
}

let BST = new BinarySearchTree();
let tree = [8, 4, 2, 6, 12, 9, 15]
tree.forEach(v => {
    BST.insert(v)
})
console.log(BST.max()) //15

6.二叉樹節點刪除

  1. 先判斷節點是否為 null,如果等於 null 直接返回。
  2. 判斷要刪除節點小於當前節點,往樹的左側查詢
  3. 判斷要刪除節點大於當前節點,往樹的右側查詢
  4. 節點已找到,另劃分為四種情況:

    4.1.當前節點即無左側節點又無右側節點,直接刪除,返回 null

    4.2. 若左側節點為 null,就證明它有右側節點,將當前節點的引用改為右側節點的引用,返回更新之後的值

    4.3. 若右側節點為 null,就證明它有左側節點,將當前節點的引用改為左側節點的引用,返回更新之後的值

    4.4. 若左側節點、右側節點都不為空情況

實現

//移除節點
let remove = function (key) {
    this.roots = removeNode(this.roots, key)
    console.log(this.roots)
}
function findMinNode(node, key) {
    while (node !== null && node.left !== null) {
        node = findMinNode(node.left, key);
    }
    return node;
}
function removeNode(node, key) {
    //1.要刪除節點小於當前節點,往樹的左側查詢
    if (node.key > key) {
        node.left = removeNode(node.left, key);
        return node;
    }
    //2.要刪除節點大於當前節點,往樹的右側查詢
    if (node.key < key) {
        node.right = removeNode(node.right, key);
        return node;
    }
    if (node.key === key) {
        //1.當前節點即無左側節點又無右側節點,直接刪除,返回 null
        if (node.left === null && node.right === null) {
            return null;
        }
        //2.若右側節點為 null,就證明它有左側節點,將當前節點的引用改為左側節點的引用,返回更新之後的值
        if (node.left !== null && node.right === null) {
            return node.left;
        }
        //3.若左側節點為 null,就證明它有右側節點,將當前節點的引用改為右側節點的引用,返回更新之後的值
        if (node.left === null && node.right !== null) {
            return node.right;
        }
        node.right = findMinNode(node.right, key);
        return node;
    }
}

let BST = new BinarySearchTree();
let tree = [8, 4, 2, 6, 12, 9, 15]
tree.forEach(v => {
    BST.insert(v)
})
console.log(BST.remove(15)) //15

7.搜尋二叉樹完整的程式碼

//基礎類
function BinarySearchTree() {
    let Node = function (key) {
        this.key = key;
        this.left = null;
        this.right = null;
    }
    this.roots = null;
    //二叉樹插入
    this.insert = function (key) {
        let newNode = new Node(key)
        if (this.roots === null) {
            this.roots = newNode
        } else {
            insertNode(this.roots, newNode)
        }
    }
    function 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)
            }
        }
    }

    //中序遍歷是一種以從最小到最大的順序訪問所有節點的遍歷方式
    this.inOrderTraverse = function (callback) {
        inOrderTraverseNode(this.roots, callback)
    }
    function inOrderTraverseNode(node, callback) {
        if (node !== null) {
            inOrderTraverseNode(node.left, callback)
            callback(node.key)
            inOrderTraverseNode(node.right, callback)
        }
    }
    //先序遍歷是以優先於後代節點的順序訪問每一個節點。
    this.preOrderTraverse = function (callback) {
        preOrderTraverseNode(this.roots, callback)
    }
    function preOrderTraverseNode(node, callback) {
        if (node !== null) {
            callback(node.key)
            preOrderTraverseNode(node.left, callback)
            preOrderTraverseNode(node.right, callback)
        }
    }
    //後序遍歷是先訪問節點的後代節點,再訪問節點本身
    this.postOrderTraverse = function (callback) {
        postOrderTraverseNode(this.roots, callback)
    }
    function postOrderTraverseNode(node, callback) {
        if (node !== null) {
            postOrderTraverseNode(node.left, callback)
            postOrderTraverseNode(node.right, callback)
            callback(node.key)
        }
    }

    //搜尋二叉樹
    this.search = function (key) {
        searchNode(this.roots, key)
    }
    function searchNode(node, key) {
        if (node === null) {
            return false;
        }
        if (node.key > key) {
            searchNode(node.left, key)
        }
        if (node.key < key) {
            searchNode(node.right, key)
        }
        return true;
    }
    //最小值
    this.min = function () {
        minNode(this.roots)
    }
    function minNode(node) {
        while (node !== null && node.left !== null) {
            node = node.left
        }
        return node.key
    }
    //最大值
    this.max = function () {
        maxNode(this.roots)
    }
    function maxNode(node) {
        while (node !== null && node.right !== null) {
            node = node.right
        }
        return node.key
    }
    //移除節點
    this.remove = function (key) {
        this.roots = removeNode(this.roots, key)
    }
    function findMinNode(node, key) {
        while (node !== null && node.left !== null) {
            node = findMinNode(node.left, key);
        }
        return node;
    }
    function removeNode(node, key) {
        //1.要刪除節點小於當前節點,往樹的左側查詢
        if (node.key > key) {
            node.left = removeNode(node.left, key);
            return node;
        }
        //2.要刪除節點大於當前節點,往樹的右側查詢
        if (node.key < key) {
            node.right = removeNode(node.right, key);
            return node;
        }
        if (node.key === key) {
            //1.當前節點即無左側節點又無右側節點,直接刪除,返回 null
            if (node.left === null && node.right === null) {
                return null;
            }
            //2.若右側節點為 null,就證明它有左側節點,將當前節點的引用改為左側節點的引用,返回更新之後的值
            if (node.left !== null && node.right === null) {
                return node.left;
            }
            //3.若左側節點為 null,就證明它有右側節點,將當前節點的引用改為右側節點的引用,返回更新之後的值
            if (node.left === null && node.right !== null) {
                return node.right;
            }
            node.right = findMinNode(node.right, key);
            return node;
        }
    }
}
let nodeTree = [1, 12, 2, 3, 4, 5, 14, 6, 19];
let BST = new BinarySearchTree();

nodeTree.forEach(v => {
    BST.insert(v)
})
console.log(BST.remove(19))
console.log(BST.max())
console.log(BST.min())
console.log(BST.preOrderTraverse(key => console.log(key)))
console.log(BST.inOrderTraverse(key => console.log(key)))
console.log(BST.postOrderTraverse(key => console.log(key)))

8.總結

樣的資料,不同的插入順序,樹的結果是不一樣的。這就是二叉樹的侷限性。同時節點過多時,會比較消耗效能。有啥問題可以評論區留言,一起學習,一起精進。也可以 訪問個人部落格地址。叫我詹躲躲

線上DEMO地址線上DEMO地址

9.參考資料

1.二叉樹查詢與節點刪除的javascript實現

2.二叉樹的javascript實現

3.JavaScript二叉樹深入理解

4.資料結構(二):二叉搜尋樹(Binary Search Tree)

5.實現一個二叉搜尋樹(JavaScript 版)

6.二叉搜尋樹的插入與刪除圖解

7.二叉樹的四種遍歷方式

8.102. 二叉樹的層序遍歷

相關文章