在这里总结一下二叉搜索树常用的方法。

准备工作:仿照链表的实现,在二叉搜索树类中也需要一个内部节点类。该节点类包含三个属性:存储节点值的key、指向左子树的left、指向右子树的right。此外,在二叉搜索树中添加一个root属性,指向根节点。

function Node(key) {
        this.key = key
        this.left = null
        this.right = null
}
this.root = null

 1、insert方法:在二叉树中将指定值插入为叶子结点。

由于二叉树层层递进的特殊结构,很多方法中都可以通过递归调用来实现,所以为了实现一个功能我们通常需要定义两个方法。一个作为入口,另一个作为被递归调用的函数。

入口方法中,我们创建出新节点,并处理了二叉树为空的情况——直接将新节点作为根节点。否则,通过递归来完成。

      BinaySearchTree.prototype.insert = function (key) {
        //  创建新节点
        let newNode = new Node(key)
        if (!this.root) {
          this.root = newNode
        } else {
          this.insertNode(this.root, newNode)
        }
      }

很显然,为了插入到合适的位置,我们需要判断新节点的值与根节点的值孰大孰小,还要判断根节点的左/右子树是否存在——如果不存在则直接插入,如果存在进入下一层递归。

      BinaySearchTree.prototype.insertNode = function (node, newNode) {
        if (newNode.key < node.key) {
          if (!node.left) {
            node.left = newNode
          } else {
            this.insertNode(node.left, newNode)
          }
        } else {
          if (!node.right) {
            node.right = newNode
          } else {
            this.insertNode(node.right, newNode)
          }
        }
      }

2、preOrderTraversal方法:先序遍历,即按照根—左—右的顺序遍历二叉搜索树的所有节点。

我们定义一个数组res来存储所有节点的值。在内部的inOrder方法中,先把当前节点的值压入数组,然后再遍历左子树,最后遍历右子树,每层皆是如此。最终每个节点都以根节点的身份被压入数组,并且按照这个顺序遍历得到的结果就是先序遍历,当遍历至叶子结点时,给下次遍历传入的值为null,所以在进入函数后如果检测到传入的值为假,则直接退出函数。

      BinaySearchTree.prototype.preOrderTraversal = function () {
        const res = []
        const inOrder = (node) => {
          if(!node) return
          res.push(node.val)
          inOrder(node.left)
          inOrder(node.right)
        }
        inOrder(this.root)
        return res
      }    

 3、midOrderTraversal方法:中序遍历,即按照左—根—右的顺序遍历二叉搜索树的所有节点。

中序遍历与先序遍历的实现思路完全一样,无非是递归时,先递归左子树,然后把作为根节点的当前节点压入数组,最后递归右子树。反映到代码中,就是调整这三条语句的顺序。
 
      BinaySearchTree.prototype.midOrderTraversal = function () {
        const res = []
        const inOrder = (node) => {
          if(!node) return
          inOrder(node.left)
          res.push(node.val)
          inOrder(node.right)
        }
        inOrder(this.root)
        return res
      }        

4、postOrderTraversal方法:后序遍历,即按照右—左—根的顺序遍历二叉搜索树的所有节点。

后序遍历的实现思路也一样,按照其定义调整三条语句的顺序即可。

      BinaySearchTree.prototype.midOrderTraversal = function () {
        const res = []
        const inOrder = (node) => {
          if(!node) return
          inOrder(node.right)
          inOrder(node.left)
          res.push(node.val)  
        }
        inOrder(this.root)
        return res
      }     

5、min方法:获取二叉搜索树中的最小值。

根据二叉树的排列方式,最小值位于最左边的节点上。所以直接从根节点开始不断向左遍历即可。当跳出循环时,node恰好指向最小值节点;如果把终止条件改为node == null,则达不到效果。

      BinaySearchTree.prototype.min = function () {
        if (this.root === null) return false
        let node = this.root
        while (node.left !== null) {
          node = node.left
        }
        return node.key
      }

6、max方法:获取二叉搜索树中的最大值。

根据二叉树的排列方式,最大值位于最右边的节点上。所以直接从根节点开始不断向右遍历即可。

      BinaySearchTree.prototype.max = function () {
        if (this.root === null) return false
        let node = this.root
        while (node.right !== null) {
          node = node.right
        }
        return node.key
      }

7、search方法:返回给定值在二叉搜索树中是否存在。

