[前端] JavaScript二叉搜索树构建操作详解

1928 0
Honkers 2022-10-21 15:24:39 | 显示全部楼层 |阅读模式
目录

    前言什么是二叉搜索树构建一颗二叉搜索树二叉搜索树的操作
      向二叉搜索树中插入数据查找二叉搜索树中的数据删除二叉搜索树的某个节点
        前驱后继节点删除一个节点的三种情况实现代码

    完整代码总结


前言

前面我们介绍了二叉树这个数据结构以及二叉树的遍历算法,这篇文章我们来学习一下一个特殊的二叉树——二叉搜索树(BST Binary Search Tree),也叫二叉排序树、二叉查找树。

什么是二叉搜索树

二叉搜索树首先它是一棵二叉树,而且还满足下面这些特质:
    对于任何一个非空节点来说,它左子树上的值必须小于当前值;对于任何一个非空节点来说,它右子树上的值必须大于当前值;任何一颗子树满足上面的条件;
如下图所示:


上图就是一颗二叉搜索树,我们就拿根节点来说,根节点的值71,它的左子树的值分别是22、35、46、53和66,这几个都是满足左子树小于当前值;它的右子树的值分别是78、87和98,这几个值是满足子树大于当前值的;以此类推,所以上图就是一棵二叉搜索树。
根据二叉搜索树的特质,我们还能得到以下结论:
    二叉搜索树的任何一个节点的左子树、右子树都是一颗二叉搜索树;二叉搜索树的最小的节点是整颗树的最左下角的叶子节点;二叉搜索树的最大的节点是整棵树的最右下角的叶子节点

构建一颗二叉搜索树

我们现在使用JavaScript来构建一颗二叉搜索树,要知道一颗二叉搜索树也是由一个一个节点组成,这里我们通过class创建一个节点类,
示例代码如下:
  1. class BinarySearchTree {
  2.   constructor() {
  3.     // 初始化根节点
  4.     this.root = null
  5.   }
  6.   // 创建一个节点
  7.   Node(val) {
  8.     return {
  9.       left: null, // 左子树
  10.       right: null, // 右子树
  11.       parent: null, // 父节点
  12.       val,
  13.     }
  14.   }
  15. }
复制代码
这里一个节点由四部分组成,分别是指向左子树的指针、指向右子树的指针、指向父节点的指针以及当前值。

二叉搜索树的操作

关于二叉树的遍历操作我们在上一篇文章中已经介绍了,这里不在重复,这里主要介绍如下操作:
    插入操作查找操作删除操作

向二叉搜索树中插入数据

向一个二叉搜索树插入数据实现思路如下:
    判断root是否为空,如果为空则创建root;如果root非空,则需要判断插入节点的val比根节点的val是大还是小;如果比根节点小,说明是左子树的节点;如果比根节点大,说明是右子树的节点;上面两步重复执行,直到找到一个点,如果这个点小于我们要插入的值,且不存在右子树,将这个点作为其右叶子节点;如果这个点大于我们要插入的值,且不存在右子树,将这个点作为其左叶子节点。
示例代码如下:
  1. // 创建要给插入节点的方法
  2. insertNode(val) {
  3.   const that = this
  4.   // 允许接受一个数组,批量插入
  5.   if (Object.prototype.toString.call(val) === '[object Array]') {
  6.     val.forEach(function (v) {
  7.       that.insertNode(v)
  8.     })
  9.     return
  10.   }
  11.   if (typeof val !== 'number') throw Error('插入的值不是一个数字')
  12.   const newNode = this.Node(val)
  13.   if (this.root) {
  14.     // 根节点非空
  15.     this.#insertNode(this.root, newNode)
  16.   } else {
  17.     // 根节点是空的,直接创建
  18.     this.root = newNode
  19.   }
  20. }
  21. // 私有方法,插入节点
  22. #insertNode(root, newNode) {
  23.   if (newNode.val < root.val) {
  24.     // 新节点比根节点小,左子树
  25.     if (root.left === null) {
  26.       // 如果左子树上没有内容,则直接插入,如果有,寻找下一个插入位置
  27.       root.left = newNode
  28.       root.left.parent = root
  29.     } else {
  30.       this.#insertNode(root.left, newNode)
  31.     }
  32.   } else {
  33.     // 新节点比根节点大,右子树
  34.     if (root.right === null) {
  35.       // 如果右子树上没有内容,则直接插入,如果有,寻找下一个插入位置
  36.       root.right = newNode
  37.       root.right.parent = root
  38.     } else {
  39.       this.#insertNode(root.right, newNode)
  40.     }
  41.   }
  42. }
