JavaScript實現簡單二叉查找樹

前兩天接到了螞蟻金服的面試電話,面試官很直接,上來就拋出了三道算法題。。。node

其中有一道關於二叉樹實現中序遍歷的,當時沒回答好,因此特地學習了一把二叉樹的知識,行文記錄總結。git

二叉樹&二叉查找樹

樹相關術語:

節點: 樹中的每一個元素稱爲一個節點,github

根節點: 位於整棵樹頂點的節點,它沒有父節點, 如上圖 5面試

子節點: 其餘節點的後代算法

葉子節點: 沒有子節點的元素稱爲葉子節點, 如上圖 3 8 24數組

二叉樹:二叉樹就是一種數據結構, 它的組織關係就像是天然界中的樹同樣。官方語言的定義是:是一個有限元素的集合,該集合或者爲空、或者由一個稱爲根的元素及兩個不相交的、被分別稱爲左子樹和右子樹的二叉樹組成。bash

二叉查找樹: 二叉查找樹也叫二叉搜索樹(BST),它只容許咱們在左節點存儲比父節點更小的值,右節點存儲比父節點更大的值,上圖展現的就是一顆二叉查找樹。數據結構

代碼實現

首先建立一個類來表示二叉查找樹,它的內部應該有一個Node類,用來建立節點post

function BinarySearchTree () {
        var Node = function(key) {
            this.key = key,
            this.left = null,
            this.right = null
        }
        var root = null
    }
複製代碼

它還應該有一些方法:學習

  • insert(key) 插入一個新的鍵
  • inOrderTraverse() 對樹進行中序遍歷,並打印結果
  • preOrderTraverse() 對樹進行先序遍歷,並打印結果
  • postOrderTraverse() 對樹進行後序遍歷,並打印結果
  • search(key) 查找樹中的鍵,若是存在返回true,不存在返回fasle
  • findMin() 返回樹中的最小值
  • findMax() 返回樹中的最大值
  • remove(key) 刪除樹中的某個鍵

向樹中插入一個鍵

向樹中插入一個新的鍵,首頁應該建立一個用來表示新節點的Node類實例,所以須要new一下Node類並傳入須要插入的key值,它會自動初始化爲左右節點爲null的一個新節點

而後,須要作一些判斷,先判斷樹是否爲空,若爲空,新插入的節點就做爲根節點,如不爲空,調用一個輔助方法insertNode()方法,將根節點和新節點傳入

this.insert = function(key) {
        var newNode = new Node(key)
        if(root === null) {
            root = newNode
        } else {
            insertNode(root, newNode)
        }
    }
複製代碼

定義一下insertNode() 方法,這個方法會經過遞歸得調用自身,來找到新添加節點的合適位置

var insertNode = function(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)
            }
        }
    } 
複製代碼

完成中序遍歷方法

要實現中序遍歷,咱們須要一個inOrderTraverseNode(node)方法,它能夠遞歸調用自身來遍歷每一個節點

this.inOrderTraverse = function() {
        inOrderTraverseNode(root)
    }
複製代碼

這個方法會打印每一個節點的key值,它須要一個遞歸終止條件————檢查傳入的node是否爲null,若是不爲空,就繼續遞歸調用自身檢查node的left、right節點 實現起來也很簡單:

var inOrderTraverseNode = function(node) {
        if (node !== null) {
            inOrderTraverseNode(node.left)
            console.log(node.key)
            inOrderTraverseNode(node.right)
        }
    }
複製代碼

先序遍歷、後序遍歷

有了中序遍歷的方法,只須要稍做改動,就能夠實現先序遍歷和後序遍歷了 上代碼:

這樣就能夠對整棵樹進行中序遍歷了

// 實現先序遍歷
    this.preOrderTraverse = function() {
        preOrderTraverseNode(root)
    }
    var preOrderTraverseNode = function(node) {
        if (node !== null) {
            console.log(node.key)
            preOrderTraverseNode(node.left)
            preOrderTraverseNode(node.right)
        }
    }

    // 實現後序遍歷
    this.postOrderTraverse = function() {
        postOrderTraverseNode(root)
    }
    var postOrderTraverseNode = function(node) {
        if (node !== null) {
            postOrderTraverseNode(node.left)
            postOrderTraverseNode(node.right)
            console.log(node.key)
        }
    }
複製代碼

發現了吧,其實就是內部語句更換了先後位置,這也恰好符合三種遍歷規則:先序遍歷(根-左-右)、中序遍歷(左-根-右)、中序遍歷(左-右-根)

先來作個測試吧

如今的完整代碼以下:

