JS数据结构与算法==>二叉搜索树

66 阅读17分钟

二叉搜索树

学起来比之前学过的栈,队列,链表之类的都要复杂很多,大部分的方法既可以通过循环实现,也可以通过递归实现,总体来说,通过循环实现效率更高但代码量更大,通过递归实现效率更低但代码量更小,关键点还是在于先理解一遍思路,再开始写。

二叉搜索树

二叉搜索树(BST,Binary Search Tree),也称为二叉排序树和二叉查找树。

二叉搜索树是一棵二叉树,可以为空。

如果不为空,则满足以下性质

  • 条件 1:非空左子树的所有键值小于其根节点的键值。比如三中节点 6 的所有非空左子树的键值都小于 6;
  • 条件 2:非空右子树的所有键值大于其根节点的键值;比如三中节点 6 的所有非空右子树的键值都大于 6;
  • 条件 3:左、右子树本身也都是二叉搜索树;

image-20230418211447841

如上图所示,树二和树三符合 3 个条件属于二叉树,树一不满足条件 3 所以不是二叉树。

总结:二叉搜索树的特点主要是相对较小的值总是保存在左节点上,相对较大的值总是保存在右节点上。这种特点使得二叉搜索树的查找效率非常高,这也就是二叉搜索树中“搜索”的来源。

二叉搜索树应用举例

下面是一个二叉搜索树:

image-20230418211538959

若想在其中查找数据 10,只需要查找 4 次,查找效率非常高。

  • 第 1 次:将 10 与根节点 9 进行比较,由于 10 > 9,所以 10 下一步与根节点 9 的右子节点 13 比较;
  • 第 2 次:由于 10 < 13,所以 10 下一步与父节点 13 的左子节点 11 比较;
  • 第 3 次:由于 10 < 11,所以 10 下一步与父节点 11 的左子节点 10 比较;
  • 第 4 次:由于 10 = 10,最终查找到数据 10 。

image-20230418211524637

同样是 15 个数据,在排序好的数组中查询数据 10,需要查询 10 次:

image-20230418211551584

其实:如果是排序好的数组,可以通过二分查找:第一次找 9,第二次找 13,第三次找 11...。我们发现如果把每次二分的数据拿出来以树的形式表示的话就是二叉搜索树。这就是数组二分法查找效率之所以高的原因。

二叉搜索树的封装

二叉搜索树有四个最基本的属性:指向节点的根(root),节点中的键(key)、左指针(left)、右指针(right)。

image-20230418211605603

所以,二叉搜索树中除了定义 root 属性外,还应定义一个节点内部类,里面包含每个节点中的 left、right 和 key 三个属性。

// 节点类
class Node {
  constructor(key) {
    this.key = key //节点值
    this.left = null // 左侧子节点引用
    this.right = null //右侧子节点引用
  }
}

二叉搜索树的常见操作

  • insert(key) 向树中插入一个新的键。
  • search(key) 在树中查找一个键,如果节点存在,则返回 true;如果不存在,则返回 false
  • preOrderTraverse 通过先序遍历方式遍历所有节点。
  • inOrderTraverse 通过中序遍历方式遍历所有节点。
  • postOrderTraverse 通过后序遍历方式遍历所有节点。
  • min 返回树中最小的值/键。
  • max 返回树中最大的值/键。
  • remove(key) 从树中移除某个键。

插入数据

实现思路:

  • 首先根据传入的 key 创建节点对象。
  • 然后判断根节点是否存在,不存在时通过:this.root = newNode,直接把新节点作为二叉搜索树的根节点。
  • 若存在根节点则重新定义一个内部方法 insertNode() 用于查找插入点。

insert(key) 代码实现

  // 1.插入 -- 如何插入? ==> 通过比大小,在空节点处插入
  insert(value) {
    let newNode = new node(value)
    // return this.insertNodeByIteration(this.root, newNode)
    return this.insertNodeByRecursion(this.root, newNode)
    
  }

insertNode() 的实现思路:

两种思路--循环 / 递归

先看递归:

根据比较传入的两个节点,一直查找新节点适合插入的位置,直到成功插入新节点为止。

  • 当 newNode.key < node.key 向左查找:

    • 情况 1:当 node 无左子节点时,直接插入:
    • 情况 2:当 node 有左子节点时,递归调用 insertNode(),直到遇到无左子节点成功插入 newNode 后,不再符合该情况,也就不再调用 insertNode(),递归停止。
  • 当 newNode.key >= node.key 向右查找,与向左查找类似:

    • 情况 1:当 node 无右子节点时,直接插入:
    • 情况 2:当 node 有右子节点时,依然递归调用 insertNode(),直到遇到传入 insertNode 方法 的 node 无右子节点成功插入 newNode 为止。