从根节点开始向下遍历。每次循环比较给定值和当前节点值的大小,如相等,则说明存在;若给定值大,则向右查找;若给定值小,则向左查找。若一直找到了某棵子树的叶子结点还未找到,则说明不存在。

      BinaySearchTree.prototype.search = function (key) {
        let node = this.root
        while (node != null) {
          if (key < node.key) {
            node = node.left
          } else if (key > node.key) {
            node = node.right
          } else {
            return true
          }
        }
        return false
      }

8、remove方法:删除二叉树中给定值所对应的节点。

删除操作非常复杂,我们一步一步来实现。

8.1 准备工作

由于删除某个节点后,对其父、子节点均有影响,所以我们设置current变量用来动态遍历各个节点、parent变量用来保存current的父节点、isLeftChild变量表示current是否是parent的左子节点。

let parent = null
let current = this.root
let isLeftChild = true

8.2 判断给定值是否在二叉搜索树中存在。

有了前面的search方法,我们只需要在其基础上稍加改进即可。(主要是isLeftChild和parent的变化)如果不存在,则返回false;如果存在,则将要删除的节点保存在current中,进入下一步。

        while (key !== current.key) {
          parent = current
          if (key < current.key) {
            current = current.left
            isLeftChild = true
          } else {
            current = current.right
            isLeftChild = false
          }
          if (current == null) return false
        }

8.3 如果需要删除的节点current是叶子节点。

删除叶子节点,不会对树的其它部分造成影响。删除操作就相当于把parent的left/right置为null,具体哪一个取决于isLeftChild的值。当然还要考虑一种特殊情况——整棵树只有一个根节点,并且需要删除它。此时parent为null,所以需要单独处理,将root指向null即可。

        if (current.left == null && current.right == null) {
          if (current == this.root) {
            this.root = null
          } else if (isLeftChild) {
            parent.left = null
          } else {
            parent.right = null
          }
        }

8.4 如果需要删除的节点current有一个子节点。

这一情况下我们要进行两层判断:current的子节点是左子还是右子?current本身又是parent的左子还是右子?最后给出四种处理方式。举例来说,比如current的子节点是左子节点,current是parent的右子节点,那么删除操作就相当于把parent的right指向current的left。当然,current为root又成为了一种特殊情况,不过只需要将root指向current的子节点即可。

        else if (current.right == null) {
          if (current == this.root) {
            this.root = current.left
          }
          if (isLeftChild) {
            parent.left = current.left
          } else {
            parent.right = current.left
          }
        } else if (current.left == null) {
          if (current == this.root) {
            this.root = current.right
          } else if (isLeftChild) {
            parent.left = current.right
          } else {
            parent.right = current.right
          }
        }

8.5 如果需要删除的节点current有两个子节点。

此时有两种处理方式:(1)  将current左子树中最大的节点填充到current的位置;  

          (2)  将current右子树中最小的节点填充到current的位置。

两种方法效果类似,这里以第二种为例。

8.5.1 获取current右子树中最小(最左)的节点,记作“后继successor”。

  由于挪走successor后会对其父子节点均产生影响,所以定义successorParent来保存success的父节点。逐级遍历,前一次循环的successor到了下一次循环中就变成了successorParent。最终循环终止时,有两种可能:a. successor就是current的右节点,这意味着current.right.left为null,此时不需要处理successor上下元素之间的对接。b. successor是current右节点的左子树上的节点,注意:successor不可能还有left节点,并且它一定是successorParent的left节点。所以当successor被拿走后,我们需要将successorParent的left指向successor的right。然后将successor放在current的位置,即将successor的right指向current的right。最后将successor返回。

      BinaySearchTree.prototype.getSuccessor = function(delNode){
        let successorParent = delNode
        let successor = delNode
        let current = delNode.right
        while(current != null){
          successorParent = successor
          successor = current
          current = current.left
        }
        if(successor != delNode.right){
          successorParent.left = successor.right
          successor.right = delNode.right
        }
        return successor
      }

8.5.2  正式删除有两个子节点的节点

  如果被删除的节点current为root,则让root指向获取到的后继节点successor;否则让parent的left/right指向successor。最后让successor的left指向被删除节点的left。

        else{
          let successor = this.getSuccessor(current)       
          if(this.root === current){
            this.root = successor
          }else if(isLeftChild){
            parent.left = successor
          }else{
            parent.right = successor
          }
          successor.left = current.left
        }

8.5.3  delete方法小结

可以看出,在把current替换为successor后,需要处理successor和上下两个方向元素之间的关系。我们在getSuccessor方法中处理了对下面右侧元素(successorParent)的影响;而和上方元素(parent)、下方左侧元素的对接,则放在了getSuccessor方法外面。

posted on 2021-06-22 19:32  springxxxx  阅读(67)  评论(0编辑  收藏  举报