头图

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

二叉树

二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。
  • 根节点:无父节点的节点
  • 叶子结点:无子节点的节点
  • 兄弟节点:有相同根节点的节点

关于“树”,还有三个比较相似的概念:高度(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 二叉树

数据结构与算法之美-二叉树基础下


Believer
47 声望5 粉丝

无法忍受尘世间的丑 便看不到尘世间的美