关于javascript:二叉树中的小钢炮🔍二叉查找树

原文参考我的公众号文章 梳理一波「二叉树

二叉树

二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,别离是左子节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。

  • 根节点:无父节点的节点
  • 叶子结点:无子节点的节点
  • 兄弟节点:有雷同根节点的节点

对于“树”,还有三个比拟类似的概念:高度(Height)、深度(Depth)、层(Level)。它们的定义是这样的:

二叉查找(排序)树

  • 也叫二叉搜寻树,反对动态数据汇合的疾速插入、删除、查找操作;
  • 有序二叉树:在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值;
  • 是一颗二叉树,每个节点最多两个子结点,即每一层最多 2^(level – 1)个节点;
树结构
/** 树节点 */
class BinaryNode {
  constructor(data) {
    this.data = data; //存储的数据
    this.left = null; //左节点
    this.right = null; //右节点
    this.level = 1; //所属层级
    this.count = 1; //反复数据次数
    this.isDeleted = false; //是否被删除
  }
}

/** 二叉树 */
class BinarySearchTree {
  constructor() {
    this.root = null; //根节点:在第一次insert时会被赋予值
    this.nodes = 0; //节点数
    this.list = []; //遍历二叉树时长期存储数据的数组
  }
}
插入节点

插入规定:新结点值与以后插入地位(从根节点开始)的节点值进行比拟

  • 小于父节点值:插在父节点左节点上;
  • 大于父节点值:插在父节点右节点上;
  • 等于父节点值:父节点 count++;(这种形式看状况,如果结点还有其余数据而非只有一个 data,那能够。否则应合并到上一种状况,合并到右节点上)
  • 插入过程可减少被插入的结点所属 level 的保护!
// class BinarySearchTree { ... }

  /**
   * 插入数据
   * @param {*} data 结点的值
   */
  insert(data) {
    this.nodes++;

    // 递归插入
    // let newNode = new BinaryNode(data);
    // if (this.root === null) {
    //   this.root = newNode;
    // } else {
    //   this._insert(this.root, newNode);
    // }

    // 循环插入
    this.loopInsert(data)
  }

  /**
   * 递归的形式插入节点:新结点值与以后插入地位(从根节点开始)的节点值进行比拟
   * 小于根节点值:插在根节点左节点上;
   * 大于等于根节点值:插在根节点右节点上;
   * 减少结点所属level的保护!
   * @param {*} theRootNode 被插入地位节点(从根节点开始)
   * @param {*} newNode 新插入的节点
   */
  _sameValAsBigInsert(theRootNode, newNode) {
    newNode.level++; //从根登程,每执行一次_sameValAsBigInsert,层级便主动+1
    if (theRootNode.data > newNode.data) {
      if (theRootNode.left === null) {
        theRootNode.left = newNode;
      } else {
        // 阐明不是叶子结点,持续向上层插入判断
        this._sameValAsBigInsert(theRootNode.left, newNode);
      }
    } else {
      if (theRootNode.right === null) {
        theRootNode.right = newNode;
      } else {
        // 阐明不是叶子结点,持续向上层插入判断
        this._sameValAsBigInsert(theRootNode.right, newNode);
      }
    }
  }

  /**
   * !!!值雷同,则合并到以后根节点
   * @param {*} theRootNode 被插入地位节点(从根节点开始)
   * @param {*} newNode 新插入的节点
   */
  _sameValMergeInsert(theRootNode, newNode) {
    newNode.level++; //从根登程,每执行一次_sameValMergeInsert,层级便主动+1
    if (theRootNode.data > newNode.data) {
      if (theRootNode.left === null) {
        theRootNode.left = newNode;
      } else {
        // 阐明不是叶子结点,持续向上层插入判断
        this._sameValMergeInsert(theRootNode.left, newNode);
      }
    } else if (theRootNode.data < newNode.data) {
      if (theRootNode.right === null) {
        theRootNode.right = newNode;
      } else {
        // 阐明不是叶子结点,持续向上层插入判断
        this._sameValMergeInsert(theRootNode.right, newNode);
      }
    } else {
      theRootNode.count++; // 值雷同,则合并到以后根节点
    }
  }

   /**
   * 循环的形式插入
   * @param {*} data
   * @returns
   */
  loopInsert(data) {
    if (this.root == null) {
      this.root = new BinaryNode(data);
      return;
    }

    let p = this.root;
    while (p != null) {
      if (data > p.data) {
        if (p.right == null) {
          p.right = new BinaryNode(data);
          return;
        }
        p = p.right;
      } else {
        // data <= p.data
        if (p.left == null) {
          p.left = new BinaryNode(data);
          return;
        }
        p = p.left;
      }
    }
  }