insertNode(root, node) 代码实现

  /**
   *
   * @param {*} current 当前遍历的节点(从根节点开始)
   * @param {*} newNode 要插入的新节点
   */  
insertNodeByRecursion(current, newNode) {
    // 还是优先考虑根节点为空的情况
    if (!this.root) {
      this.root = newNode
      return true
    }
    // 比大小,然后判断current子节点是否为空,若为空直接替代,不为空则递归调用
    if (newNode.value < current.value) {
      // 进入左子树
      if (!current.left) {
        // 判空,为空则直接替代
        current.left = newNode
        return true
      } else {
        // 不为空,则递归
        return this.insertNodeByRecursion(current.left, newNode)
      }
    } else if (newNode.value > current.value) {
      // 进入右子树
      if (!current.right) {
        // 判空,为空则直接替代
        current.right = newNode
        return true
      } else {
        // 不为空,则递归
        return this.insertNodeByRecursion(current.right, newNode)
      }
    } else {
      return '该节点已存在'
    }
  }

再看循环:

  /**
   *
   * @param {*} current 当前遍历的节点(从根节点开始)
   * @param {*} newNode 要插入的新节点
   * @returns
   */
  insertNodeByIteration(current, newNode) {
    // 先分析root为空的情况,此时新的节点直接取代root
    if (!current) {
      this.root = newNode
      return true
    }
    // 此处分析root不为空的情况
    while (current) {
      if (newNode.value < current.value) {
        // 往左子树处遍历
        // current的左子树是否为空,若为空则直接插入,否组继续向下遍历
        if (current.left) {
          //  不为空的情况
          current = current.left
        } else {
          // 为空的情况
          current.left = newNode
          return true
        }
      } else if (newNode.value > current.value) {
        // 往右子树处遍历
        // current的右子树是否为空,若为空则直接插入,否组继续向下遍历
        if (current.right) {
          //  不为空的情况
          current = current.right
        } else {
          // 为空的情况
          current.right = newNode
          return true
        }
      } else {
        return '该节点已存在'
      }
    }
  }

遍历数据

这里所说的树的遍历不仅仅针对二叉搜索树,而是适用于所有的二叉树。由于树结构不是线性结构,所以遍历方式有多种选择,常见的三种二叉树遍历方式为:

  • 先序遍历;
  • 中序遍历;
  • 后序遍历;

还有层序遍历,使用较少。

先序遍历

先序遍历的过程为:

  1. 访问根节点;
  2. 先序遍历其左子树;
  3. 先序遍历其右子树;

image-20230418211621242

如上图所示,二叉树的节点遍历顺序为:A -> B -> D -> H -> I -> E -> C -> F -> G。

代码实现:

// 先序遍历(根左右 DLR)
preOrderTraverse(callback) {
  this.preOrderTraverseNode(this.root, callback)
}
​
preOrderTraverseNode(node, callback) {
  // 检查以参数形式传入的节点是否为null,是递归算法的基线条件
  if (node !== null) {
    // 访问根节点
    callback(node.key)
    // 先序遍历其左子树
    this.preOrderTraverseNode(node.left, callback)
    // 先序遍历其右子树
    this.preOrderTraverseNode(node.right, callback)
  }
}

中序遍历

实现思路:与先序遍历原理相同,只不过是遍历的顺序不一样了。

  1. 中序遍历其左子树;
  2. 访问根节点;
  3. 中序遍历其右子树;

过程图解:

image-20230418211634524

输出节点的顺序应为:3 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 11 -> 12 -> 13 -> 14 -> 15 -> 18 -> 20 -> 25 。

代码实现:

// 中序遍历(左根右 LDR)
inOrderTraverse(callback) {
  this.inOrderTraverseNode(this.root, callback)
}
​
inOrderTraverseNode(node, callback) {
  if (node != null) {
    // 中序遍历其左子树
    this.inOrderTraverseNode(node.left, callback)
    // 访问根节点
    callback(node.key)
    // 中序遍历其右子树
    this.inOrderTraverseNode(node.right, callback)
  }
}

后序遍历

