在这里总结一下二叉搜索树常用的方法。
准备工作:仿照链表的实现,在二叉搜索树类中也需要一个内部节点类。该节点类包含三个属性:存储节点值的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方法外面。