复制代码
在类中定义了insertNode方法,这个方法接受数值或者数值类型的数组,将其插入这个二叉搜索树中;插入方法我们定义了一个私有的#insertNode方法,用于节点的插入。
为了看到效果,我们这里定义了一个静态方法,用于中序遍历(因为中序遍历的顺序是左根右,在二叉搜索树中使用中序排序,最终结果是从小到大依次排序的)这个树,并返回一个数组,
示例代码如下:
  1. // 中序遍历这个树
  2. static inorder(root) {
  3.   if (!root) return
  4.   const result = []
  5.   const stack = []
  6.   // 定义一个指针
  7.   let p = root
  8.   // 如果栈中有数据或者p不是null,则继续遍历
  9.   while (stack.length || p) {
  10.     // 如果p存在则一致将p入栈并移动指针
  11.     while (p) {
  12.       // 将 p 入栈,并以移动指针
  13.       stack.push(p)
  14.       p = p.left
  15.     }
  16.     const node = stack.pop()
  17.     result.push(node.val)
  18.     p = node.right
  19.   }
  20.   return result
  21. }
复制代码
测试代码如下:
const tree = new BinarySearchTree()
tree.insertNode([71, 35, 87, 22, 53, 46, 66, 78, 98])
const arr = BinarySearchTree.inorder(tree.root)
console.log(arr) // [ 22, 35, 46, 53, 66,71, 78, 87, 98 ]
最终的树结构如下:



查找二叉搜索树中的数据

现在我们封装一个find方法,用于查找二叉搜索树中的某个数据,假如我们查找66这个数据,利用上面那个树,
其查找思路如下图所示:


递归方式实现如下:
  1. /**
  2. * 根据 val 查找节点
  3. * @param {number} val 需要查找的数值
  4. * @returns 如果找到返回当前节点的引用,如果未找到返回 undefined
  5. */
  6. find(val) {
  7.   if (typeof val !== 'number') throw Error('插入的值不是一个数字')
  8.   function r(node, val) {
  9.     // console.log(node)
  10.     if (!node) return
  11.     if (node.val < val) {
  12.       return r(node.right, val)
  13.     } else if (node.val > val) {
  14.       return r(node.left, val)
  15.     } else {
  16.       return node
  17.     }
  18.   }
  19.   return r(this.root, val)
  20. }
复制代码
迭代方式实现如下:
  1. /**
  2. * 根据 val 查找节点
  3. * @param {number} val 需要查找的数值
  4. * @returns 如果找到返回当前节点的引用,如果未找到返回 undefined
  5. */
  6. find(val) {
  7.   if (typeof val !== 'number') throw Error('插入的值不是一个数字')
  8.   let node = this.root
  9.   while (node) {
  10.     if (node.val < val) {
  11.       // 进入右子树
  12.       node = node.right
  13.     } else if (node.val > val) {
  14.       // 进入左子树
  15.       node = node.left
  16.     } else {
  17.       return node
  18.     }
  19.   }
  20.   return
  21. }
复制代码
两者相对来说,使用迭代的方式更优一些。

删除二叉搜索树的某个节点


前驱后继节点

在开始删除二叉搜索树中的某个节点之前,我们先来了解一下什么是前驱和后继节点;
    前驱节点指的是使用中序遍历当前二叉搜索树时,当前节点的上一个节点就是前驱节点,换一种说法就是在二叉搜索树中,当前节点的左子树的最大值,就是该节点的前驱节点;后继节点指的是使用中序遍历当前二叉搜索树时,当前节点的下一个节点就是后继节点,换一种说法就是在二叉搜索树中,当前节点的右子树的最小值,就是该节点的后继节点