实现思路:与先序遍历原理相同,只不过是遍历的顺序不一样了。

  1. 后序遍历其左子树;
  2. 后序遍历其右子树;
  3. 访问根节点;

过程图解:

image-20230418211647361

image-20230418145545490

输出节点的顺序应为:3 -> 6 -> 5 -> 8 -> 10 -> 9 -> 7 -> 12 -> 14 -> 13 -> 18 -> 25 -> 20 -> 15 -> 11 。

代码实现:

// 后序遍历(左右根 LRD)
postOrderTraverse(callback) {
  this.postOrderTraverseNode(this.root, callback)
}
​
postOrderTraverseNode(node, callback) {
  if (node != null) {
    // 后序遍历其左子树
    this.postOrderTraverseNode(node.left, callback)
    // 后序遍历其右子树
    this.postOrderTraverseNode(node.right, callback)
    // 访问根节点
    callback(node.key)
  }
}

总结

以遍历根(父)节点的顺序来区分三种遍历方式。比如:先序遍历先遍历根节点、中序遍历第二遍历根节点、后续遍历最后遍历根节点。

查找数据

查找最大值或最小值

在二叉搜索树中查找最值非常简单,最小值在二叉搜索树的最左边,最大值在二叉搜索树的最右边。只需要一直向左/右查找就能得到最值,如下图所示:

代码实现:

// min() 获取二叉搜索树最小值
min() {
  return this.minNode(this.root)
}
​
// min()的辅助方法,查找以节点node为根节点的树的最小的节点
minNode(node) {
  if (!node) return null
  let current = node
  while (current.left !== null) {
    current = current.left
  }
  return current
}
​
// max() 获取二叉搜索树最大值
max() {
  return this.maxNode(this.root)
}
​
// max()的辅助方法,查找以节点node为根节点的树的最大的节点
maxNode(node) {
  let current = node
  while (current != null && current.right != null) {
    current = current.right
  }
  return current
}

查找特定值

查找二叉搜索树当中的特定值效率也非常高。只需要从根节点开始将需要查找节点的 key 值与之比较,若 node.key < root 则向左查找,若 node.key > root 就向右查找,直到找到或查找到 null 为止。这里可以使用递归实现,也可以采用循环来实现。

代码实现:

// search(key) 查找BST中是否有特定的值key,存在返回 true,否则返回 false
search(key) {
  return this.searchNode(this.root, key)
}
​
//search(key)的辅助方法,通过递归实现
searchNode(node, key) {
  if (node === null) return false
  if (key < node.key) {
    // 这里注意要加return,否则此条件下无返回值了
    return this.searchNode(node.left, key)
  } else if (key > node.key) {
    return this.searchNode(node.right, key)
  } else {
    return true
  }
}
​
// 迭代实现查找BST某一个特定的值
searchIterative(key) {
  // 取得根节点
  let node = this.root
  // 只要当前节点非空,就继续查找
  while (node !== null) {
    // 要查找的数据小于当前节点的数据,则向左查找
    if (key < node.key) {
      node = node.left
    // 要查找的数据大于当前节点的数据,则向右查找
    } else if (key > node.key) {
      node = node.right
    // 找到该数据,返回true
    } else {
      return true
    }
  }
  // 查到叶节点也没找到,说明无此数据,返回false
  return false
}

删除数据(通过循环迭代实现)

实现思路:

第一步:先找到需要删除的节点,若没找到,则不需要删除;

首先定义变量 current 用于保存需要 删除的节点、变量 parent 用于保存它的父节点、变量 isLeftChild 保存 current 是否为 parent 的左节点,这样方便之后删除节点时改变相关节点的指向。

// 二叉树为空,直接返回false
if (!this.root) return false// 寻找要删除的节点
// 定义临时保存的变量
let current = this.root //查找的当前节点
let parent = null //当前节点的父节点
let isLeftChild = true //当前节点是否为父节点的左子节点// 开始寻找要删除的节点
while (current.key !== key) {
  parent = current
  // 要删除的节点小于当前节点,往左找
  if (key < current.key) {
    isLeftChild = true
    current = current.left
  // 要删除的节点大于当前节点,往右找
  } else {
    isLeftChild = false
    current = current.right
  }
  // 查找到叶节点仍未找到,返回false
  if (current === null) {
    return false
  }
}

第二步:删除找到的指定节点,后分 3 种情况:

  • 删除的是叶子节点;
  • 删除的是只有一个子节点的节点;
  • 删除的是有两个子节点的节点;

删除的是叶子节点

