二叉树

树拥有很多种结构,二叉树是树中最常用的结构,同时也是一个天然的递归结构。

二叉树拥有一个根节点,每个节点至多拥有两个子节点,分别为:左节点和右节点。树的最底部节点称之为叶节点,当一颗树的叶数量数量为满时,该树可以称之为满二叉树。

树 - 图1

二分搜索树

二分搜索树也是二叉树,拥有二叉树的特性。但是区别在于二分搜索树每个节点的值都比他的左子树的值大,比右子树的值小。

这种存储方式很适合于数据搜索。如下图所示,当需要查找 6 的时候,因为需要查找的值比根节点的值大,所以只需要在根节点的右子树上寻找,大大提高了搜索效率。

树 - 图2

实现

  1. class Node {
  2. constructor(value) {
  3. this.value = value
  4. this.left = null
  5. this.right = null
  6. }
  7. }
  8. class BST {
  9. constructor() {
  10. this.root = null
  11. this.size = 0
  12. }
  13. getSize() {
  14. return this.size
  15. }
  16. isEmpty() {
  17. return this.size === 0
  18. }
  19. addNode(v) {
  20. this.root = this._addChild(this.root, v)
  21. }
  22. // 添加节点时,需要比较添加的节点值和当前
  23. // 节点值的大小
  24. _addChild(node, v) {
  25. if (!node) {
  26. this.size++
  27. return new Node(v)
  28. }
  29. if (node.value > v) {
  30. node.left = this._addChild(node.left, v)
  31. } else if (node.value < v) {
  32. node.right = this._addChild(node.right, v)
  33. }
  34. return node
  35. }
  36. }

以上是最基本的二分搜索树实现,接下来实现树的遍历。

对于树的遍历来说,有三种遍历方法,分别是先序遍历、中序遍历、后序遍历。三种遍历的区别在于何时访问节点。在遍历树的过程中,每个节点都会遍历三次,分别是遍历到自己,遍历左子树和遍历右子树。如果需要实现先序遍历,那么只需要第一次遍历到节点时进行操作即可。

以下都是递归实现,如果你想学习非递归实现,可以 点击这里阅读

  1. // 先序遍历可用于打印树的结构
  2. // 先序遍历先访问根节点,然后访问左节点,最后访问右节点。
  3. preTraversal() {
  4. this._pre(this.root)
  5. }
  6. _pre(node) {
  7. if (node) {
  8. console.log(node.value)
  9. this._pre(node.left)
  10. this._pre(node.right)
  11. }
  12. }
  13. // 中序遍历可用于排序
  14. // 对于 BST 来说,中序遍历可以实现一次遍历就
  15. // 得到有序的值
  16. // 中序遍历表示先访问左节点,然后访问根节点,最后访问右节点。
  17. midTraversal() {
  18. this._mid(this.root)
  19. }
  20. _mid(node) {
  21. if (node) {
  22. this._mid(node.left)
  23. console.log(node.value)
  24. this._mid(node.right)
  25. }
  26. }
  27. // 后序遍历可用于先操作子节点
  28. // 再操作父节点的场景
  29. // 后序遍历表示先访问左节点,然后访问右节点,最后访问根节点。
  30. backTraversal() {
  31. this._back(this.root)
  32. }
  33. _back(node) {
  34. if (node) {
  35. this._back(node.left)
  36. this._back(node.right)
  37. console.log(node.value)
  38. }
  39. }

以上的这几种遍历都可以称之为深度遍历,对应的还有种遍历叫做广度遍历,也就是一层层地遍历树。对于广度遍历来说,我们需要利用之前讲过的队列结构来完成。

  1. breadthTraversal() {
  2. if (!this.root) return null
  3. let q = new Queue()
  4. // 将根节点入队
  5. q.enQueue(this.root)
  6. // 循环判断队列是否为空,为空
  7. // 代表树遍历完毕
  8. while (!q.isEmpty()) {
  9. // 将队首出队,判断是否有左右子树
  10. // 有的话,就先左后右入队
  11. let n = q.deQueue()
  12. console.log(n.value)
  13. if (n.left) q.enQueue(n.left)
  14. if (n.right) q.enQueue(n.right)
  15. }
  16. }

接下来先介绍如何在树中寻找最小值或最大数。因为二分搜索树的特性,所以最小值一定在根节点的最左边,最大值相反

  1. getMin() {
  2. return this._getMin(this.root).value
  3. }
  4. _getMin(node) {
  5. if (!node.left) return node
  6. return this._getMin(node.left)
  7. }
  8. getMax() {
  9. return this._getMax(this.root).value
  10. }
  11. _getMax(node) {
  12. if (!node.right) return node
  13. return this._getMin(node.right)
  14. }