function BinarySearchTree () {
        var Node = function(key) {
            this.key = key,
            this.left = null,
            this.right = null
        }
        var root = null
        
        //插入節點
        this.insert = function(key) {
            var newNode = new Node(key)
            if(root === null) {
                root = newNode
            } else {
                insertNode(root, newNode)
            }
        }
        var insertNode = function(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() {
            inOrderTraverseNode(root)
        }
        var inOrderTraverseNode = function(node) {
            if (node !== null) {
                inOrderTraverseNode(node.left)
                console.log(node.key)
                inOrderTraverseNode(node.right)
            }
        }
        // 實現先序遍歷
        this.preOrderTraverse = function() {
            preOrderTraverseNode(root)
        }
        var preOrderTraverseNode = function(node) {
            if (node !== null) {
                console.log(node.key)
                preOrderTraverseNode(node.left)
                preOrderTraverseNode(node.right)
            }
        }

        // 實現後序遍歷
        this.postOrderTraverse = function() {
            postOrderTraverseNode(root)
        }
        var postOrderTraverseNode = function(node) {
            if (node !== null) {
                postOrderTraverseNode(node.left)
                postOrderTraverseNode(node.right)
                console.log(node.key)
            }
        }
    }
複製代碼

居然已經完成了添加新節點和遍歷的方式,咱們來測試一下吧:

定義一個數組,裏面有一些元素

var arr = [9,6,3,8,12,15]

咱們將arr中的每一個元素依此插入到二叉搜索樹中,而後打印結果

var tree = new BinarySearchTree()
    arr.map(item => {
        tree.insert(item)
    })
    tree.inOrderTraverse()
    tree.preOrderTraverse()
    tree.postOrderTraverse()
複製代碼

運行代碼後,咱們先來看看插入節點後整顆樹的狀況:

輸出結果

中序遍歷: 3 6 8 9 12 15

先序遍歷: 9 6 3 8 12 15

後序遍歷: 3 8 6 15 12 9

很明顯,結果是符合預期的,因此,咱們用上面的JavaScript代碼,實現了對樹的節點插入,和三種遍歷方法,同時,很明顯能夠看到,在二叉查找樹樹種,最左側的節點的值是最小的,而最右側的節點的值是最大的,因此二叉查找樹能夠很方便的拿到其中的最大值和最小值

查找最小、最大值

怎麼作呢?其實只須要將根節點傳入minNode/或maxNode方法,而後經過循環判斷node爲左側(minNode)/右側(maxNode)的節點爲null

實現代碼:

// 查找最小值
    this.findMin = function() {
        return minNode(root)
    }
    var minNode = function(node) {
        if (node) {
            while (node && node.left !== null) {
                node = node.left
            }
            return node.key
        }
        return null
    }
    
    // 查找最大值
    this.findMax = function() {
        return maxNode(root)
    }
    var maxNode = function (node) {
        if(node) {
            while (node && node.right !== null) {
                node =node.right
            }
            return node.key
        }
        return null
    }
複製代碼

所搜特定值

this.search = function(key) {
    return searchNode(root, key)
}
複製代碼

一樣,實現它須要定義一個輔助方法,這個方法首先會檢驗node的合法性,若是爲null,直接退出,並返回fasle。若是傳入的key比當前傳入node的key值小,它會繼續遞歸查找node的左側節點,反之,查找右側節點。若是找到相等節點,直接退出,並返回true

var searchNode = function(node, key) {
        if (node === null) {
            return false
        }
        if (key < node.key) {
            return searchNode(node.left, key)
        }else if (key > node.key) {
            return searchNode(node.right, key)
        }else {
            return true
        }
    }
複製代碼

移除節點

移除節點的實現狀況比較複雜,它會有三種不一樣的狀況:

  • 須要移除的節點是一個葉子節點

  • 須要移除的節點包含一個子節點

  • 須要移除的節點包含兩個子節點

和實現搜索指定節點一元,要移除某個節點,必須先找到它所在的位置,所以移除方法的實現中部分代碼和上面相同:

// 移除節點
    this.remove = function(key) {
        removeNode(root,key)
    }
    var removeNode = function(node, key) {
        if (node === null) {
            return null
        }
        if (key < node.key) {
            node.left = removeNode(node.left, key)
            return node
        }else if(key > node.key) {
            node.right = removeNode(node.right,key)
            return node
        }else{
            //須要移除的節點是一個葉子節點
            if (node.left === null && node.right === null) {
                node = null
                return node
            }
            //須要移除的節點包含一個子節點
            if (node.letf === null) {
                node = node.right
                return node
            }else if (node.right === null) {
                node = node.left
                return node
            }
            //須要移除的節點包含兩個子節點
            var aux = findMinNode(node.right)
            node.key = aux.key
            node.right = removeNode(node.right, axu.key)
            return node
        }
    }
    var findMinNode = function(node) {
        if (node) {
            while (node && node.left !== null) {
                node = node.left
            }
            return node
        }
        return null
    }
複製代碼

其中,移除包含兩個子節點的節點是最複雜的狀況,它包含左側節點和右側節點,對它進行移除主要須要三個步驟:

  1. 須要找到它右側子樹中的最小節點來代替它的位置
  2. 將它右側子樹中的最小節點移除
  3. 將更新後的節點的引用指向原節點的父節點

有點繞兒,但必須這樣,由於刪除元素後的二叉搜索樹必須保持它的排序性質

測試刪除節點

tree.remove(8)
tree.inOrderTraverse()
複製代碼

打印結果:

3 6 9 12 15

8 這個節點被成功刪除了,可是對二叉查找樹進行中序遍歷依然是保持排序性質的

到這裏,一個簡單的二叉查找樹就基本上完成了,咱們爲它實現了,添加、查找、刪除以及先中後三種遍歷方法

存在的問題

可是實際上這樣的二叉查找樹是存在一些問題的,當咱們不斷的添加更大/更小的元素的時候,會出現以下狀況:

tree.insert(16)
tree.insert(17)
tree.insert(18)
複製代碼

來看看如今整顆樹的狀況:

很容易發現,它是不平衡的,這又會引出平衡樹的概念,要解決這個問題,還須要更復雜的實現,例如:AVL樹,紅黑樹 哎,以後再慢慢去學習吧

關於實現二叉排序樹,我也找到慕課網的一系列的視頻:Javascript實現二叉樹算法, 內容和上述實現基本一致

原文連接:行無忌的成長小屋:JavaScript實現簡單二叉查找樹

相關文章
相關標籤/搜索