删除的是叶子节点分两种情况:

  • 叶子节点也是根节点

    当该叶子节点为根节点时,如下图所示,此时 current == this.root,直接通过:this.root = null,删除根节点。

  • 叶子节点不为根节点

    当该叶子节点不为根节点时也有两种情况,如下图所示

    若 current = 8,可以通过:parent.left = null,删除节点 8;

    若 current = 10,可以通过:parent.right = null,删除节点 10;

    代码实现:

    // 1、删除的是叶子节点的情况
    if (current.left === null && current.right === null) {
      // 叶子节点同时是根节点
      if (current === this.root) {
        this.root = null
        // 是父节点的左子节点
      } else if (isLeftChild) {
        parent.left = null
        // 是父节点的右子节点
      } else {
        parent.right = null
      }
    

删除的是只有一个子节点的节点

有六种情况:

当 current 存在左子节点时(current.right == null):

  • 情况 1:current 为根节点(current == this.root),如节点 11,此时通过:this.root = current.left,删除根节点 11;
  • 情况 2:current 为父节点 parent 的左子节点(isLeftChild == true),如节点 5,此时通过:parent.left = current.left,删除节点 5;
  • 情况 3:current 为父节点 parent 的右子节点(isLeftChild == false),如节点 9,此时通过:parent.right = current.left,删除节点 9;

当 current 存在右子节点时(current.left = null):

  • 情况 4:current 为根节点(current == this.root),如节点 11,此时通过:this.root = current.right,删除根节点 11。
  • 情况 5:current 为父节点 parent 的左子节点(isLeftChild == true),如节点 5,此时通过:parent.left = current.right,删除节点 5;
  • 情况 6:current 为父节点 parent 的右子节点(isLeftChild == false),如节点 9,此时通过:parent.right = current.right,删除节点 9;

代码实现:

// 2、删除的是只有一个子节点的节点
} else if (current.right === null) {
  //-- 2.1、要删除的节点只存在<左子节点>的情况
​
  //---- 2.1.1、要删除的节点为根节点
  if (current === this.root) {
    this.root = current.left
    //---- 2.1.2、要删除的节点是其父节点的左子节点
  } else if (isLeftChild) {
    parent.left = current.left
    //---- 2.1.3、要删除的节点是其父节点的右子节点
  } else {
    parent.right = current.left
  }
} else if (current.left === null) {
  //-- 2.2、要删除的节点只存在<右子节点>的情况
​
  //---- 2.2.1 要删除的节点为根节点
  if (current === this.root) {
    this.root = current.right
    //---- 2.2.2 要删除的节点是其父节点的左子节点
  } else if (isLeftChild) {
    parent.left = current.right
    //---- 2.2.3 要删除的节点是其父节点的右子节点
  } else {
    parent.right = current.right
  }

删除的是有两个子节点的节点

这种情况十分复杂,首先依据以下二叉搜索树,讨论这样的问题:

删除节点 9

在保证删除节点 9 后原二叉树仍为二叉搜索树的前提下,有两种方式:

  • 方式 1:从节点 9 的左子树中选择一合适的节点替代节点 9,可知节点 8 符合要求;
  • 方式 2:从节点 9 的右子树中选择一合适的节点替代节点 9,可知节点 10 符合要求;

删除节点 7

在保证删除节点 7 后原二叉树仍为二叉搜索树的前提下,也有两种方式:

  • 方式 1:从节点 7 的左子树中选择一合适的节点替代节点 7,可知节点 5 符合要求;
  • 方式 2:从节点 7 的右子树中选择一合适的节点替代节点 7,可知节点 8 符合要求;

删除节点 15

在保证删除节点 15 后原树二叉树仍为二叉搜索树的前提下,同样有两种方式:

  • 方式 1:从节点 15 的左子树中选择一合适的节点替代节点 15,可知节点 14 符合要求;
  • 方式 2:从节点 15 的右子树中选择一合适的节点替代节点 15,可知节点 18 符合要求;

相信你已经发现其中的规律了!

规律总结:如果要删除的节点有两个子节点,甚至子节点还有子节点,这种情况下需要从要删除节点下面的子节点中找到一个合适的节点,来替换当前的节点

若用 current 表示需要删除的节点,则合适的节点指的是:

  • current 左子树中比 current 小一点点的节点,即 current 左子树中的最大值
  • current 右子树中比 current 大一点点的节点,即 current 右子树中的最小值
前驱&后继

在二叉搜索树中,这两个特殊的节点有特殊的名字:

  • 比 current 小一点点的节点,称为 current 节点的前驱。比如下图中的节点 5 就是节点 7 的前驱;
  • 比 current 大一点点的节点,称为 current 节点的后继。比如下图中的节点 8 就是节点 7 的后继;

也就是说为了能够删除有两个子节点的current, 要么找到它的前驱, 要么找到它的后继.

查找需要被删除的节点 current 的后继时,需要在 current 的右子树中查找最小值,即在 current 的右子树中一直向左遍历查找;

查找前驱时,则需要在 current 的左子树中查找最大值,即在 current 的左子树中一直向右遍历查找。

下面只讨论查找 current 后继的情况,查找前驱的原理相同,这里暂不讨论。

代码实现:

// 3、删除的是有两个子节点的节点
  } else {
​
    // 3.1 找到要删除节点的后继节点
    const successor = this.getSuccessor(current)
​
    // 3.2.1 要删除的节点为根节点
    if (current === this.root) {
      this.root = successor
      // 3.2.2 要删除的节点是其父节点的左子节点
    } else if (isLeftChild) {
      parent.left = successor
      // 3.2.3 要删除的节点是其父节点的右子节点
    } else {
      parent.right = successor
    }
​
    // 3.3 将后继节点的左子节点改为被删除的节点的左子节点
    successor.left = current.left
  }
}
​
// 获取要删除节点的后继节点,即从要删除的节点的右边开始查找最小的值
getSuccessor(delNode) {
​
  // 定义状态变量,保存要找到的后续节点相关的信息
  let successor = delNode     //保存后继节点
  let successorParent = null  //保存后继节点的父节点
  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
}

