JavaScript 数据结构和算法——树

143 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 8 天,点击查看活动详情

介绍

树是一种分层数据的抽象模型。现实生活中最常见的树的例子是家谱,或是公司的组织架构。

树的相关术语

一个树结构包含一系列存在父子关系的节点。每个节点都有一个父节点(除了顶部的第一个节点)以及零个或多个子节点。 tree.png

根节点

位于树顶部的节点叫做根节点,它没有父节点。树种的每个元素都叫做节点,节点分为内部节点外部节点。至少有一个子节点的节点叫做内部节点,没有子元素的节点叫做外部节点。

叶子结点

没有一个子元素的叫做叶子结点(3、6、8、10、12、14、18、25)。

子树

子树有节点和他的后代组成。上图当中(12、14、13)构成一个子树。

深度

节点的深度取决于它的祖先节点的数量。例如,节点 3 有 3 个祖先节点,那么它的深度就是 3。

二叉搜索树

二叉树

二叉树中的节点最多只能有两个节点:一个是左侧子节点,另外一个是右侧子节点。 二叉搜索树是二叉树的一种,但是只允许你在左侧节点添加比父节点小的值,而在右侧节点添加比父节点大的值。上面的图中展现了一颗二叉搜索树。

二叉搜索树实现

//实现二叉搜索树
class TreeNode<T> {
  //二叉树节点的数据域
  public key: T;
  //指向左子树
  public left: TreeNode<T>;
  //指向右子树
  public right: TreeNode<T>;

