JavaScript二叉搜索树构建操作详解

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

前言

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

什么是二叉搜索树

二叉搜索树首先它是一棵二叉树,而且还满足下面这些特质:

  • 对于任何一个非空节点来说,它左子树上的值必须小于当前值
  • 对于任何一个非空节点来说,它右子树上的值必须大于当前值
  • 任何一颗子树满足上面的条件;

如下图所示:

上图就是一颗二叉搜索树,我们就拿根节点来说,根节点的值71,它的左子树的值分别是22、35、46、53和66,这几个都是满足左子树小于当前值;它的右子树的值分别是78、87和98,这几个值是满足右子树大于当前值的;以此类推,所以上图就是一棵二叉搜索树。

根据二叉搜索树的特质,我们还能得到以下结论:

  • 二叉搜索树的任何一个节点的左子树、右子树都是一颗二叉搜索树;
  • 二叉搜索树的最小的节点是整颗树的最左下角的叶子节点
  • 二叉搜索树的最大的节点是整棵树的最右下角的叶子节点

构建一颗二叉搜索树

我们现在使用JavaScript来构建一颗二叉搜索树,要知道一颗二叉搜索树也是由一个一个节点组成,这里我们通过class创建一个节点类,

示例代码如下:

class BinarySearchTree {
  constructor() {
    // 初始化根节点
    this.root = null
  }

  // 创建一个节点
  Node(val) {
    return {
      left: null, // 左子树
      right: null, // 右子树
      parent: null, // 父节点
      val,
    }
  }
}

这里一个节点由四部分组成,分别是指向左子树的指针、指向右子树的指针、指向父节点的指针以及当前值。

二叉搜索树的操作

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

  • 插入操作
  • 查找操作
  • 删除操作

向二叉搜索树中插入数据

向一个二叉搜索树插入数据实现思路如下:

  • 判断root是否为空,如果为空则创建root;
  • 如果root非空,则需要判断插入节点的val比根节点的val是大还是小;
  • 如果比根节点小,说明是左子树的节点;
  • 如果比根节点大,说明是右子树的节点;
  • 上面两步重复执行,直到找到一个点,如果这个点小于我们要插入的值,且不存在右子树,将这个点作为其右叶子节点;如果这个点大于我们要插入的值,且不存在右子树,将这个点作为其左叶子节点。

示例代码如下:

// 创建要给插入节点的方法
insertNode(val) {
  const that = this
  // 允许接受一个数组,批量插入
  if (Object.prototype.toString.call(val) === '[object Array]') {
    val.forEach(function (v) {
      that.insertNode(v)
    })
    return
  }
  if (typeof val !== 'number') throw Error('插入的值不是一个数字')
  const newNode = this.Node(val)
  if (this.root) {
    // 根节点非空
    this.#insertNode(this.root, newNode)
  } else {
    // 根节点是空的,直接创建
    this.root = newNode
  }
}
// 私有方法,插入节点
#insertNode(root, newNode) {
  if (newNode.val < root.val) {
    // 新节点比根节点小,左子树
    if (root.left === null) {
      // 如果左子树上没有内容,则直接插入,如果有,寻找下一个插入位置
      root.left = newNode
      root.left.parent = root
    } else {
      this.#insertNode(root.left, newNode)
    }
  } else {
    // 新节点比根节点大,右子树
    if (root.right === null) {
      // 如果右子树上没有内容,则直接插入,如果有,寻找下一个插入位置
      root.right = newNode
      root.right.parent = root
    } else {
      this.#insertNode(root.right, newNode)
    }
  }
}

在类中定义了insertNode方法,这个方法接受数值或者数值类型的数组,将其插入这个二叉搜索树中;插入方法我们定义了一个私有的#insertNode方法,用于节点的插入。

为了看到效果,我们这里定义了一个静态方法,用于中序遍历(因为中序遍历的顺序是左根右,在二叉搜索树中使用中序排序,最终结果是从小到大依次排序的)这个树,并返回一个数组,

示例代码如下:

// 中序遍历这个树
static inorder(root) {
  if (!root) return
  const result = []
  const stack = []
  // 定义一个指针
  let p = root
  // 如果栈中有数据或者p不是null,则继续遍历
  while (stack.length || p) {
    // 如果p存在则一致将p入栈并移动指针
    while (p) {
      // 将 p 入栈,并以移动指针
      stack.push(p)
      p = p.left
    }
    const node = stack.pop()
    result.push(node.val)
    p = node.right
  }
  return result
}

测试代码如下:

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这个数据,利用上面那个树,

其查找思路如下图所示:

递归方式实现如下:

/**
 * 根据 val 查找节点
 * @param {number} val 需要查找的数值
 * @returns 如果找到返回当前节点的引用,如果未找到返回 undefined
 */
find(val) {
  if (typeof val !== 'number') throw Error('插入的值不是一个数字')
  function r(node, val) {
    // console.log(node)
    if (!node) return
    if (node.val < val) {
      return r(node.right, val)
    } else if (node.val > val) {
      return r(node.left, val)
    } else {
      return node
    }
  }
  return r(this.root, val)
}

迭代方式实现如下:

/**
 * 根据 val 查找节点
 * @param {number} val 需要查找的数值
 * @returns 如果找到返回当前节点的引用,如果未找到返回 undefined
 */
find(val) {
  if (typeof val !== 'number') throw Error('插入的值不是一个数字')
  let node = this.root
  while (node) {
    if (node.val < val) {
      // 进入右子树
      node = node.right
    } else if (node.val > val) {
      // 进入左子树
      node = node.left
    } else {
      return node
    }
  }
  return
}

两者相对来说,使用迭代的方式更优一些。

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

前驱后继节点

在开始删除二叉搜索树中的某个节点之前,我们先来了解一下什么是前驱和后继节点;

  • 前驱节点指的是使用中序遍历当前二叉搜索树时,当前节点的上一个节点就是前驱节点,换一种说法就是在二叉搜索树中,当前节点的左子树的最大值,就是该节点的前驱节点
  • 后继节点指的是使用中序遍历当前二叉搜索树时,当前节点的下一个节点就是后继节点,换一种说法就是在二叉搜索树中,当前节点的右子树的最小值,就是该节点的后继节点

如下图所示:

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

删除一个节点的三种情况

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

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

如下图所示:

当需要删除的节点存在一个子节点时, 删除步骤如下:

  • 找到当前节点的前驱或者后继节点,这里选择后继;
  • 然后将后继节点的值赋值给当前节点;
  • 删除后继节点。

如下图所示:

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

实现代码

实现代码如下:

remove(val) {
  // 1. 删除节点
  const cur = this.find(val)
  if (!val) return false // 未找到需要删除的节点
  if (!cur.left && !cur.right) {
    // 1. 当前节点是叶子节点的情况
    this.#removeLeaf(cur)
  } else if (cur.left && cur.right) {
    // 2. 当前节点存在两个子节点
    // 2.1 找到当前节点的后继节点
    const successorNode = this.#minNode(cur.right)
    // 2.2 将后继节点的值赋值给当前值
    cur.val = successorNode.val
    if (!successorNode.left && !successorNode.right) {
      // 2.3 后继节点是叶子节点,直接删除
      this.#removeLeaf(successorNode)
    } else {
      // 2.4 后继节点不是叶子节点
      // 2.4.1记录该节点的子节点,
      let child =
        successorNode.left !== null ? successorNode.left : successorNode.right
      // 2.4.2 记录该节点的父节点
      let parent = successorNode.parent
      // 2.4.3 如果当前父节点的left是后继结点,则把后继结点的父节点的left指向后继结点的子节点
      if (parent.left === successorNode) {
        parent.left = child
      } else {
        // 2.4.4 如果不是,则让父节点的right指向后继结点的子节点
        parent.right = child
      }
      // 2.4.5 修改子节点的parent指针
      child.parent = parent
    }

    // 2.3 删除后继节点
  } else {
    // 记录当前节点的是否是父节点的左子树
    const isLeft = cur.val < cur.parent.val
    // 3. 仅存在一个子节点
    if (cur.left) {
      // 3.1 当前节点存在左子树
      cur.parent[isLeft ? 'left' : 'right'] = cur.left
      cur.left.parent = cur.parent
    } else if (cur.right) {
      // 3.2 当前节点存在右子树
      cur.parent[isLeft ? 'left' : 'right'] = cur.right
      cur.right.parent = cur.parent
    }
  }
}
// 删除叶子节点
#removeLeaf(node) {
  if (!node) return
  const parent = node.parent
  if (node.val < parent.val) {
    // 当前要删除的叶子节点是左节点
    parent.left = null
  } else {
    // 当前要删除的叶子节点是右节点
    parent.right = null
  }
}
// 查找最小值
#minNode(node) {
  if (!node) return
  if (!node.left) return node
  let p = node.left
  while (p.left) {
    p = p.left
  }
  return p
}

完整代码

本篇文章中的完整代码如下:

class BinarySearchTree {
  constructor() {
    // 初始化根节点
    this.root = null
  }