完整实现

// 删除节点,通过循环迭代实现
removeIterative(key) {
  // 二叉树为空,直接返回false
  if (!this.root) return false
​
  // 寻找要删除的节点
  // 定义临时保存的变量
  let current = this.root //查找的当前节点
  let parent = null //当前节点的父节点
  let isLeftChild = true //当前节点是否为父节点的左子节点
​
  // 开始寻找要删除的节点
  while (current.key !== key) {
    parent = current
    // 要删除的节点小于当前节点,往左找
    if (key < current.key) {
      isLeftChild = true
      current = current.left
    // 要删除的节点大于当前节点,往右找
    } else {
      isLeftChild = false
      current = current.right
    }
    // 查找到叶节点仍未找到,返回false
    if (current === null) {
      return false
    }
  }
​
  // 找到要删除的节点,分类讨论
  // 1、删除的是叶子节点的情况
  if (current.left === null && current.right === null) {
    // 叶子节点同时是根节点
    if (current === this.root) {
      this.root = null
      // 是父节点的左子节点
    } else if (isLeftChild) {
      parent.left = null
      // 是父节点的右子节点
    } else {
      parent.right = null
    }
​
    // 2、删除的是只有一个子节点的节点
  } else if (current.right === null) {
    //-- 2.1、要删除的节点只存在<左子节点>的情况
​
    //---- 2.1.1、要删除的节点为根节点
    if (current === this.root) {
      this.root = current.left
      //---- 2.1.2、要删除的节点是其父节点的左子节点
    } else if (isLeftChild) {
      parent.left = current.left
      //---- 2.1.3、要删除的节点是其父节点的右子节点
    } else {
      parent.right = current.left
    }
  } else if (current.left === null) {
    //-- 2.2、要删除的节点只存在<右子节点>的情况
​
    //---- 2.2.1 要删除的节点为根节点
    if (current === this.root) {
      this.root = current.right
      //---- 2.2.2 要删除的节点是其父节点的左子节点
    } else if (isLeftChild) {
      parent.left = current.right
      //---- 2.2.3 要删除的节点是其父节点的右子节点
    } else {
      parent.right = current.right
    }
​
    // 3、删除的是有两个子节点的节点
  } else {
​
    // 3.1 找到要删除节点的后继节点
    const successor = this.getSuccessor(current)
​
    // 3.2.1 要删除的节点为根节点
    if (current === this.root) {
      this.root = successor
      // 3.2.2 要删除的节点是其父节点的左子节点
    } else if (isLeftChild) {
      parent.left = successor
      // 3.2.3 要删除的节点是其父节点的右子节点
    } else {
      parent.right = successor
    }
​
    // 3.3 将后继节点的左子节点改为被删除的节点的左子节点
    successor.left = current.left
  }
}
​
// 获取要删除节点的后继节点,即从要删除的节点的右边开始查找最小的值
getSuccessor(delNode) {
​
  // 定义状态变量,保存要找到的后续节点相关的信息
  let successor = delNode     //保存后继节点
  let successorParent = null  //保存后继节点的父节点
  let current = delNode.right //当前节点,用于遍历标识是否已找到后续节点
​
  // 当没找到后继节点时,遍历要删除节点的右子树的左子节点,直到找到后继节点
  while (current !== null) {
    successorParent = successor
    successor = current
    current = current.left
  }
​
  // 若找到的后继节点不是要删除节点的直接右子节点,则后继节点可能有右子节点(不可能有左子节点,若有的话,该节点会成为后继节点,情况反而变简单了),需要后继节点的父节点指向后继节点的右子节点,同时要删除节点的右子节点要成为后继节点的右子节点
  if (successor !== delNode.right) {
    // 后继节点的父节点指向后继节点的右子节点
    successorParent.left = successor.right0
    // 要删除节点的右子节点要成为后继节点的右子节点
    successor.right = delNode.right
  }
​
  // 返回找到的后继节点
  return successor
}