向上取整和向下取整,这两个操作是相反的,所以代码也是类似的,这里只介绍如何向下取整。既然是向下取整,那么根据二分搜索树的特性,值一定在根节点的左侧。只需要一直遍历左子树直到当前节点的值不再大于等于需要的值,然后判断节点是否还拥有右子树。如果有的话,继续上面的递归判断。

  1. floor(v) {
  2. let node = this._floor(this.root, v)
  3. return node ? node.value : null
  4. }
  5. _floor(node, v) {
  6. if (!node) return null
  7. if (node.value === v) return v
  8. // 如果当前节点值还比需要的值大,就继续递归
  9. if (node.value > v) {
  10. return this._floor(node.left, v)
  11. }
  12. // 判断当前节点是否拥有右子树
  13. let right = this._floor(node.right, v)
  14. if (right) return right
  15. return node
  16. }

排名,这是用于获取给定值的排名或者排名第几的节点的值,这两个操作也是相反的,所以这个只介绍如何获取排名第几的节点的值。对于这个操作而言,我们需要略微的改造点代码,让每个节点拥有一个 size 属性。该属性表示该节点下有多少子节点(包含自身)。

  1. class Node {
  2. constructor(value) {
  3. this.value = value
  4. this.left = null
  5. this.right = null
  6. // 修改代码
  7. this.size = 1
  8. }
  9. }
  10. // 新增代码
  11. _getSize(node) {
  12. return node ? node.size : 0
  13. }
  14. _addChild(node, v) {
  15. if (!node) {
  16. return new Node(v)
  17. }
  18. if (node.value > v) {
  19. // 修改代码
  20. node.size++
  21. node.left = this._addChild(node.left, v)
  22. } else if (node.value < v) {
  23. // 修改代码
  24. node.size++
  25. node.right = this._addChild(node.right, v)
  26. }
  27. return node
  28. }
  29. select(k) {
  30. let node = this._select(this.root, k)
  31. return node ? node.value : null
  32. }
  33. _select(node, k) {
  34. if (!node) return null
  35. // 先获取左子树下有几个节点
  36. let size = node.left ? node.left.size : 0
  37. // 判断 size 是否大于 k
  38. // 如果大于 k,代表所需要的节点在左节点
  39. if (size > k) return this._select(node.left, k)
  40. // 如果小于 k,代表所需要的节点在右节点
  41. // 注意这里需要重新计算 k,减去根节点除了右子树的节点数量
  42. if (size < k) return this._select(node.right, k - size - 1)
  43. return node
  44. }

接下来讲解的是二分搜索树中最难实现的部分:删除节点。因为对于删除节点来说,会存在以下几种情况

  • 需要删除的节点没有子树
  • 需要删除的节点只有一条子树
  • 需要删除的节点有左右两条树

对于前两种情况很好解决,但是第三种情况就有难度了,所以先来实现相对简单的操作:删除最小节点,对于删除最小节点来说,是不存在第三种情况的,删除最大节点操作是和删除最小节点相反的,所以这里也就不再赘述。

  1. delectMin() {
  2. this.root = this._delectMin(this.root)
  3. console.log(this.root)
  4. }
  5. _delectMin(node) {
  6. // 一直递归左子树
  7. // 如果左子树为空,就判断节点是否拥有右子树
  8. // 有右子树的话就把需要删除的节点替换为右子树
  9. if ((node != null) & !node.left) return node.right
  10. node.left = this._delectMin(node.left)
  11. // 最后需要重新维护下节点的 `size`
  12. node.size = this._getSize(node.left) + this._getSize(node.right) + 1
  13. return node
  14. }

最后讲解的就是如何删除任意节点了。对于这个操作,T.Hibbard 在 1962 年提出了解决这个难题的办法,也就是如何解决第三种情况。

当遇到这种情况时,需要取出当前节点的后继节点(也就是当前节点右子树的最小节点)来替换需要删除的节点。然后将需要删除节点的左子树赋值给后继结点,右子树删除后继结点后赋值给他。

你如果对于这个解决办法有疑问的话,可以这样考虑。因为二分搜索树的特性,父节点一定比所有左子节点大,比所有右子节点小。那么当需要删除父节点时,势必需要拿出一个比父节点大的节点来替换父节点。这个节点肯定不存在于左子树,必然存在于右子树。然后又需要保持父节点都是比右子节点小的,那么就可以取出右子树中最小的那个节点来替换父节点。

  1. delect(v) {
  2. this.root = this._delect(this.root, v)
  3. }
  4. _delect(node, v) {
  5. if (!node) return null
  6. // 寻找的节点比当前节点小,去左子树找
  7. if (node.value < v) {
  8. node.right = this._delect(node.right, v)
  9. } else if (node.value > v) {
  10. // 寻找的节点比当前节点大,去右子树找
  11. node.left = this._delect(node.left, v)
  12. } else {
  13. // 进入这个条件说明已经找到节点
  14. // 先判断节点是否拥有拥有左右子树中的一个
  15. // 是的话,将子树返回出去,这里和 `_delectMin` 的操作一样
  16. if (!node.left) return node.right
  17. if (!node.right) return node.left
  18. // 进入这里,代表节点拥有左右子树
  19. // 先取出当前节点的后继结点,也就是取当前节点右子树的最小值
  20. let min = this._getMin(node.right)
  21. // 取出最小值后,删除最小值
  22. // 然后把删除节点后的子树赋值给最小值节点
  23. min.right = this._delectMin(node.right)
  24. // 左子树不动
  25. min.left = node.left
  26. node = min
  27. }
  28. // 维护 size
  29. node.size = this._getSize(node.left) + this._getSize(node.right) + 1
  30. return node
  31. }

