JS 数据结构 —— 二叉搜索树(中篇)

270 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

上篇文章中,我们使用 js 自行初步封装了一个类 BinarySearchTree 用来表示二叉搜索树(BST)。并且定义了增加节点的方法 insert(),然后我们生成了一个二叉搜索树实例并添加了一些节点生成了如下图所示的树结构,这将作为本篇将要介绍的 3 种遍历 BST 及关于极值讨论的案例。
2022-02-04_000228.png

二叉树的遍历有 3 种常见的方式:

  1. 先序遍历
  2. 中序遍历
  3. 后序遍历

下面首先定义先序遍历的方法

先序遍历 preOrder()

所谓先序遍历,就是按照根节点 - 左子树 - 右子树的顺序进行遍历。以上篇我们定义的这个二叉搜索树为例,访问顺序就应该是 7 - 4 - 3 - 5 - 11 - 8。

// 查 - 先序遍历
preOrder() {
  this.preOrderNode(this.root)
}
preOrderNode(node) {
  if (node) {
    // 如果节点存在
    console.log('key', node.key)
    this.preOrderNode(node.left)
    console.log(`开始遍历${node.key}的的右节点`)
    this.preOrderNode(node.right)
  } else {
    // 如果节点不存在
    return
  }
}

实现先序遍历依旧用到了递归的思想,定义 preOrderNode 递归函数,传入节点作为参数,先传根节点,如果节点存在则进行打印输出,然后传入根节点的左子节点继续递归执行 preOrderNode。直到某个节点的左子节点不存在,则返回,结束当前执行函数(这里可以结合函数调用帮助理解,也就是最后入栈的 preOrderNode 执行完毕,出栈),代码继续执行,将该节点的右子节点传入 preOrderNode 继续递归。

现在我们执行 bst.preOrder() 进行测试,最终浏览器打印结果如下图所示:

image.png

中序遍历 midOrder()

中序遍历是首先遍历左子树,然后访问根节点,最后遍历右子树。代码写起来很简单,只需要把之前先序遍历的打印节点键值这一步挪到遍历左子节点之后执行即可。

// 查 - 中序遍历
midOrder() {
  this.midOrderNode(this.root)
}
midOrderNode(node) {
  if (node) {
    this.midOrderNode(node.left)
    console.log('key', node.key)
    this.midOrderNode(node.right)
  } else {
    return
  }
}

还是以之前生成的二叉搜索树为例,执行 bst.midOrder(),并解释过程:

  1. 先遍历左子树,递归执行 midOrderNode,也就是不断有函数入栈,如下图 ① 所示;
  2. 直至当传入 midOrderNode 的参数为键值为 3 的这个叶节点(所谓叶节点就是度为 0 的节点)的左子节点,其实是 null,那么执行 return,栈顶的 midOrderNode(null) 出栈,如下图 ② 所示;
  3. 继续执行 midOrderNode(3),此时对 key 值进行打印,输出结果为 3。然后将节点 3 的右子节点(null)作为参数传入 midOrderNode ,也就是将 midOrderNode(null) 入栈,然后经过 if else 判断直接 return 出栈,此时 midOrderNode(3) 也执行完毕,进行出栈。如下图 ③ 所示;
  4. 现在继续执行 midOrderNode(4),此时应该继续执行第 8 行代码,打印当前节点的 key 值,输出 4。然后执行第 9 行,将节点 4 的右子节点传入,也就是执行 midOrderNode(5),如下图 ④ 所示;
  5. 因为节点 5 也是个叶节点,左右子节点均为空,打印输出 5,当 midOrderNode(5) 执行结束并出栈后,midOrderNode(4) 也随之执行完毕,出栈,如下图 ⑤ 所示:

yuque_diagram.jpg
6. 现在则继续执行 midOrderNode(7),先打印输出根节点的键值 7,随后遍历根节点的右子树,过程与遍历左子树类似,不再赘述。最后实际测试一下,得到的结果是:

image.png
可以发现,中序遍历的顺序是按照键值的大小从小到大排列的~

后序遍历 postOrder()

了解完前序和中序遍历之后,后续遍历的实现就非常简单了。所谓后序,可以理解成在访问每个节点前,都要先把以该节点作为根节点的左右子树上的所有节点都访问一遍,最后访问该节点。

// 查 - 后序遍历
postOrder() {
  this.postOrderNode(this.root)
}
postOrderNode(node) {
  if (node) {
    this.postOrderNode(node.left)
    this.postOrderNode(node.right)
    console.log('key', node.key)
  } else {
    return
  }
}

执行 bst.postOrder() 测试,结果输出如下:

image.png

极值

介绍完遍历,我们来说说二叉搜索树的一个优点 —— 可以快速地找出所有节点中,key 的最大值或最小值。还是以上篇中生成的二叉树为例,我们几乎可以一眼看出,最左边的叶节点 3,即为 key 值最小的节点,最右边的节点 11,即为 key 值最大的节点。
2022-02-04_002750.png

  • 最小值

代码写起来也比较简单,只需从根节点开始,不断循环遍历各层节点的左子节点,直到某个节点的 left 属性为 null,则该节点即为整个二叉搜索树中 key 值最小的节点。代码如下:

// 查 - 最小值
min() {
  let node = this.root
  while (node?.left) {
    node = node.left
  }
  return node ? node.key : null
}

因为二叉树可以为空,也就是没有节点,根节点为 null 的情况,所以我用到了可选链操作符 ?.,保证即使 nodenull,取 nodeleft 属性也不会报错而是直接返回 null

  • 最大值

最大值就是不断循环遍历节点的右子节点,最终得到即为 key 值最大的节点。代码如下:

// 查 - 最大值
max() {
  let node = this.root
  while (node?.right) {
    node = node.right
  }
  return node ? node.key : null
}

给定 key 值

与查询极值的思路相似,利用递归函数(当然也可以用 while 循环),将 key 值与节点的 key 值进行比较,如果要搜索的 key 值更大,则再与当前节点的右子节点进行比较;反之则与左子节点进行比较,找到则返回 true,没找到则返回 false:

// 查 - 某个特定 key 值
search(key) {
  if (this.root) {
    return this.serachNode(this.root, key)
  } else {
    return false
  }
}
serachNode(node, key) {
  if (node.key === key) {
    return true
  } else {
    node = node.key > key ? node.left : node.right
  }
  if (node) {
    return this.serachNode(node, key)
  } else {
    return false
  }
}

注意第 16 行,要将 this.serachNode(node, key) 的结果 return,这样才能把结果一层层返回出去。否则,当搜索的节点层级大于等于 2 时,最终得到的结果会是 undefined

至于删除节点的方法,由于需要考虑的情况较多,将放在下篇文章继续介绍。

感谢.gif 点赞.png