js 数据结构 树(二叉搜索树的实现)

Posted lin-fighting

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了js 数据结构 树(二叉搜索树的实现)相关的知识,希望对你有一定的参考价值。

二叉树重要的特性

  • 一个二叉树第i层的最大节点数为 2 ^ (i - 1), i>=1
  • 深度为k的二叉树有最大节点总数为: 2 ^ k -1 k>=1
  • 对于任何非空二叉树T,若n0表示叶节点的个数,n2是度为2的非叶节点个数,那么两者的关系是 n0 = n2 + 1

完美二叉树

除了最下面的叶子节点,其他节点的度均为2。

完全二叉树,

除了二叉树最后一层外,其他各层的节点数都达到最大个数。
且组后一层从左向右的叶子节点连续存在,只缺若干节点,
完美二叉树是特殊的完全二叉树

这个并不是完全二叉树,D下面的一个叶子节点不在了。

二叉树的存储

常见的有数组和链表。
使用数组:

  • 完全二叉树:按从上往下,从左往右进行排序。(优)
  • 非完全二叉树:非完全二叉树要转成完全二叉树擦能按照上面的方案存储,但是会造成很大的空间浪费。
    使用链表:
    每个节点封装成一个node,包含存储的数据,左节点的引用,和右节点的引用。

二叉搜索树BST

二叉搜索树可以是空
但是非空的情况下:
需要满足:

  • 非空左子树的所有键值小于其根节点的键值
  • 非空右子树的所有键值大于其根节点的键值
  • 左右子树本身也是二叉搜索树。


    第一个不是,因为有左子树。非空左子树,其所有键值就是10< 18, 7 < 18,满足,非空右子树的所有键值20>18, 22 > 18满足,第三个条件,左右子树本身也是二叉搜索树。
    对于10来说,非空左子树为7,小于10,满足,但是非空右子树为5,不满足。

特点:

  • 相对较小的值总是保存在左节点,相对较大的值,总是保存在右节点。
  • 查找效率高,因为二叉搜索树已经排好序了。

实现二叉搜索树的封装


整个基础架构就是这样,树的每个节点有三个指针,分别存放value,左子节点,右子节点。

常见的二叉搜索树操作

插入

思路:利用二叉搜索树的特性,从root结点开始判断,如果大,就跟root.right节点判断,如果小,就跟root.left的节点判断,依次类推,直到判断有一个节点比newNode小并且右子节点没有值等等的情况,就可以插入了。


先跟根节点比较。

递归比较,插入。



大的节点都在根节点2的右边,小的节点都在根节点的左边。完成。
遍历:先序遍历,中序遍历,后序遍历,分别对应根节点在头部,中部,尾部处理。

先序遍历

树结构大概是

先访问根节点,再先序遍历左子树,再先序遍历右子树。所以结果应该为,4,2,3,3.5,5,6

初始化先处理root节点,然后递归调用,处理left节点。再到处理right节点。这就是先序遍历。

结果一样。

中序遍历

就是先中序遍历左子树,再访问根节点,最后中序遍历右子树。
结果应该是:2,3,3.5,4,5,6

实现的话只需要换个顺序处理,先处理左子树的节点就可以。


结果同预想的一样。

后序遍历

先后续遍历左子树,再后序遍历右子树,最后遍历根节点。所以结果应该是:2,3.5,3,5,6,4

也是调换顺序就行。

结果也同预想一样,遍历就完成了。

最大值和最小值


只要找出最左边和最右边的值就行。

搜索特定的值

二叉搜索树不仅获最值效率高,而且搜索特定值的效率也很高。

搜索只需要判断搜索的值与当前值的大小相比,大的就走右边继续递归比较,小的就走左边递归比较。直到找到为止

层级遍历

就是一层一层从左往右遍历:
思路就是维护一个数组,从根节点开始处理,将其左右子树放入数组,然后索引递增,处理每个节点即可。

如上,先处理根节点,放入其左右子树,然后处理左右子树,将他们的左右子树继续放入数组,最后数组就是的顺序就是一层一层获取的。


顺序正确

删除节点操作

删除节点主要分为三种情况

  • 1 被删除的节点,没有子节点,也就是叶子节点
  • 2 被删除的节点,只有一个左节点,或者右节点
  • 3 被删除的节点,两边都有子节点

第三个最复杂。先完成12.

先获取到删除的节点current,再获取删除的节点的父节点Paren,然后获取删除的节点current是父结点parent的右节点还是左节点。如果是叶子节点,就直接删除

如果是只有一个子节点,判断被删除的节点current的唯一一个子节点是在哪边。
然后直接让父亲指向current的子节点,跳过current节点。
第三种也是最复杂的方式,两边都有子节点。
我们可以通过一个规律:要找被删除的节点current的大一点点或者小一点的值来代替。

  • 前驱:被删除节点的小一点点的值,也就是current的左子树中最大的值。
  • 后继:被删除节点大一点点的值,也就是current的右子树中最小的值。