AVL 树

概念

二分搜索树实际在业务中是受到限制的,因为并不是严格的 O(logN),在极端情况下会退化成链表,比如加入一组升序的数字就会造成这种情况。

AVL 树改进了二分搜索树,在 AVL 树中任意节点的左右子树的高度差都不大于 1,这样保证了时间复杂度是严格的 O(logN)。基于此,对 AVL 树增加或删除节点时可能需要旋转树来达到高度的平衡。

实现

因为 AVL 树是改进了二分搜索树,所以部分代码是于二分搜索树重复的,对于重复内容不作再次解析。

对于 AVL 树来说,添加节点会有四种情况

树 - 图3

对于左左情况来说,新增加的节点位于节点 2 的左侧,这时树已经不平衡,需要旋转。因为搜索树的特性,节点比左节点大,比右节点小,所以旋转以后也要实现这个特性。

旋转之前:new < 2 < C < 3 < B < 5 < A,右旋之后节点 3 为根节点,这时候需要将节点 3 的右节点加到节点 5 的左边,最后还需要更新节点的高度。

对于右右情况来说,相反于左左情况,所以不再赘述。

对于左右情况来说,新增加的节点位于节点 4 的右侧。对于这种情况,需要通过两次旋转来达到目的。

首先对节点的左节点左旋,这时树满足左左的情况,再对节点进行一次右旋就可以达到目的。

  1. class Node {
  2. constructor(value) {
  3. this.value = value
  4. this.left = null
  5. this.right = null
  6. this.height = 1
  7. }
  8. }
  9. class AVL {
  10. constructor() {
  11. this.root = null
  12. }
  13. addNode(v) {
  14. this.root = this._addChild(this.root, v)
  15. }
  16. _addChild(node, v) {
  17. if (!node) {
  18. return new Node(v)
  19. }
  20. if (node.value > v) {
  21. node.left = this._addChild(node.left, v)
  22. } else if (node.value < v) {
  23. node.right = this._addChild(node.right, v)
  24. } else {
  25. node.value = v
  26. }
  27. node.height =
  28. 1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
  29. let factor = this._getBalanceFactor(node)
  30. // 当需要右旋时,根节点的左树一定比右树高度高
  31. if (factor > 1 && this._getBalanceFactor(node.left) >= 0) {
  32. return this._rightRotate(node)
  33. }
  34. // 当需要左旋时,根节点的左树一定比右树高度矮
  35. if (factor < -1 && this._getBalanceFactor(node.right) <= 0) {
  36. return this._leftRotate(node)
  37. }
  38. // 左右情况
  39. // 节点的左树比右树高,且节点的左树的右树比节点的左树的左树高
  40. if (factor > 1 && this._getBalanceFactor(node.left) < 0) {
  41. node.left = this._leftRotate(node.left)
  42. return this._rightRotate(node)
  43. }
  44. // 右左情况
  45. // 节点的左树比右树矮,且节点的右树的右树比节点的右树的左树矮
  46. if (factor < -1 && this._getBalanceFactor(node.right) > 0) {
  47. node.right = this._rightRotate(node.right)
  48. return this._leftRotate(node)
  49. }
  50. return node
  51. }
  52. _getHeight(node) {
  53. if (!node) return 0
  54. return node.height
  55. }
  56. _getBalanceFactor(node) {
  57. return this._getHeight(node.left) - this._getHeight(node.right)
  58. }
  59. // 节点右旋
  60. // 5 2
  61. // / \ / \
  62. // 2 6 ==> 1 5
  63. // / \ / / \
  64. // 1 3 new 3 6
  65. // /
  66. // new
  67. _rightRotate(node) {
  68. // 旋转后新根节点
  69. let newRoot = node.left
  70. // 需要移动的节点
  71. let moveNode = newRoot.right
  72. // 节点 2 的右节点改为节点 5
  73. newRoot.right = node
  74. // 节点 5 左节点改为节点 3
  75. node.left = moveNode
  76. // 更新树的高度
  77. node.height =
  78. 1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
  79. newRoot.height =
  80. 1 +
  81. Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))
  82. return newRoot
  83. }
  84. // 节点左旋
  85. // 4 6
  86. // / \ / \
  87. // 2 6 ==> 4 7
  88. // / \ / \ \
  89. // 5 7 2 5 new
  90. // \
  91. // new
  92. _leftRotate(node) {
  93. // 旋转后新根节点
  94. let newRoot = node.right
  95. // 需要移动的节点
  96. let moveNode = newRoot.left
  97. // 节点 6 的左节点改为节点 4
  98. newRoot.left = node
  99. // 节点 4 右节点改为节点 5
  100. node.right = moveNode
  101. // 更新树的高度
  102. node.height =
  103. 1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
  104. newRoot.height =
  105. 1 +
  106. Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))
  107. return newRoot
  108. }
  109. }