删除数据(通过递归实现)

代码更少更容易实现

// 删除节点,通过递归实现
remove(key) {
  // root被赋值为removeNode方法的返回值,这里很关键
  this.root = this.removeNode(this.root, key)
}
​
// remove(key)的辅助方法
removeNode(node, key) {
  // 如果正在检测的节点为null,说明键不存在于树中,返回null
  if (node === null) {
    return null
  }
  // 要找的键比当前节点的值小,沿着树的左边找到下一个节点
  if (key < node.key) {
    node.left = this.removeNode(node.left, key)
    return node
    // 要找的键比当前节点的值大,沿着树的右边找到下一个节点
  } else if (key > node.key) {
    node.right = this.removeNode(node.right, key)
    return node
  } else {
    // 找到要删除的节点,分类讨论
​
    // 1、删除的是叶子节点的情况
    if (node.left === null && node.right === null) {
​
      // 返回null来将对应的父节点指针赋予null值
      return null
​
      // 2、删除的是只有一个子节点的节点
      // 2.1 要删除的节点只存在<右子节点>的情况
    } else if (node.left === null) {
      // 把对它的引用改为对它右侧子节点的引用
      return node.right
​
      // 2.2 要删除的节点只存在<左子节点>的情况
    } else if (node.right === null) {
      // 把对它的引用改为对它左侧子节点的引用
      return node.left
​
      // 3、删除的是有两个子节点的节点
    } else {
​
      // 找到它右边子树中最小的节点(它的继承者)
      const aux = this.minNode(node.right)
      // 用它右侧子树中最小节点的键去更新这个节点的值,即它被移除了
      node.key = aux.key
      // 把右侧子树中的最小节点移除,因为它已经被移至要移除的节点的位置了
      node.right = this.removeNode(node.right, aux.key)
      // 向它的父节点返回更新后节点的引用
      return node
    }
  }
}

二叉搜索树的完整实现