如下图所示:


了解了什么是前驱和后继节点之后,现在我们来开始删除某个节点。

删除一个节点的三种情况

当删除的节点是叶子节点时,只需要将指向它的指针修改为null,即可,如下图所示:


当需要删除的节点存在一个子节点时 需要将要删除节点的子节点的parent指针指向要删除节点的父节点,然后将当前要删除节点的父节点指向子节点即可,
如下图所示:


当需要删除的节点存在一个子节点时, 删除步骤如下:
    找到当前节点的前驱或者后继节点,这里选择后继;然后将后继节点的值赋值给当前节点;删除后继节点。
如下图所示:


现在我们将这些情况已经分析完成了,现在通过代码实现一下。

实现代码

实现代码如下:
  1. remove(val) {
  2.   // 1. 删除节点
  3.   const cur = this.find(val)
  4.   if (!val) return false // 未找到需要删除的节点
  5.   if (!cur.left && !cur.right) {
  6.     // 1. 当前节点是叶子节点的情况
  7.     this.#removeLeaf(cur)
  8.   } else if (cur.left && cur.right) {
  9.     // 2. 当前节点存在两个子节点
  10.     // 2.1 找到当前节点的后继节点
  11.     const successorNode = this.#minNode(cur.right)
  12.     // 2.2 将后继节点的值赋值给当前值
  13.     cur.val = successorNode.val
  14.     if (!successorNode.left && !successorNode.right) {
  15.       // 2.3 后继节点是叶子节点,直接删除
  16.       this.#removeLeaf(successorNode)
  17.     } else {
  18.       // 2.4 后继节点不是叶子节点
  19.       // 2.4.1记录该节点的子节点,
  20.       let child =
  21.         successorNode.left !== null ? successorNode.left : successorNode.right
  22.       // 2.4.2 记录该节点的父节点
  23.       let parent = successorNode.parent
  24.       // 2.4.3 如果当前父节点的left是后继结点,则把后继结点的父节点的left指向后继结点的子节点
  25.       if (parent.left === successorNode) {
  26.         parent.left = child
  27.       } else {
  28.         // 2.4.4 如果不是,则让父节点的right指向后继结点的子节点
  29.         parent.right = child
  30.       }
  31.       // 2.4.5 修改子节点的parent指针
  32.       child.parent = parent
  33.     }
  34.     // 2.3 删除后继节点
  35.   } else {
  36.     // 记录当前节点的是否是父节点的左子树
  37.     const isLeft = cur.val < cur.parent.val
  38.     // 3. 仅存在一个子节点
  39.     if (cur.left) {
  40.       // 3.1 当前节点存在左子树
  41.       cur.parent[isLeft ? 'left' : 'right'] = cur.left
  42.       cur.left.parent = cur.parent
  43.     } else if (cur.right) {
  44.       // 3.2 当前节点存在右子树
  45.       cur.parent[isLeft ? 'left' : 'right'] = cur.right
  46.       cur.right.parent = cur.parent
  47.     }
  48.   }
  49. }
  50. // 删除叶子节点
  51. #removeLeaf(node) {
  52.   if (!node) return
  53.   const parent = node.parent
  54.   if (node.val < parent.val) {
  55.     // 当前要删除的叶子节点是左节点
  56.     parent.left = null
  57.   } else {
  58.     // 当前要删除的叶子节点是右节点
  59.     parent.right = null
  60.   }
  61. }
  62. // 查找最小值
  63. #minNode(node) {
  64.   if (!node) return
  65.   if (!node.left) return node
  66.   let p = node.left
  67.   while (p.left) {
  68.     p = p.left
  69.   }
  70.   return p
  71. }
复制代码
完整代码