  // 创建一个节点
  Node(val) {
    return {
      left: null, // 左子树
      right: null, // 右子树
      parent: null, // 父节点
      val,
    }
  }
  /**
   * 创建要给插入节点的方法
   * @param {number | array[number]} val
   * @returns
   */
  insertNode(val) {
    const that = this
    // 允许接受一个数组,批量插入
    if (Object.prototype.toString.call(val) === '[object Array]') {
      val.forEach(function (v) {
        that.insertNode(v)
      })
      return
    }

    if (typeof val !== 'number') throw Error('插入的值不是一个数字')

    const newNode = this.Node(val)
    if (this.root) {
      // 根节点非空
      this.#insertNode(this.root, newNode)
    } else {
      // 根节点是空的,直接创建
      this.root = newNode
    }
  }

  /**
   * 私有方法,插入节点
   * @param {Object{Node}} root
   * @param {Object{Node}} newNode
   */
  #insertNode(root, newNode) {
    if (newNode.val < root.val) {
      // 新节点比根节点小,左子树
      if (root.left === null) {
        // 如果左子树上没有内容,则直接插入,如果有,寻找下一个插入位置
        root.left = newNode
        root.left.parent = root
      } else {
        this.#insertNode(root.left, newNode)
      }
    } else {
      // 新节点比根节点大,右子树
      if (root.right === null) {
        root.right = newNode
        root.right.parent = root
      } else {
        this.#insertNode(root.right, newNode)
      }
    }
  }
  /**
   * 根据 val 查找节点
   * @param {number} val 需要查找的数值
   * @returns 如果找到返回当前节点的引用,如果未找到返回 undefined
   */
  find(val) {
    if (typeof val !== 'number') throw Error('插入的值不是一个数字')
    let node = this.root
    while (node) {
      if (node.val < val) {
        // 进入右子树
        node = node.right
      } else if (node.val > val) {
        // 进入左子树
        node = node.left
      } else {
        return node
      }
    }
    return
  }
  // /**
  //  * 根据 val 查找节点 递归版
  //  * @param {number} val 需要查找的数值
  //  * @returns 如果找到返回当前节点的引用,如果未找到返回 undefined
  //  */
  // find(val) {
  //   if (typeof val !== 'number') throw Error('插入的值不是一个数字')
  //   function r(node, val) {
  //     // console.log(node)
  //     if (!node) return
  //     if (node.val < val) {
  //       return r(node.right, val)
  //     } else if (node.val > val) {
  //       return r(node.left, val)
  //     } else {
  //       return node
  //     }
  //   }
  //   return r(this.root, val)
  // }
  remove(val) {
    // 1. 删除节点
    const cur = this.find(val)
    if (!val) return false // 未找到需要删除的节点

    if (!cur.left && !cur.right) {
      // 1. 当前节点是叶子节点的情况
      this.#removeLeaf(cur)
    } else if (cur.left && cur.right) {
      // 2. 当前节点存在两个子节点
      // 2.1 找到当前节点的后继节点
      const successorNode = this.#minNode(cur.right)
      // 2.2 将后继节点的值赋值给当前值
      cur.val = successorNode.val
      if (!successorNode.left && !successorNode.right) {
        // 2.3 后继节点是叶子节点,直接删除
        this.#removeLeaf(successorNode)
      } else {
        // 2.4 后继节点不是叶子节点
        // 2.4.1记录该节点的子节点,
        let child =
          successorNode.left !== null ? successorNode.left : successorNode.right
        // 2.4.2 记录该节点的父节点
        let parent = successorNode.parent
        // 2.4.3 如果当前父节点的left是后继结点,则把后继结点的父节点的left指向后继结点的子节点
        if (parent.left === successorNode) {
          parent.left = child
        } else {
          // 2.4.4 如果不是,则让父节点的right指向后继结点的子节点
          parent.right = child
        }
        // 2.4.5 修改子节点的parent指针
        child.parent = parent
      }

      // 2.3 删除后继节点
    } else {
      // 记录当前节点的是否是父节点的左子树
      const isLeft = cur.val < cur.parent.val
      // 3. 仅存在一个子节点
      if (cur.left) {
        // 3.1 当前节点存在左子树
        cur.parent[isLeft ? 'left' : 'right'] = cur.left
        cur.left.parent = cur.parent
      } else if (cur.right) {
        // 3.2 当前节点存在右子树
        cur.parent[isLeft ? 'left' : 'right'] = cur.right
        cur.right.parent = cur.parent
      }
    }
  }
  // 删除叶子节点
  #removeLeaf(node) {
    if (!node) return
    const parent = node.parent
    if (node.val < parent.val) {
      // 当前要删除的叶子节点是左节点
      parent.left = null
    } else {
      // 当前要删除的叶子节点是右节点
      parent.right = null
    }
  }
  // 查找最小值
  #minNode(node) {
    if (!node) return
    if (!node.left) return node
    let p = node.left
    while (p.left) {
      p = p.left
    }
    return p
  }
  // 中序遍历这个树
  static inorder(root) {
    if (!root) return
    const result = []
    const stack = []
    // 定义一个指针
    let p = root
    // 如果栈中有数据或者p不是null,则继续遍历
    while (stack.length || p) {
      // 如果p存在则一致将p入栈并移动指针
      while (p) {
        // 将 p 入栈,并以移动指针
        stack.push(p)
        p = p.left
      }

      const node = stack.pop()
      result.push(node.val)
      p = node.right
    }
    return result
  }
}
const tree = new BinarySearchTree()
tree.insertNode([71, 35, 84, 22, 53, 46, 66, 81, 83, 82, 88, 98])
console.log(BinarySearchTree.inorder(tree.root)) // [ 22, 35, 46, 53, 66, 71, 81, 82, 83, 84, 88, 98 ]
tree.remove(71
console.log(BinarySearchTree.inorder(tree.root)) // [ 22, 35, 46, 53, 66, 81, 82, 83, 84, 88, 98 ]

总结

文章介绍了二叉搜索树的性质以及二叉搜索树的构建、查找和删除

到此这篇关于JavaScript二叉搜索树构建操作详解的文章就介绍到这了,更多相关JS二叉搜索树内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 如何利用JavaScript实现二叉搜索树

    计算机科学中最常用和讨论最多的数据结构之一是二叉搜索树.这通常是引入的第一个具有非线性插入算法的数据结构.二叉搜索树类似于双链表,每个节点包含一些数据,以及两个指向其他节点的指针:它们在这些节点彼此相关联的方式上有所不同.二叉搜索树节点的指针通常被称为"左"和"右",用来指示与当前值相关的子树.这种节点的简单 JavaScript 实现如下: var node = { value: 125, left: null, right: null }; 从名称中可以看出,二

  • 面向JavaScript入门初学者的二叉搜索树算法教程

    目录 什么是二叉搜索树 (BST)? 二叉树基本遍历(中序.后序.前序) 中序遍历 后序遍历 前序遍历 什么是有效的二叉搜索树? 如何找到二叉树最大深度 如何找到两个树节点之间的最小公共祖先

  • javascript算法之二叉搜索树的示例代码

    什么是二叉树 二叉树就是树的每个节点最多只能有两个子节点 什么是二叉搜索树 二叉搜索树在二叉树的基础上,多了一个条件,就是二叉树在插入值时,若插入值比当前节点小,就插入到左节点,否则插入到右节点:若插入过程中,左节点或右节点已经存在,那么继续按如上规则比较,直到遇到一个新的节点. 二叉搜索树的特性 二叉搜索树由于其独特的数据结构,使得其无论在增删,还是查找,时间复杂度都是O(h),h为二叉树的高度.因此二叉树应该尽量的矮,即左右节点尽量平衡. 二叉搜索树的构造 要构造二叉搜索树,首先要构造二叉树

  • Javascript实现从小到大的数组转换成二叉搜索树

    废话不多说了,直接给大家贴代码了,具体代码如下所示: var Array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; var Tree = createTree(Array); console.log(Tree); // 构造一个节点 function Node(nodeData, leftData, rightData) { this.nodeData = nodeData; this.leftData = leftData; this.rightData = rig

  • javascript数据结构之二叉搜索树实现方法

    本文实例讲述了javascript二叉搜索树实现方法.分享给大家供大家参考,具体如下: 二叉搜索树:顾名思义,树上每个节点最多只有二根分叉:而且左分叉节点的值 < 右分叉节点的值 . 特点:插入节点.找最大/最小节点.节点值排序 非常方便 二叉搜索树-javascript实现 <script type="text/javascript"> // <![CDATA[ //打印输出 function println(msg) { document.write(msg

  • JavaScript实现二叉搜索树

    JavaScript中的搜索二叉树实现,供大家参考,具体内容如下 二叉搜索树(BST,Binary Search Tree),也称二叉排序树或二叉查找树 二叉搜索树是一颗二叉树, 可以为空:如果不为空,满足以下性质: 非空左子树的所有键值小于其根结点的键值 非空右子树的所有键值大于其根结点的键值 也就是左结点值想<根结点值<右节点值 左.右子树本身也都是二叉搜索树 二叉搜索树的操作 insert(key):向树中插入一个新的键 search(key):在树中查找一个键,如果结点存在,则返回tr

  • JavaScript二叉搜索树构建操作详解

    目录 前言 什么是二叉搜索树 构建一颗二叉搜索树 二叉搜索树的操作 向二叉搜索树中插入数据 查找二叉搜索树中的数据 删除二叉搜索树的某个节点 前驱后继节点 删除一个节点的三种情况 实现代码 完整代码 总结 前言 前面我们介绍了二叉树这个数据结构以及二叉树的遍历算法,这篇文章我们来学习一下一个特殊的二叉树——二叉搜索树(BST Binary Search Tree),也叫二叉排序树.二叉查找树. 什么是二叉搜索树 二叉搜索树首先它是一棵二叉树,而且还满足下面这些特质: 对于任何一个非空节点来说,它

  • Java二叉搜索树遍历操作详解【前序、中序、后序、层次、广度优先遍历】

    本文实例讲述了Java二叉搜索树遍历操作.分享给大家供大家参考,具体如下: 前言:在上一节Java二叉搜索树基础中,我们对树及其相关知识做了了解,对二叉搜索树做了基本的实现,下面我们继续完善我们的二叉搜索树. 对于二叉树,有深度遍历和广度遍历,深度遍历有前序.中序以及后序三种遍历方法,广度遍历即我们寻常所说的层次遍历,如图: 因为树的定义本身就是递归定义,所以对于前序.中序以及后序这三种遍历我们使用递归的方法实现,而对于广度优先遍历需要选择其他数据结构实现,本例中我们使用队列来实现广度优先遍历.

  • C++二叉搜索树BSTree使用详解

    目录 一.概念 二.基础操作 1.查找find 2.插入Insert 3.中序遍历InOrder 4.删除erase 三.递归写法 1.递归查找 2.递归插入 3.递归删除 四.应用 五.题目练习 一.概念 二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树: 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值 左<根<右 它的左右子树也分别为二叉搜索树 之所以又叫二叉排序树,是因为二叉搜索树中序遍历的结果

  • C++数据结构之二叉搜索树的实现详解

    目录 前言 介绍 实现 节点的实现 二叉搜索树的查找 二叉搜索树的插入 二叉搜索树的删除 总结 前言 今天我们来学一个新的数据结构:二叉搜索树. 介绍 二叉搜索树也称作二叉排序树,它具有以下性质: 非空左子树的所有键值小于其根节点的键值 非空右子树的所有键值大于其根节点的键值 左,右子树都是二叉搜索树 那么我先画一个二叉搜索树给大家看看,是不是真的满足上面的性质. 我们就以根节点6为例子来看,我们会发现比6小的都在6的左边,而比6大的都在6的右边.对于6的左右子树来说,所有的节点都遵循这个规则.

  • Javascript的无new构建实例详解

    看jquery源代码第一步的时候,对于jquery对象的创建就看的云里雾里,琢磨半天终于有点感觉了,在此记录下 第一种方式: var A = function(){ return A.prototype.init(); } A.prototype = { init:function(){ this.age = 50; console.log(this); return this; }, age:100 } console.log(A() === new A()); 1.分析下结果为什么为true

  • Java底层基于二叉搜索树实现集合和映射/集合Set功能详解

    本文实例讲述了Java底层基于二叉搜索树实现集合和映射功能.分享给大家供大家参考,具体如下: 前言:在第5章的系列学习中,已经实现了关于二叉搜索树的相关操作,详情查看第5章即可.在本节中着重学习使用底层是我们已经封装好的二叉搜索树相关操作来实现一个基本的集合(set)这种数据结构. 集合set的特性: 集合Set存储的元素是无序的.不可重复的.为了能达到这种特性就需要寻找可以作为支撑的底层数据结构. 这里选用之前自己实现的二叉搜索树,这是由于该二叉树是不能盛放重复元素的.因此我们可以使用二叉搜索

  • Java删除二叉搜索树最大元素和最小元素的方法详解

    本文实例讲述了Java删除二叉搜索树最大元素和最小元素的方法.分享给大家供大家参考,具体如下: 在前面一篇<Java二叉搜索树遍历操作>中完成了树的遍历,这一节中将对如何从二叉搜索树中删除最大元素和最小元素做介绍: 我们要想删除二分搜索树的最小值和最大值,就需要先找到二分搜索树的最小值和最大值,其实也还是很容易的,因为根据二叉搜索树的特点,它的左子树一定比当前节点要小,所以二叉搜索树的最小值一定是左子树一直往下走,一直走到底.同样在二叉搜索树中,右子树节点值,一定比当前节点要大,所以右子树一直

随机推荐