// 节点类的封装
class node {
  constructor(value) {
    this.value = value // 节点的值
    this.left = null // 节点的左子节点
    this.right = null // 节点的右子节点
  }
}
​
// 二叉搜索树的封装 -- 常用的方法有:插入、搜索、遍历、删除
class BinarySearchTree {
  constructor() {
    this.root = null // 根节点
  }
​
  // 1.插入 -- 如何插入? ==> 通过比大小,在空节点处插入
  insert(value) {
    let newNode = new node(value)
    // return this.insertNodeByIteration(this.root, newNode)
    return this.insertNodeByRecursion(this.root, newNode)
  }
  // 两种方法 -- 循环 / 递归
​
  /**
   * 循环的写法
   * @param {*} current 当前遍历的节点(从根节点开始)
   * @param {*} newNode 要插入的新节点
   * @returns
   */
  insertNodeByIteration(current, newNode) {
    // 先分析root为空的情况,此时新的节点直接取代root
    if (!current) {
      this.root = newNode
      return true
    }
    // 此处分析root不为空的情况
    while (current) {
      if (newNode.value < current.value) {
        // 往左子树处遍历
        // current的左子树是否为空,若为空则直接插入,否组继续向下遍历
        if (current.left) {
          //  不为空的情况
          current = current.left
        } else {
          // 为空的情况
          current.left = newNode
          return true
        }
      } else if (newNode.value > current.value) {
        // 往右子树处遍历
        // current的右子树是否为空,若为空则直接插入,否组继续向下遍历
        if (current.right) {
          //  不为空的情况
          current = current.right
        } else {
          // 为空的情况
          current.right = newNode
          return true
        }
      } else {
        return '该节点已存在'
      }
    }
  }
​
  /**
   * 递归的写法
   * @param {*} current 当前遍历的节点(从根节点开始)
   * @param {*} newNode 要插入的新节点
   */
  insertNodeByRecursion(current, newNode) {
    // 还是优先考虑根节点为空的情况
    if (!this.root) {
      this.root = newNode
      return true
    }
    // 比大小,然后判断current子节点是否为空,若为空直接替代,不为空则递归调用
    if (newNode.value < current.value) {
      // 进入左子树
      if (!current.left) {
        // 判空,为空则直接替代
        current.left = newNode
        return true
      } else {
        // 不为空,则递归
        return this.insertNodeByRecursion(current.left, newNode)
      }
    } else if (newNode.value > current.value) {
      // 进入右子树
      if (!current.right) {
        // 判空,为空则直接替代
        current.right = newNode
        return true
      } else {
        // 不为空,则递归
        return this.insertNodeByRecursion(current.right, newNode)
      }
    } else {
      return '该节点已存在'
    }
  }
​
  // 2. 搜索
  search(value) {
    // 先判断根节点为空的情况
    if (!this.root) return false
    // 根节点不为空的情况
    // return this.searchByIteration(this.root, value)
    return this.searchByRecursion(this.root, value)
  }
  // 依然有两种方式 -- 循环/递归
​
  /**
   * 通过循环迭代的方式搜索
   * @param {*} current 当前遍历的节点
   * @param {*} value 搜索的值
   * @returns  true / false
   */
  searchByIteration(current, value) {
    while (current) {
      // 比大小
      if (value < current.value) {
        // 往左子树遍历
        current = current.left
      } else if (value > current.value) {
        // 往右子树遍历
        current = current.right
      } else {
        // 找到了
        return true
      }
    }
    return false
  }
​
  /**
   * 通过递归的方式搜索
   * @param {*} current 当前遍历的节点
   * @param {*} value  搜索的值
   * @returns true / false
   */
  searchByRecursion(current, value) {
    if (current) {
      if (value < current.value) {
        return this.searchByRecursion(current.left, value)
      } else if (value > current.value) {
        return this.searchByRecursion(current.right, value)
      } else if (value === current.value) {
        return true
      }
    }
    return false
  }
​
  // 3. 遍历 -- 又可分为先序遍历、中序遍历、后序遍历
  // <1> 先序遍历
  preOrderTraverse() {
    return this.preOrderTraverseNode(this.root)
  }
  preOrderTraverseNode(current) {
    if (!current) return
    console.log(current.value)
    this.preOrderTraverseNode(current.left)
    this.preOrderTraverseNode(current.right)
  }
​
  // <2> 中序遍历
  inOrderTraverse() {
    return this.inOrderTraverseNode(this.root)
  }
  inOrderTraverseNode(current) {
    if (!current) return
    this.inOrderTraverseNode(current.left)
    console.log(current.value)
    this.inOrderTraverseNode(current.right)
  }
​
  // <3> 后序遍历
  postOrderTraverse() {
    return this.postOrderTraverseNode(this.root)
  }
  postOrderTraverseNode(current) {
    if (!current) return
    this.postOrderTraverseNode(current.left)
    this.postOrderTraverseNode(current.right)
    console.log(current.value)
  }
​
  // 4. 删除 -- 删除也分循环和递归两种实现方式。
  // 同时删除功能也要分为三种情况
  // <1> 删除的是叶子节点  <2> 删除的节点下面还有一个节点  <3> 删除的节点下面还有两个节点
  remove(value) {
    // 首先考虑根节点不存在的情况
    if (!this.root) {
      return '根节点不存在'
    }
    // 考虑删除根节点的情况
    if (
      this.root.left == null &&
      this.root.right == null &&
      this.root.value === value
    ) {
      this.root = null
      return `已删除根节点${value}`
    }
    return this.removeNode(this.root, value)
  }
​
  // 此处通过循环实现
  removeNode(current, value) {
    // 初始化变量parentNode 用于记录当前遍历节点的父节点
    let parentNode = null
​
    while (current) {
      // 首先还是要先找到那个要删除的节点
      if (value < current.value) {
        parentNode = current
        current = current.left
      } else if (value > current.value) {
        parentNode = current
        current = current.right
      } else if (value === current.value) {
        return this.DelNode(current, parentNode)
      }
    }
    return '不存在该值的节点'
  }
  DelNode(current, parentNode) {
    // <1> 删除的是叶子节点 -- 判断current是parentNode的左子树还是右子树
    if (current.left == null && current.right == null) {
      // 左右子树都为空 直接将删除节点的父节点指向空
      if (current.value < parentNode.value) {
        parentNode.left = null
        return `已删除节点${current.value}`
      } else {
        parentNode.right = null
        return `已删除节点${current.value}`
      }
    }
    // <2> 删除的节点下面还有一个节点
    else if (current.left == null || current.right == null) {
      // 先排除current就是根节点的情况
      if (current.value === this.root.value) {
        // 此时只需要让根节点直接指向current的子节点
        if (this.root.left) {
          this.root = current.left
          return `已删除根节点${current.value}`
        } else {
          this.root = current.right
          return `已删除根节点${current.value}`
        }
      }
​
      if (current.left == null) {
        // 左节点为空
        if (current.value < parentNode.value) {
          parentNode.left = current.right
          return `已删除值为${current.value}的节点`
        } else {
          parentNode.right = current.right
          return `已删除值为${current.value}的节点`
        }
      } else {
        // 右节点为空
        if (current.value < parentNode.value) {
          parentNode.left = current.left
          return `已删除值为${current.value}的节点`
        } else {
          parentNode.right = current.left
          return `已删除值为${current.value}的节点`
        }
      }
    }
    // <3> 删除的节点下面还有两个节点
    else {
      // 往删除节点的右找后继节点
      let successor = current.right
      let successorParent = current
      // 先解决根节点的情况
​
      /* 
      思路 :
      1.找到删除节点current的后继节点successor
      2.让successor的父节点的左子节点指向successor的右子节点
      3.让successor的右子节点指向current的右子节点
      4.再将successor的左子节点指向current的左子节点
      5.parentNode的右子节点指向successor
      */
​
      // ~1~ 找到删除节点current的后继节点successor
      // 然后一直往左找最左子树
      while (successor.left) {
        successorParent = successor
        successor = successor.left
      }
      // ~2~ 让successor的父节点指向successor的右子节点
      successorParent.left = successor.right
      // ~3~ 让successor的右子节点指向current的右子节点
      successor.right = current.right
      // ~4~ 再将successor的左子节点指向current的左子节点
      successor.left = current.left
      // ~5~ parentNode的子节点(需要判断)指向successor(正式删除current)
      // ~5.1~ 需要考虑一种特殊的情况,我们在这里处理删除根节点的情况
      if (current.value === this.root.value) {
        this.root = successor
        return `已删除值为${current.value}的根节点`
      }
      if (current.value < parentNode.value) {
        parentNode.left = successor
        return `已删除值为${current.value}的节点`
      }
      if (current.value > parentNode.value) {
        parentNode.right = successor
        return `已删除值为${current.value}的节点`
      }
    }
  }
​
  // 下面通过递归实现删除操作
  removeByRecursion(value) {
    if (this.search(value)) {
      this.root = this.removeByRecursionNode(this.root, value)
      return true
    } else {
      return false 
    }
​
  }
​
  removeByRecursionNode(current, value) {
    if (value < current.value) {
      current.left = this.removeByRecursionNode(current.left, value)
      return current
    } else if (value > current.value) {
      current.right = this.removeByRecursionNode(current.right, value)
      return current
    } else if (value === current.value) {
      // 此时意味着已经找到了该删除节点
      // <1> 该删除节点为叶子结点
      if (current.left == null && current.right == null) {
        // 直接赋空
        return null
      }
      // <2> 该删除节点下面还有一个节点
      else if (current.left == null || current.right == null) {
        // 将其子节点的值赋给自己,再将子节点赋空
        if (current.left) {
          current = current.left
        } else {
          current = current.right
        }
        return current
      }
      // <3> 该删除节点下面还有两个节点
      else {
        // 先找到后继节点
        let successor = this.findSuccessor(current.right)
        current.value = successor.value
        current.right = this.removeByRecursionNode(current.right, successor.value)
        return current
      }
    }
  }
​
  findSuccessor(successor) {
    while (successor != null && successor.left != null) {
      successor = successor.left
    }
    return successor
  }
}