  constructor(key: T) {
    //初始化数据域
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

interface IBinarySearchTree<T> {
  //    插入节点
  insert(key: T): boolean;

  //   搜索树当中是否存在某个节点
  search(key: T): boolean;

  //   中序遍历
  inOrderTraverse();

  //   先序遍历
  preOrderTraverse();

  //   后序遍历
  postOrderTraverse();

  //   返回树当中的最小值
  min(): T;

  //   返回树当中的最大值
  max(): T;

  //   删除树当中的指定节点
  remove(key: T): boolean;

  //    删除指定节点树的节点
  removeNode(node: TreeNode<T>, key: T): TreeNode<T>;

  //   将值插入都某个节点当中
  insertNode(node: TreeNode<T>, key: T): boolean;

  //   搜索树当中是否存在某个节点值
  searchNode(node: TreeNode<T>, key: T): boolean;

  //    中序遍历某个节点树
  inOrderTraverseNode(node: TreeNode<T>);

  //    先序遍历某个节点树
  preOrderTraverseNode(node: TreeNode<T>);

  //    后序遍历某个节点
  postOrderTraverseNode(node: TreeNode<T>);

  //    查找树的最小值
  minNode(node: TreeNode<T>): T;

  //   查找树的最大值
  maxNode(node: TreeNode<T>): T;
}

/**
 *
 * @param nodeKey 比较的节点值
 * @param key 比较的插入值
 * @returns 返回比较结果 -1节点值大于插入值 1插入值大于节点值
 */
function compare<T>(nodeKey: T, insertKey: T) {
  if (nodeKey > insertKey) {
    return -1;
  } else if (nodeKey < insertKey) {
    return 1;
  } else {
    return 0;
  }
}

class BinarySearchTree<T> implements IBinarySearchTree<T> {
  private root: TreeNode<T>;
  private compare: (nodeKey: T, insertKey: T) => number;
  constructor(compare: (nodeKey: T, insertKey: T) => number) {
    //创建树的根节点
    this.root = null;
    //加入自定义比较函数
    this.compare = compare;
  }

  /**
   *
   * @param key 搜索值
   * @returns 返回是否搜索到
   */
  searchNode(node: TreeNode<T>, key: T): boolean {
    if (this.compare(node.key, key) === -1) {
      //比当前节点值小
      if (node.left === null) {
        //判断当前节点的左子节点是否含有值,如果没有则说明没有找到
        return false;
      } else {
        //有值的话就继续找
        this.searchNode(node.left, key);
      }
    } else if (this.compare(node.key, key) === -1) {
      //比当前节点值大
      if (node.right === null) {
        //判断当前节点的右子节点是否含有值,如果没有则说明没有找到
        return false;
      } else {
        //有值的话就继续找
        this.searchNode(node.right, key);
      }
    } else {
      //等于当前节点值
      return true;
    }
  }

  /**
   *
   * @param node 需要插入值的节点
   * @param key 插入的值
   * @returns 返回是否插入成功
   */
  insertNode(node: TreeNode<T>, key: T): boolean {
    //先判断插入值比跟根节点大还是小,小的在左边,大的在右边
    if (this.compare(node.key, key) === -1) {
      //插入值比节点值小,放树的左边
      if (node.left === null) {
        //如果左子树为空,则直接创建节点
        node.left = new TreeNode<T>(key);
      } else {
        //左子树含有值,那么就和左子树的值进行比较
        this.insertNode(node.left, key);
      }
    } else {
      // 插入值比节点值大,放树的右边
      if (node.right === null) {
        //当右子树为空,则直接创建节点
        node.right = new TreeNode<T>(key);
      } else {
        //左子树含有值,我们需要继续和左子树的值进行比较
        this.insertNode(node.right, key);
      }
    }
    return false;
  }
  /**
   *
   * @param key 插入节点的值
   * @returns 返回是否插入成功
   */
  insert(key: T): boolean {
    //先判断当前插入的是否为根节点
    if (this.root === null) {
      this.root = new TreeNode<T>(key);
      return true;
    } else {
      //当前插入不是根节点,需要进行判断
      return this.insertNode(this.root, key);
    }
  }

  /**
   *
   * @param key 需要搜索的值
   * @returns 返回是否搜索到对应的键值
   */
  search(key: T): boolean {
    //先判断当前搜索值比节点大还是小,如果大就在右子树,小就在左子树,相等就找到值
    if (this.root === null) {
      return false;
    } else {
      return this.searchNode(this.root, key);
    }
  }

  /**
   * @returns 返回节点当中值的中序排列
   */
  inOrderTraverse() {
    //进行先序遍历
    this.inOrderTraverseNode(this.root);
  }

  /**
   *中序遍历某个节点
   * @param node 传入需要遍历的节点
   */
  inOrderTraverseNode(node: TreeNode<T>) {
    //   如果根节点不是为null,那么就执行递归
    if (node !== null) {
      this.inOrderTraverseNode(node.left);
      //打印
      console.log(node.key);
      this.inOrderTraverseNode(node.right);
    }
  }

  /**
   *
   * @returns 返回节点当中值的先序排列
   */
  preOrderTraverse() {
    this.preOrderTraverseNode(this.root);
  }

  /**
   *
   * @param node 传入需要遍历的节点
   */
  preOrderTraverseNode(node: TreeNode<T>) {
    if (node !== null) {
      console.log(node.key);
      this.preOrderTraverseNode(node.left);
      this.preOrderTraverseNode(node.right);
    }
  }

  /**
   * @returns 返回节点当中值的后序排列
   */
  postOrderTraverse() {
    this.postOrderTraverseNode(this.root);
  }

  /**
   *
   * @param node 传入需要遍历的节点
   */
  postOrderTraverseNode(node: TreeNode<T>) {
    if (node !== null) {
      this.postOrderTraverseNode(node.left);
      this.postOrderTraverseNode(node.right);
      console.log(node.key);
    }
  }
  /**
   * @returns 返回二叉树当中的最小节点值
   */
  min(): T {
    return this.minNode(this.root);
  }

  /**
   *
   * @param node 需要查找最小值的树
   * @returns 返回找到的最小值
   */
  minNode(node: TreeNode<T>): T {
    //判断当前节点是否为null和左子节点是否为空
    if (node != null && node.left !== null) {
      return this.minNode(node.left);
    }
    return node.key;
    //如果当前节点不为空,但是左子节点为空,那么这个就是我们要找的最小节点
  }

  /**
   * 返回二叉树当中最大节点值
   */
  max(): T {
    return this.maxNode(this.root);
  }

  /**
   *
   * @param node 需要查找最大值的树
   * @returns 返回找到的最大值
   */
  maxNode(node: TreeNode<T>): T {
    if (node !== null && node.right !== null) {
      return this.maxNode(node.right);
    }
    return node.key;
  }

  /**
   *
   * @param key 删除某个节点
   * @returns 是否删除成功
   */
  remove(key: T): boolean {
    this.root = this.removeNode(this.root, key);
    return true;
  }

  /**
   *
   * @param node 需要查找删除节点的节点树
   * @param key 需要删除的键
   * @returns 返回更改后的节点
   */
  removeNode(node: TreeNode<T>, key: T): TreeNode<T> {
    if (node === null) {
      return null;
    }
    //判断需要删除值的位置
    if (this.compare(node.key, key) === -1) {
      //如果需要删除的值比节点值小,那么在节点树的左边
      node.left = this.removeNode(node.left, key);
      //返回更新后的节点
      return node;
    } else if (this.compare(node.key, key) === 1) {
      //如果删除的值比节点值小
      node.right = this.removeNode(node.right, key);
      //返回更新后的节点值
      return node;
    } else {
      //删除的值等于节点值,这时候代表我们已经找到了节点值,我们需要判断以下三种情况

      //第一种情况:当节点的左子节点和右子节点全部为null时
      if (node.left === null && node.right === null) {
        //将当前节点值清空
        node = null;
        //清空完当前节点,我们知道,父节点还对这个节点包含引用,还是可以找到这个节点,所以我们需要将父节点的引用删除
        return node;
      }

      //第二种情况:当前节点的左子节点或右子节点有一个为null
      if (node.left === null) {
        //将当前节点的值清除
        node.key = null;
        //返回当前节点的右子节点引用
        return node.right;
      } else if (node.right === null) {
        //将当前节点的值清除
        node.key = null;
        //返回当前节点的左子节点引用
        return node.left;
      }

      //   第三种情况:当节点的两个子节点都有值时

      //aux是当前节点右子树最小的节点
      let aux = this.minNode(node);

      //使用右子树的最小节点替换当前节点的值
      node.key = aux;

      //删除那个右子树最小节点
      node = this.removeNode(node, aux);

      //但会更新后的节点
      return node;
    }
  }
}

const binarySearchTree = new BinarySearchTree<number>(compare);