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 ,3,2,3.5, 6,5
在这里插入图片描述

初始化先处理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>
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

coderlin_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值