数据结构和算法(十一)之二叉搜索树
一. 二叉搜索树的概念
我们先来简单理解一下什么是二叉搜索树.
什么是二叉搜索树?
-
二叉搜索树(BST,Binary Search Tree),也称二叉排序树或二叉查找树
-
二叉搜索树是一颗二叉树, 可以为空;如果不为空,满足以下性质:
- 非空左子树的所有键值小于其根结点的键值。
- 非空右子树的所有键值大于其根结点的键值。
- 左、右子树本身也都是二叉搜索树。
-
下面哪些是二叉搜索树, 哪些不是?
-
二叉搜索树的特点:
- 二叉搜索树的特点就是相对较小的值总是保存在左结点上, 相对较大的值总是保存在右结点上.
- 那么利用这个特点, 我们可以做什么事情呢?
- 查找效率非常高, 这也是二叉搜索树中, 搜索的来源.
二叉搜索树的操作
- 二叉搜索树有哪些常见的操作呢?
insert(key)
:向树中插入一个新的键。search(key)
:在树中查找一个键,如果结点存在,则返回true
;如果不存在,则返回false
。inOrderTraverse
:通过中序遍历方式遍历所有结点。preOrderTraverse
:通过先序遍历方式遍历所有结点。postOrderTraverse
:通过后序遍历方式遍历所有结点。min
:返回树中最小的值 / 键。max
:返回树中最大的值 / 键。remove(key)
:从树中移除某个键。
二. 二叉搜索树的实现
现在, 我们通过代码来实现二叉搜索树.
创建二叉搜索树
-
我们像封装其他数据结构一样, 先来封装一个 BinarySearchTree 的类
// 创建节点类 class Node { constructor(key) { this.key = key; this.left = null; this.right = null; } }
-
代码解析:
- 封装 BinarySearchTree 的构造函数.
- 还需要封装一个用于保存每一个结点的类 Node.
- 该类包含三个属性: 结点对应的 key, 指向的左子树, 指向的右子树
- 对于 BinarySearchTree 来说, 只需要保存根结点即可, 因为其他结点都可以通过根结点找到.
向树中插入数据
-
我们两个部分来完成这个功能.
-
外界调用的 insert 方法
// 向树中插入数据 insert(key){ // 1.根据key创建对应的node const newNode = new Node(key); // 2.判断根结点是否有值 if (this.root === null) { this.root = newNode } else { this.insertNode(this.root, newNode) } }
-
代码解析:
- 首先, 根据传入的 key, 创建对应的 Node.
- 其次, 向树中插入数据需要分成两种情况:
- 第一次插入, 直接修改根结点即可.
- 其他次插入, 需要进行相关的比较决定插入的位置.
- 在代码中的 insertNode 方法, 我们还没有实现, 也是我们接下来要完成的任务.
-
插入非根结点
insertNode(root, node) { if (newNode.key < node.key) { // 1.准备向左子树插入数据 if (node.left === null) { // 1.1.node的左子树上没有内容 node.left = newNode } else { // 1.2.node的左子树上已经有了内容 this.insertNode(node.left, newNode) } } else { // 2.准备向右子树插入数据 if (node.right === null) { // 2.1.node的右子树上没有内容 node.right = newNode } else { // 2.2.node的右子树上有内容 this.insertNode(node.right, newNode) } } }
-
代码解析:
- 插入其他节点时, 我们需要判断该值到底是插入到左边还是插入到右边.
- 判断的依据来自于新节点的 key 和原来节点的 key 值的比较.
- 如果新节点的 newKey 小于原节点的 oldKey, 那么就向左边插入.
- 如果新节点的 newKey 大于原节点的 oldKey, 那么就向右边插入.
- 代码的 1 序号位置, 就是准备向左子树插入数据. 但是它本身又分成两种情况
- 情况一 (代码 1.1 位置): 左子树上原来没有内容, 那么直接插入即可.
- 情况二 (代码 1.2 位置): 左子树上已经有了内容, 那么就一次向下继续查找新的走向, 所以使用递归调用即可.
- 代码的 2 序号位置, 和 1 序号位置几乎逻辑是相同的, 只是是向右去查找.
- 情况一 (代码 2.1 位置): 左右树上原来没有内容, 那么直接插入即可.
- 情况二 (代码 2.2 位置): 右子树上已经有了内容, 那么就一次向下继续查找新的走向, 所以使用递归调用即可.
-
测试代码: 如果按照下面的代码插入, 最后形成什么样的树呢?
// 测试代码 let bst = new BinarySerachTree() // 插入数据 bst.insert(11) bst.insert(7) bst.insert(15) bst.insert(5) bst.insert(3) bst.insert(9) bst.insert(8) bst.insert(10) bst.insert(13) bst.insert(12) bst.insert(14) bst.insert(20) bst.insert(18) bst.insert(25)
-
形成的树:
-
如果这个时候, 我新插入一个数据 6, 那么插入的位置和顺序应该怎样的呢?
bst.insert(6)
-
新的树:
遍历二叉搜索树
- 前面, 我们向树中插入了很多的数据, 为了能很多的看到测试结果. 我们先来学习一下树的遍历.
- 注意: 这里我们学习的树的遍历, 针对所有的二叉树都是适用的, 不仅仅是二叉搜索树.
- 树的遍历:
- 遍历一棵树是指访问树的每个结点 (也可以对每个结点进行某些操作, 我们这里就是简单的打印)
- 但是树和线性结构不太一样, 线性结构我们通常按照从前到后的顺序遍历, 但是树呢?
- 应该从树的顶端还是底端开始呢? 从左开始还是从右开始呢?
- 二叉树的遍历常见的有三种方式: 先序遍历 / 中序遍历 / 后续遍历. (还有程序遍历, 使用较少, 可以使用队列来完成)
先序遍历
-
遍历过程为:
- ①访问根结点;
- ②先序遍历其左子树;
- ③先序遍历其右子树。
-
遍历过程:
-
遍历的代码实现
preorderTraversal() { const result = []; this.preorderTraversalNode(this.root, result); return result; } preorderTraversalNode(node, result) { if (node === null) return result; result.push(node.key); this.preorderTraversalNode(node.left, result); this.preorderTraversalNode(node.right, result); }
-
测试代码:
// 测试前序遍历结果 let resultString = "" bst.preOrderTraversal(function (key) { resultString += key + " " }) alert(resultString) // 11 7 5 3 6 9 8 10 15 13 12 14 20 18 25
-
代码解析:
- 遍历树最好用的办法就是递归, 因为每个节点都可能有自己的子节点, 所以递归调用是最好的方式.
- 在先序遍历中, 我们在经过节点的时候, 会先将该节点打印出来.
- 然后, 我们会遍历节点的左子树, 再然后遍历节点的右子树.
-
代码先序遍历图解:
中序遍历
-
遍历过程为:
- ①中序遍历其左子树;
- ②访问根结点;
- ③中序遍历其右子树。
-
遍历过程:
-
遍历的代码实现:
// 中序遍历 inorderTraversal() { const result = []; this.inorderTraversalNode(this.root, result); return result; } inorderTraversalNode(node, result) { if (node === null) return result; this.inorderTraversalNode(node.left, result); result.push(node.key); this.inorderTraversalNode(node.right, result); }
-
测试代码:
// 测试中序遍历结果 resultString = "" bst.inOrderTraversal(function (key) { resultString += key + " " }) alert(resultString) // 3 5 6 7 8 9 10 11 12 13 14 15 18 20 25
-
代码解析:
- 先从最左边开始, 进行中序遍历.
- 依次向右移动, 最后遍历最右边.
- 可以根据代码和图片解析来查看. (这里不太好描述, 但是一图胜千言, 大家多看一下图片)
-
代码中序遍历图解:
后序遍历
-
遍历过程为:
- ①后序遍历其左子树;
- ②后序遍历其右子树;
- ③访问根结点。
-
遍历过程:
-
遍历的代码实现:
// 后续遍历 postorderTraversal() { const result = []; this.postorderTraversalNode(this.root, result); return result; } postorderTraversalNode(node, result) { if (node === null) return result; this.postorderTraversalNode(node.left, result); this.postorderTraversalNode(node.right, result); result.push(node.key); }
-
测试代码:
// 测试后续遍历结果 resultString = "" bst.postOrderTraversal(function (key) { resultString += key + " " }) alert(resultString) // 3 6 5 8 10 9 7 12 14 13 18 25 20 15 11
-
后续遍历
- 先遍历左子树上的节点, 再遍历右子树上的节点, 最后遍历根节点. (仔细查看图片和代码)
-
代码后续遍历图解:
最大值 & 最小值
-
在二叉搜索树中搜索最值是一件非常简单的事情, 其实用眼睛看就可以看出来了.
-
下面, 我们通过代码来实现一下.
-
获取最大值 & 最小值:
// 获取最大值和最小值 min() { if (!this.root) return null; let node = this.root; while (node.left !== null) { node = node.left; } return node.key; } max() { if (!this.root) return null; let node = this.root; while (node.right !== null) { node = node.right; } return node.key; }
-
代码解析:
- 代码也是比较简单的:
- 代码依次向左找到最左边的结点就是最小值,
- 代码依次向右找到最右边的结点就是最大值.
- 也可以使用递归来实现, 不过这里就没有什么必要了, 递归反而增加代码的复杂度.
- 代码也是比较简单的:
-
代码测试:
// 获取最值 alert(bst.min()) // 3 alert(bst.max()) // 25
搜索特定的值
-
二叉搜索树不仅仅获取最值效率非常高, 搜索特定的值效率也非常高.
search(key) { return this.searchNode(this.root, key); } // 通过递归实现 searchNode(node, key) { if (node === null) return false; if (key < node.key) { return this.searchNode(node.left, key); } else if (key > node.key) { return this.searchNode(node.right, key); } else { return true; } }
-
代码解析:
- 这里我们还是使用了递归的方式. 待会儿我们来写一个非递归的实现.
- 递归必须有退出条件, 我们这里是两种情况下退出.
- node === null, 也就是后面不再有节点的时候.
- 找到对应的 key, 也就是 node.key === key 的时候.
- 在其他情况下, 根据 node. 的 key 和传入的 key 进行比较来决定向左还是向右查找.
- 如果 node.key > key, 那么说明传入的值更小, 需要向左查找.
- 如果 node.key < key, 那么说明传入的值更大, 需要向右查找.
-
测试代码:
// 查找特定的值 alert(bst.search(10)) // true alert(bst.search(21)) // false
-
非递归代码实现:
search2(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; }
-
递归 or 循环?
- 其实递归和循环之间可以相互转换.
- 大多数情况下, 递归调用可以简化代码, 但是也会增加空间的复杂度.
- 循环空间复杂度较低, 但是代码会相对复杂.
- 可以根据实际的情况自行选择, 不需要套死必须使用某种方式.
三. 二叉搜索树的删除
二叉搜索树的删除有些复杂, 为了大家更加清晰的理解其中的原理, 我单独讲解这部分内容.
删除节点的思路
-
删除节点要从查找要删的节点开始, 找到节点后, 需要考虑三种情况:
- 该节点是也结点 (没有字节点, 比较简单)
- 该节点有一个子节点 (也相对简单)
- 该节点有两个子节点.(情况比较复杂, 我们后面慢慢道来)
-
我们先从查找要删除的节点入手
// 删除结点 remove(key) { let currentNode = this.root; let parentNode = null; let isLeftChild = true; // 循环查找到要删除的节点 currentNode,以及他的parentNode、isLeftChild while (currentNode.key !== key) { parentNode = currentNode; // 小于,往左查找 if (key < currentNode.key) { isLeftChild = true; currentNode = currentNode.left; } else { // 否则往右查找 isLeftChild = false; currentNode = currentNode.right; } // 找到最后没找到相等的节点,返回 false if (currentNode === null) { return false; } } return true }
-
代码解析:
- 在上面的代码序号 1 位置中, 我们先保存了一些临时变量.
- current: 用于一会儿找到的要删除的节点对应的 node.
- parent: 用于保存 current 节点的父节点. 因为如果 current 有子节点, 那么在删除 current 节点的时候, 必然需要将 parent 的 left 或者 right 指向它的某一个子节点. 所以需要保存起来 current 的 parent. (树中的节点关系不能向上的, 和链表非常相似)
- isLeftChild: boolean 类型, 它用户记录我们是在 current 是在父节点的左侧还是右侧, 以便到时候设置 parent 的 left 或者 right
- 在上面的代码序号 2 位置中, 开始查找对应的 key.
- 还是之前的思路, 依次向下找到节点, 同时记录 current/parent/isLeftChild 这些变量
- 如果遍历到 current === null, 那么说明在二叉搜索树中没有该 key, 直接返回 false 即可.
- 如果找到, 后面就需要我们进一步考虑更加复杂的情况了.
- 在上面的代码序号 1 位置中, 我们先保存了一些临时变量.
情况一: 没有子节点
-
情况一: 没有子节点.
- 这种情况相对比较简单, 我们需要检测 current 的 left 以及 right 是否都为 null.
- 都为 null 之后还要检测一个东西, 就是是否 current 就是根, 都为 null, 并且为跟根, 那么相当于要清空二叉树 (当然, 只是清空了根, 因为只有它).
- 否则就把父节点的 left 或者 right 字段设置为 null 即可.
-
图解过程:
-
如果只有一个单独的根, 直接删除即可
-
如果是叶结点, 那么处理方式如下:
-
-
代码实现如下:
// 3.删除的结点是叶结点 if (currentNode.left === null && currentNode.right === null) { if (currentNode === this.root) { this.root = null; } else if (isLeftChild) { parentNode.left = null; } else { parentNode.right = null; } }
-
代码解析:
- 首先, 判断是否是叶结点. 通过 current 的 left&right 是否为 null
- 上面条件成立, 再判断 current 是否是根结点: 回答是, 那么就将 this.root = null 即可.
- 如果不是根, 再判断是左结点, 还是右结点, 以便于将 parent 的 left 或者 right 设置为 null
情况二: 一个子节点
-
情况二: 有一个子节点
- 这种情况也不是很难.
- 要删除的 current 结点, 只有 2 个连接 (如果有两个子结点, 就是三个连接了), 一个连接父节点, 一个连接唯一的子节点.
- 需要从这三者之间: 爷爷 - 自己 - 儿子, 将自己 (current) 剪短, 让爷爷直接连接儿子即可.
- 这个过程要求改变父节点的 left 或者 right, 指向要删除节点的子节点.
- 当然, 在这个过程中还要考虑是否 current 就是根.
-
图解过程:
- 如果是根的情况, 大家可以自己画一下, 比较简单, 这里不再给出.
- 如果不是根, 并且只有一个子节点的情况.
-
代码实现如下:
// 4.删除有一个子节点的节点 else if (current.right === null) { if (currentNode === this.root) { this.root = currentNode.left; } else if (isLeftChild) { parentNode.left = currentNode.left; } else { parentNode.right = currentNode.left; } } else if (current.left === null) { if (currentNode === this.root) { this.root = currentNode.right; } else if (isLeftChild) { parentNode.left = currentNode.right; } else { parentNode.right = currentNode.right; } }
-
代码解析:
- 首先, 我们需要判断是 current 的 left 还是 right 为 null. 因为这样才能决定, 只有我们从 current 中取儿子的时候, 取的是 current.left 还是 current.right 来给别的地方赋值.
- 三种情况:
- current 是根节点, 那么直接将 this.root = son.
- current 不是根节点, 是父节点的 left 节点, 那么 parent.left = son.
- current 不是根节点, 是父节点的 right 节点, 那么 parent.right = son.
- 分析清楚的话, 还比较简单.
情况三: 两个子节点
-
情况三: 有两个子节点.
- 事情变得非常复杂, 也非常有趣了.
-
我们先来思考一下我提出的一些问题:
-
先来, 我们来总结一下删除有两个节点的规律:
- 如果我们要删除的节点有两个子节点, 甚至子节点还有子节点, 这种情况下我们需要从下面的子节点中找到一个节点, 来替换当前的节点.
- 但是找到的这个节点有什么特征呢? 应该是 current 节点下面所有节点中最接近 current 节点的.
- 要么比 current 节点小一点点, 要么比 current 节点大一点点.
- 总结你最接近 current, 你就可以用来替换 current 的位置.
- 这个节点怎么找呢?
- 比 current 小一点点的节点, 一定是 current 左子树的最大值.
- 比 current 大一点点的节点, 一定是 current 右子树的最小值.
- 前驱 & 后继
- 而在二叉搜索树中, 这两个特别的节点, 有两个特比的名字.
- 比 current 小一点点的节点, 称为 current 节点的前驱.
- 比 current 大一点点的节点, 称为 current 节点的后继.
- 也就是为了能够删除有两个子节点的 current, 要么找到它的前驱, 要么找到它的后继.
- 所以, 接下来, 我们先找到这样的节点 (前驱或者后继都可以, 我这里以找后继为例)
-
寻找后继的代码实现:
// 找后继的方法 getSuccessor(delNode) { // 定义变量,保存要找到的后续 let successor = delNode; let current = delNode.right; let successorParent = delNode; // 循环查找 current 的右子树结点 while (current !== null) { successorParent = successor; successor = current; current = current.left; } // 判断寻找到的后续节点是否直接就是要删除节点的 right if (successor !== delNode.right) { successorParent.left = successor.right; successor.right = delNode.right; } return successor; }
-
代码解析:
- 代码是根据传入的 delNode 来寻找后继节点.
- 本身代码比较简单, 但是后面有一个序号 3 的代码, 相对较难理解.
- 我们这里先不做讨论, 先把找到后继后进行的操作写完, 再后头理解这段代码.
- 序号 3: TODO
-
找到后继后的处理代码:
// 5.删除有两个节点的节点 else { // 1.找到后续节点 let successor = this.getSuccessor(currentNode); // 2.判断是否为根结点 if (currentNode === this.root) { this.root = successor; } else if (isLeftChild) { parentNode.left = successor; } else { parentNode.right = successor; } // 3.将后续的左节点改为被删除的左节点 successor.left = currentNode.left; }
-
代码解析:
- 序号 1: 调用刚才封装的方法, 获取后继节点.
- 序号 2: 判断三种情况:
- 情况一: 是根节点, 那么 this.root = successor. 并且 successor 的 left 应该等于 current 的 left
- 情况二: 是父节点的左结点, parent.left = successor, 并且 successor 的 left 应该等于 current 的 left
- 情况三: 是父节点的右结点, parent.right = successor, 并且 successor 的 left 应该等于 current 的 left
- 需要 3: 就是将 successor.left = current.left 从判断中抽取出来.
-
回头头看 TODO 的情况
- 上面的代码实现, 对于删除 9 是适用的. 做法就是将 7 节点的 left 赋值为 10. 10 节点的 left 应该赋值为 8 即可.
- 但是, 对于删除 15 我们还缺少什么呢?
- 已经完成: 11 的 left 指向了 18, 18 的 right 指向了 13.
- 没有完成: 19 怎么办? 20 这个左子树怎么办?
- 很明显, 19 应该放在 20 的左边, 20 应该放在 18 的右边.
- 19 放在 20 的左边代码: successorParent.left = successor.right
- 20 放在 18 的右边代码: successor.right = delNode.right
- 搞定, 收工!!!
删除节点完整代码
-
最后, 还是给出完整代码
remove(key) { let currentNode = this.root; let parentNode = null; let isLeftChild = true; // 循环查找到要删除的节点 currentNode,以及他的 parentNode、isLeftChild while (currentNode.key !== key) { parentNode = currentNode; // 小于,往左查找 if (key < currentNode.key) { isLeftChild = true; currentNode = currentNode.left; } else { // 否则往右查找 isLeftChild = false; currentNode = currentNode.right; } // 找到最后没找到相等的节点,返回 false if (currentNode === null) { return false; } } // 1.删除的是叶子结点的情况 if (currentNode.left === null && currentNode.right === null) { if (currentNode === this.root) { this.root = null; } else if (isLeftChild) { parentNode.left = null; } else { parentNode.right = null; } // 2.删除的是只是一个子节点的节点 } else if (currentNode.right === null) { // currentNode 只存在左节点 // 2.1、currentNode 只存在<左节点>的情况 // 2.1.1、currentNode 等于root // 2.1.2、parentNode.left等于currentNode // 2.1.3、parentNode.right等于currentNode if (currentNode === this.root) { this.root = currentNode.left; } else if (isLeftChild) { parentNode.left = currentNode.left; } else { parentNode.right = currentNode.left; } } else if (currentNode.left === null) { // 2.2、currentNode 只存在<右节点>的情况 // 2.1.1、currentNode 等于root // 2.1.2、parentNode.left 等于 currentNode // 2.1.1、parentNode.rigth 等于 currentNode if (currentNode === this.root) { this.root = currentNode.right; } else if (isLeftChild) { parentNode.left = currentNode.right; } else { parentNode.right = currentNode.right; } // 3.删除的是有两个子节点的节点 } else { // 1.找到后续节点 let successor = this.getSuccessor(currentNode); // 2.判断是否为根结点 if (currentNode === this.root) { this.root = successor; } else if (isLeftChild) { parentNode.left = successor; } else { parentNode.right = successor; } // 3.将后续的左节点改为被删除的左节点 successor.left = currentNode.left; } }
删除节点的回顾
- 看到这里, 你就会发现删除节点相当棘手.
- 实际上, 因为它非常复杂, 一些程序员都尝试着避开删除操作.
- 他们的做法是在 Node 类中添加一个 boolean 的字段, 比如名称为 isDeleted.
- 要删除一个节点时, 就将此字段设置为 true.
- 其他操作, 比如 find() 在查找之前先判断这个节点是不是标记为删除.
- 这样相对比较简单, 每次删除节点不会改变原有的树结构.
- 但是在二叉树的存储中, 还保留着那些本该已经被删除掉的节点.
- 上面的做法看起来很聪明, 其实是一种逃避.
- 这样会造成很大空间的浪费, 特别是针对数据量较大的情况.
- 而且, 作为程序员要学会通过这些复杂的操作, 锻炼自己的逻辑, 而不是避重就轻.
四. 二叉搜索树完整代码
-
最后, 我们还是给出二叉搜索树的完整代码
// 创建节点类 class Node { constructor(key) { this.key = key; this.left = null; this.right = null; } } // 封装二叉搜索树(特点:左子树节点值 < ) export class BinarySearchTree { constructor() { this.root = null; } /******* * @description: 插入数据 * @param {*} key * @return {*} */ insert(key) { const newNode = new Node(key); if (this.root === null) { this.root = newNode; } else { this.insertNode(this.root, newNode); } } insertNode(root, node) { if (node.key < root.key) { // 往左边查找插入 if (root.left === null) { root.left = node; } else { this.insertNode(root.left, node); } } else { // 往右边查找插入 if (root.right === null) { root.right = node; } else { this.insertNode(root.right, node); } } } // ----------- 二叉树遍历 ----------// /******* * @description: 先序遍历(根左右DLR) * @param {*} * @return {*} result */ preorderTraversal() { const result = []; this.preorderTraversalNode(this.root, result); return result; } preorderTraversalNode(node, result) { if (node === null) return result; result.push(node.key); this.preorderTraversalNode(node.left, result); this.preorderTraversalNode(node.right, result); } /******* * @description: 中序遍历(左根右LDR) * @param {*} * @return {*} result */ inorderTraversal() { const result = []; this.inorderTraversalNode(this.root, result); return result; } inorderTraversalNode(node, result) { if (node === null) return result; this.inorderTraversalNode(node.left, result); result.push(node.key); this.inorderTraversalNode(node.right, result); } /******* * @description: 后序遍历(左右根 LRD) * @param {*} * @return {*} restult */ postorderTraversal() { const result = []; this.postorderTraversalNode(this.root, result); return result; } postorderTraversalNode(node, result) { if (node === null) return result; this.postorderTraversalNode(node.left, result); this.postorderTraversalNode(node.right, result); result.push(node.key); } /******* * @description: 获取二叉搜索树最小值 * @param {*} * @return {*} node.key */ min() { if (!this.root) return null; let node = this.root; while (node.left !== null) { node = node.left; } return node.key; } /******* * @description:获取二叉搜索树最大值 * @param {*} * @return {*} node.key */ max() { if (!this.root) return null; let node = this.root; while (node.right !== null) { node = node.right; } return node.key; } /******* * @description: 查找二叉搜索树中是否有相同的key * @param {*} key * @return {*} */ search(key) { return this.searchNode(this.root, key); } // 通过递归实现 searchNode(node, key) { if (node === null) return false; if (key < node.key) { return this.searchNode(node.left, key); } else if (key > node.key) { return this.searchNode(node.right, key); } else { return true; } } /******* * @description: 通过 while 循环实现 * @param {*} key * @return {*} */ /******* * @description: 删除节点 * @param {*} key * @return {*} */ remove(key) { let currentNode = this.root; let parentNode = null; let isLeftChild = true; // 循环查找到要删除的节点 currentNode,以及他的 parentNode、isLeftChild while (currentNode.key !== key) { parentNode = currentNode; // 小于,往左查找 if (key < currentNode.key) { isLeftChild = true; currentNode = currentNode.left; } else { // 否则往右查找 isLeftChild = false; currentNode = currentNode.right; } // 找到最后没找到相等的节点,返回 false if (currentNode === null) { return false; } } // 1.删除的是叶子结点的情况 if (currentNode.left === null && currentNode.right === null) { if (currentNode === this.root) { this.root = null; } else if (isLeftChild) { parentNode.left = null; } else { parentNode.right = null; } // 2.删除的是只是一个子节点的节点 } else if (currentNode.right === null) { // currentNode 只存在左节点 // 2.1、currentNode 只存在<左节点>的情况 // 2.1.1、currentNode 等于root // 2.1.2、parentNode.left等于currentNode // 2.1.3、parentNode.right等于currentNode if (currentNode === this.root) { this.root = currentNode.left; } else if (isLeftChild) { parentNode.left = currentNode.left; } else { parentNode.right = currentNode.left; } } else if (currentNode.left === null) { // 2.2、currentNode 只存在<右节点>的情况 // 2.1.1、currentNode 等于root // 2.1.2、parentNode.left 等于 currentNode // 2.1.1、parentNode.rigth 等于 currentNode if (currentNode === this.root) { this.root = currentNode.right; } else if (isLeftChild) { parentNode.left = currentNode.right; } else { parentNode.right = currentNode.right; } // 3.删除的是有两个子节点的节点 } else { // 1.找到后续节点 let successor = this.getSuccessor(currentNode); // 2.判断是否为根结点 if (currentNode === this.root) { this.root = successor; } else if (isLeftChild) { parentNode.left = successor; } else { parentNode.right = successor; } // 3.将后续的左节点改为被删除的左节点 successor.left = currentNode.left; } } /******* * @description: 获取后续节点,即从要删除的节点的右边开始查找最小值 * @param {*} delNode * @return {*} */ getSuccessor(delNode) { // 定义变量,保存要找到的后续 let successor = delNode; let current = delNode.right; let successorParent = delNode; // 循环查找 current 的右子树结点 while (current !== null) { successorParent = successor; successor = current; current = current.left; } // 判断寻找到的后续节点是否直接就是要删除节点的 right if (successor !== delNode.right) { successorParent.left = successor.right; successor.right = delNode.right; } return successor; } }