本篇文章中的完整代码如下:
  1. class BinarySearchTree {
  2.   constructor() {
  3.     // 初始化根节点
  4.     this.root = null
  5.   }
  6.   // 创建一个节点
  7.   Node(val) {
  8.     return {
  9.       left: null, // 左子树
  10.       right: null, // 右子树
  11.       parent: null, // 父节点
  12.       val,
  13.     }
  14.   }
  15.   /**
  16.    * 创建要给插入节点的方法
  17.    * @param {number | array[number]} val
  18.    * @returns
  19.    */
  20.   insertNode(val) {
  21.     const that = this
  22.     // 允许接受一个数组,批量插入
  23.     if (Object.prototype.toString.call(val) === '[object Array]') {
  24.       val.forEach(function (v) {
  25.         that.insertNode(v)
  26.       })
  27.       return
  28.     }
  29.     if (typeof val !== 'number') throw Error('插入的值不是一个数字')
  30.     const newNode = this.Node(val)
  31.     if (this.root) {
  32.       // 根节点非空
  33.       this.#insertNode(this.root, newNode)
  34.     } else {
  35.       // 根节点是空的,直接创建
  36.       this.root = newNode
  37.     }
  38.   }
  39.   /**
  40.    * 私有方法,插入节点
  41.    * @param {Object{Node}} root
  42.    * @param {Object{Node}} newNode
  43.    */
  44.   #insertNode(root, newNode) {
  45.     if (newNode.val < root.val) {
  46.       // 新节点比根节点小,左子树
  47.       if (root.left === null) {
  48.         // 如果左子树上没有内容,则直接插入,如果有,寻找下一个插入位置
  49.         root.left = newNode
  50.         root.left.parent = root
  51.       } else {
  52.         this.#insertNode(root.left, newNode)
  53.       }
  54.     } else {
  55.       // 新节点比根节点大,右子树
  56.       if (root.right === null) {
  57.         root.right = newNode
  58.         root.right.parent = root
  59.       } else {
  60.         this.#insertNode(root.right, newNode)
  61.       }
  62.     }
  63.   }
  64.   /**
  65.    * 根据 val 查找节点
  66.    * @param {number} val 需要查找的数值
  67.    * @returns 如果找到返回当前节点的引用,如果未找到返回 undefined
  68.    */
  69.   find(val) {
  70.     if (typeof val !== 'number') throw Error('插入的值不是一个数字')
  71.     let node = this.root
  72.     while (node) {
  73.       if (node.val < val) {
  74.         // 进入右子树
  75.         node = node.right
  76.       } else if (node.val > val) {
  77.         // 进入左子树
  78.         node = node.left
  79.       } else {
  80.         return node
  81.       }
  82.     }
  83.     return
  84.   }
  85.   // /**
  86.   //  * 根据 val 查找节点 递归版
  87.   //  * @param {number} val 需要查找的数值
  88.   //  * @returns 如果找到返回当前节点的引用,如果未找到返回 undefined
  89.   //  */
  90.   // find(val) {
  91.   //   if (typeof val !== 'number') throw Error('插入的值不是一个数字')
  92.   //   function r(node, val) {
  93.   //     // console.log(node)
  94.   //     if (!node) return
  95.   //     if (node.val < val) {
  96.   //       return r(node.right, val)
  97.   //     } else if (node.val > val) {
  98.   //       return r(node.left, val)
  99.   //     } else {
  100.   //       return node
  101.   //     }
  102.   //   }
  103.   //   return r(this.root, val)
  104.   // }
  105.   remove(val) {
  106.     // 1. 删除节点
  107.     const cur = this.find(val)
  108.     if (!val) return false // 未找到需要删除的节点
  109.     if (!cur.left && !cur.right) {
  110.       // 1. 当前节点是叶子节点的情况
  111.       this.#removeLeaf(cur)
  112.     } else if (cur.left && cur.right) {
  113.       // 2. 当前节点存在两个子节点
  114.       // 2.1 找到当前节点的后继节点
  115.       const successorNode = this.#minNode(cur.right)
  116.       // 2.2 将后继节点的值赋值给当前值
  117.       cur.val = successorNode.val
  118.       if (!successorNode.left && !successorNode.right) {
  119.         // 2.3 后继节点是叶子节点,直接删除
  120.         this.#removeLeaf(successorNode)
  121.       } else {
  122.         // 2.4 后继节点不是叶子节点
  123.         // 2.4.1记录该节点的子节点,
  124.         let child =
  125.           successorNode.left !== null ? successorNode.left : successorNode.right
  126.         // 2.4.2 记录该节点的父节点
  127.         let parent = successorNode.parent
  128.         // 2.4.3 如果当前父节点的left是后继结点,则把后继结点的父节点的left指向后继结点的子节点
  129.         if (parent.left === successorNode) {
  130.           parent.left = child
  131.         } else {
  132.           // 2.4.4 如果不是,则让父节点的right指向后继结点的子节点
  133.           parent.right = child
  134.         }
  135.         // 2.4.5 修改子节点的parent指针
  136.         child.parent = parent
  137.       }
  138.       // 2.3 删除后继节点
  139.     } else {
  140.       // 记录当前节点的是否是父节点的左子树
  141.       const isLeft = cur.val < cur.parent.val
  142.       // 3. 仅存在一个子节点
  143.       if (cur.left) {
  144.         // 3.1 当前节点存在左子树
  145.         cur.parent[isLeft ? 'left' : 'right'] = cur.left
  146.         cur.left.parent = cur.parent
  147.       } else if (cur.right) {
  148.         // 3.2 当前节点存在右子树
  149.         cur.parent[isLeft ? 'left' : 'right'] = cur.right
  150.         cur.right.parent = cur.parent
  151.       }
  152.     }
  153.   }
  154.   // 删除叶子节点
  155.   #removeLeaf(node) {
  156.     if (!node) return
  157.     const parent = node.parent
  158.     if (node.val < parent.val) {
  159.       // 当前要删除的叶子节点是左节点
  160.       parent.left = null
  161.     } else {
  162.       // 当前要删除的叶子节点是右节点
  163.       parent.right = null
  164.     }
  165.   }
  166.   // 查找最小值
  167.   #minNode(node) {
  168.     if (!node) return
  169.     if (!node.left) return node
  170.     let p = node.left
  171.     while (p.left) {
  172.       p = p.left
  173.     }
  174.     return p
  175.   }
  176.   // 中序遍历这个树
  177.   static inorder(root) {
  178.     if (!root) return
  179.     const result = []
  180.     const stack = []
  181.     // 定义一个指针
  182.     let p = root
  183.     // 如果栈中有数据或者p不是null,则继续遍历
  184.     while (stack.length || p) {
  185.       // 如果p存在则一致将p入栈并移动指针
  186.       while (p) {
  187.         // 将 p 入栈,并以移动指针
  188.         stack.push(p)
  189.         p = p.left
  190.       }
  191.       const node = stack.pop()
  192.       result.push(node.val)
  193.       p = node.right
  194.     }
  195.     return result
  196.   }
  197. }
  198. const tree = new BinarySearchTree()
  199. tree.insertNode([71, 35, 84, 22, 53, 46, 66, 81, 83, 82, 88, 98])
  200. console.log(BinarySearchTree.inorder(tree.root)) // [ 22, 35, 46, 53, 66, 71, 81, 82, 83, 84, 88, 98 ]
  201. tree.remove(71
  202. console.log(BinarySearchTree.inorder(tree.root)) // [ 22, 35, 46, 53, 66, 81, 82, 83, 84, 88, 98 ]
复制代码
总结

文章介绍了二叉搜索树的性质以及二叉搜索树的构建、查找和删除
到此这篇关于JavaScript二叉搜索树构建操作详解的文章就介绍到这了,更多相关JS二叉搜索树内容请搜索中国红客联盟以前的文章或继续浏览下面的相关文章希望大家以后多多支持中国红客联盟!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Honkers

特级红客

关注
  • 3159
    主题
  • 36
    粉丝
  • 0
    关注
这家伙很懒,什么都没留下!

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2025 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行