定义两个方法来获取这两个节点。
获取前驱,注意是从当前节点的左子树找最大值,所以需要循环遍历一直往右边找。需要使用parent来切断前驱节点与它的父结点的关系。

如果只有一层子节点,表示current的左子节点就是前驱,让parent.left置为null。而不止一层的话就需要往右边查找,直到找到前驱,然后切断前驱的父结点与他的关系,因为是往右边找,所以切断right。

后继也是一样的道理。
然后来看看实现:

获取前驱后继结点之后,要替换掉被删除节点,还要接手他的左右子节点。
让父结点指向后继,让后继接手被删除节点的左右子节点。

测试:
原本是这样,

删除3之后,找后继来代替,后继是右子树最小的之,也就是3.1。所以应改变这样
3.1替换了3。
看效果:

3.1代替了3的位置,并且接手了他的两个左右子树

删除完成

反转二叉搜索树

简单的将遍历,在遍历过程中左右子树直接替换就行。

如上,只要通过层级遍历,然后全部节点反转其左右子树就行了。

二叉搜索树就完成了

总结

  • 相对较小的值总是保存在左节点,相对较大的值,总是保存在右节点。
  • 查找效率高,因为二叉搜索树已经排好序了。

代码:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <script>
    //二叉搜索树的封装
    class Node 
      constructor(key) 
        this.key = key;
        this.left = null;
        this.right = null;
        this.tree = []; // 遍历
      
    

    class Tree 
      constructor() 
        this.root = null;
      

      static insertNode(node, newNode) 
        if (node.key < newNode.key) 
          if (!node.right) 
            node.right = newNode;
            return;
          
          Tree.insertNode(node.right, newNode);
         else 
          if (!node.left) 
            node.left = newNode;
            return;
          
          Tree.insertNode(node.left, newNode);
        
      

      insert(key) 
        const newNode = new Node(key);
        if (!this.root) 
          this.root = newNode;
         else 
          Tree.insertNode(this.root, newNode);
        
      

      getMax() 
        //往左边找就行了
        let current = this.root;
        while (current.right) 
          current = current.right;
        
        return current.key;
      

      getMin() 
        let current = this.root;
        while (current.left) 
          current = current.left;
        
        return current.key;
      

      search(searchKey) 
        return this._search(this.root, searchKey);
      

      _search(node, searchKey) 
        if (node) 
          if (node.key > searchKey) 
            return this._search(node.left, searchKey);
           else if (node.key < searchKey) 
            return this._search(node.right, searchKey);
           else 
            return node;
          
         else 
          return undefined;
        
      

      delete(key) 
        // 找到删除的节点
        // 三种情况 一 叶子节点 二 删除只有一个子节点 三 删除只有两个子节点的节点
        let current = this.root;
        let isLeftChild = true;
        let parent = null;
        // 获取对应的节点
        while (current && current.key !== key) 
          parent = current;
          if (current.key < key) 
            current = current.right;
            isLeftChild = false;
           else 
            current = current.left;
            isLeftChild = true;
          
        
        if (!current) 
          return undefined;
        
        const replacePosition = isLeftChild ? "left" : "right";
        const twoSonExit = current.left && current.right;
        if (twoSonExit) 
          // 两个子节点都存在, 
          // 规律:找删除节点大一点点或者小一点点的值,比删除节点大一点点,就是删除节点右子树的最小值,
          // 比删除节点小一点点,就是删除节点左子树的最大值
          // 前驱,后继。当前节点的前驱,就是比current小一点点的节点,删除节点左子树的最大值 。
          // 后继:当前节点的后继,就是比current大一点的节点。
          // 情况 1获取前驱, 而前驱后继一定是叶子节点
          //const preNode2 = this.getPreNode(current)
          // 情况2 获取后继
          const preNode = this.getEpiNode(current)
          // preNode替换current
          if (parent) 
            //很可能删除的就似乎根节点,那么parent就没值
            parent[replacePosition] = preNode
          
          preNode.left = current.left
          preNode.right = current.right
         else if (current.left || current.right) 
          //一个子节点存在
          const isLeft = !!current.left;
          if (parent) 
            //很可能删除的就似乎根节点,那么parent就没值
            parent[replacePosition] = current[isLeft ? "left" : "right"];
          
         else 
          //叶子节点
          if (parent) 
            //很可能删除的就似乎根节点,那么parent就没值
            parent[replacePosition] = null;
          
        
        return current;
      

      //获取前驱,找出左子树的最大值
      getPreNode(node) 
        // 因为是有两个节点的处理,所以node一定有left和right
        let preNode = node.left
        let parent = node
        // 循环遍历找出左子树最大的值,就是一直看有没有右节点即可。
        while (preNode && preNode.right) 
          parent = preNode
          preNode = preNode.right
        
        if (parent === node) 
          //如果只有一层,就直接让左节点为空,因为左节点就被作为前驱拿来代替了。
          parent.left = null
         else 
          //否则就是往右边继续找。
          parent.right = null
        
        return preNode
      

      // 后继 找出右子树的最小值
      getEpiNode(node) 
        // 因为是有两个节点的处理,所以node一定有left和right
        let preNode = node.right
        let parent = node
        // 循环遍历找出右子树最小的值,就是一直看有没有左节点即可。
        while (preNode && preNode.left) 
          parent = preNode
          preNode = preNode.left
        
        if (parent === node) 
          //只有一层子节点,直接让右节点作为后继代替
          parent.right = null
         else 
          // 否则就是往左边继续找
          parent.left = null
        
        return preNode
      

      //遍历树
      //三种方式 先序 中序 后续

      // 后序 先  后序遍历左子树,再后序遍历右子树,再遍历跟节点。
      epiOrderTraversal(arr) 
        Tree.EpiOrderTarversal(this.root, arr);
      

      static EpiOrderTarversal(node, arr) 
        if (node !== null) 
          //处理当前节点的右节点
          Tree.EpiOrderTarversal(node.left, arr);
          Tree.EpiOrderTarversal(node.right, arr); //递归遍历
          arr.push(node.key); //中间处理node,中序遍历
        
      

      //中序遍历
      middleOrderTraversal(arr) 
        Tree.middleOrderTraversalNode(this.root, arr);
      

      //中序遍历 中序遍历其左子树,访问根节点 中序遍历右子树 ,根节点是在中间处理的。
      static middleOrderTraversalNode(node, arr) 
        if (node !== null) 
          //处理当前节点的左节点
          Tree.middleOrderTraversalNode(node.left, arr); //递归遍历
          arr.push(node.key); //中间处理node,中序遍历
          Tree.middleOrderTraversalNode(node.right, arr);
        
      

      // 先序,访问跟节点,先序遍历其左子树,先序遍历其右子树,
      preOrderTraversal(arr) 
        Tree.preOrderTraversalNode(this.root, arr);
      

      //先序遍历 为啥叫先序遍历,根节点是在最开始处理的。
      static preOrderTraversalNode(node, arr) 
        if (node !== null) 
          //处理当前节点的左节点
          arr.push(node.key); //先处理node,先序遍历
          Tree.preOrderTraversalNode(node.left, arr); //递归遍历
          Tree.preOrderTraversalNode(node.right, arr);
        
      

      // 层序遍历, 一层一层从左到右执行,
      // 思路就是维护一个数组,然后从根节点开始,一层一层放入,然后通过索引一个一个拿出来处理,将其左右子树继续放入数组
      // 如先放入根节点,那么就是先处理根节点,将根节点的左右子树放入,然后索引向前,继续处理根节点的左子树,依次类推进行处理。
      levelOrderTraverSalNode() 
        const stack = [this.root]
        let index = 0
        let currentNode;
        while (currentNode = stack[index]) 
          if (currentNode.left) 
            // 处理当前节点的左子树,插入进去
            stack.push(currentNode.left)
          
          if (currentNode.right) 
            // 处理当前节点的右子树,插入禁区
            stack.push(currentNode.right)
          
          index++;
        
        return stack
      


      // 反转二叉树, 直接遍历然后左右交换就可以了,利用层级遍历来处理
      rever() 
        const stack = [this.root]
        let index = 0
        let currentNode;
        while (currentNode = stack[index]) 
          // 先反转,然后继续遍历,继续反转。
          let temp = currentNode.left
          currentNode.left = currentNode.right
          currentNode.right = temp
          //
          if (currentNode.left) 
            // 处理当前节点的左子树,插入进去
            stack.push(currentNode.left)
          
          if (currentNode.right) 
            // 处理当前节点的右子树,插入禁区
            stack.push(currentNode.right)
          
          index++;
        
        return stack
      
    

    const test = new Tree();
    test.insert(4);
    test.insert(3);
    test.insert(3.5);
    test.insert(6);
    test.insert(2);
    test.insert(1);
    test.insert(2.5);
    test.insert(3.1)
    test.insert(3.6)
    test.insert(5);
    // const arr = [];
    // const arr2 = [];
    // const arr3 = [];
    // test.preOrderTraversal(arr);
    // console.log(arr);
    // test.middleOrderTraversal(arr2);
    // console.log(arr2);
    // test.epiOrderTraversal(arr3);
    // console.log(arr3);
    console.log(test.levelOrderTraverSalNode());

  </script>
</body>

</html>

以上是关于js 数据结构 树(二叉搜索树的实现)的主要内容,如果未能解决你的问题,请参考以下文章

高阶数据结构 | 二叉搜索树(Binary Search Tree)

用JS实现二叉搜索树

❤️数据结构入门❤️(2 - 1)- 二叉搜索树

C++-二叉搜索树的查找&插入&删除-二叉搜索树代码实现-二叉搜索树性能分析及解决方案

二叉搜索树的理解以及AVL树的模拟实现

二叉搜索树的理解以及AVL树的模拟实现