遍历树(前序、中序、后续)

遍历树采取递归的形式,出递归的条件是 node=null

  /** 遍历树: [type] 遍历形式 */
  print(type) {
    this.list = [];
    let callee = `${type}Order`;
    this[callee](this.root);
  }
  • 前序遍历
prevOrder(node){
    if(node==null){
        return
    }
    console.log(node.data)
    this.prevOrder(node.left)
    this.prevOrder(node.right)
}
  • 中序遍历(遍历后果正好是有序的)
inOrder(node){
    if(node==null){
        return
    }
    this.inOrder(node.left)
    console.log(node.data)
    this.inOrder(node.right)
}
  • 后序遍历
postOrder(node){
    if(node==null){
        return
    }
    this.postOrder(node.left)
    this.postOrder(node.right)
    console.log(node.data)
}

查找节点

先取根节点,如果它等于咱们要查找的数据,那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归查找;如果要查找的数据比根节点的值大,那就在右子树中递归查找。

  /**
   * 查找一个节点
   * @param {*} data 要查找的值
   * @returns
   */
  find(data) {
    let node = this.root;
    while (node != null) {
      if (data < node.data) {
        node = node.left;
      } else if (data > node.data) {
        node = node.right;
      } else {
        return node;
      }
    }

    return null;
  }
删除节点
物理删除:删除操作比较复杂,个别要分以下三种状况别离解决。
  • 被删除节点没有子节点:间接将父节点中,指向要删除节点的指针置为 null;
  • 被删除节点只有一个子节点:只须要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点(左或右)就能够了;
  • 被删除节点右两个子节点(最简单):咱们须要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。而后再删除掉这个最小节点,因为最小节点必定没有左子节点(如果有左子结点,那就不是最小节点了),所以,咱们能够利用下面两条规定来删除这个最小节点;
逻辑删除节点(简易版)
  /**
   * 删除节点(简易版)
   * 对要删除的节点做标记,示意已删除
   * @param {*} data
   */
  delete(data){
    let targetNode = this.find(data);
    // 未找到要删除的数据
    if(targetNode == null){
        return null
    }
    // 标记为“已删除”
    targetNode.isDeleted = true;
  }
}
逻辑删除对应的树遍历形式批改!
  prevOrder(node) {
    if (node == null) {
      return;
    }
    /**+++ 此处减少判断,inOrder和postOrder同样解决! */
    if (node.isDeleted != true) {
      this.list.push(node.data);
      console.log(node.data);
    }
    this.prevOrder(node.left);
    this.prevOrder(node.right);
  }

测试有序二叉树
// 创立一颗有序二叉树
let bsTree = new BinarySearchTree();

// 插入数据
let arr = [6,8,7,6,3,5,9,4]
arr.map(item=>{
    bsTree.insert(item)
})

// 二叉树构造
console.log(bsTree);

// 三种形式遍历整棵树!
console.log('前序遍历');
bsTree.prevOrder(bsTree.root)
console.log('\n\n')
console.log('中序遍历(有序输入)');
bsTree.inOrder(bsTree.root)
console.log('\n\n')
console.log('后序遍历');
bsTree.postOrder(bsTree.root)
console.log('\n\n')

// 查找某个节点
console.log("\n查找值6");
console.log(bsTree.find(6));

// 删除某个节点
console.log("\n删除6");
console.log(bsTree.delete(6));
console.log("\n\n");

console.log("删除后-前序遍历");
bsTree.print("prev");

总结(来自极客工夫课程总结,总归比我本人总结的好 🤷‍♂️)

这篇文章学习了一种非凡的二叉树,二叉查找树。它反对疾速地查找、插入、删除操作。

二叉查找树中,每个节点的值都大于左子树节点的值,小于右子树节点的值。不过,这只是针对没有反复数据的状况。对于存在反复数据的二叉查找树,我介绍了两种构建办法,

  • 一种是让每个节点存储多个值雷同的数据;
  • 另一种是,每个节点中存储一个数据。针对这种状况,只须要稍加革新原来的插入、删除、查找操作即可。

在二叉查找树中,查找、插入、删除等很多操作的工夫复杂度都跟树的高度成正比。两个极其状况的工夫复杂度别离是 O(n) 和 O(logn),别离对应二叉树进化成链表的状况和齐全二叉树。

参考链接

js 二叉树

数据结构与算法之美-二叉树根底下

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理