Typescript实现二叉树搜索数据结构

178 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 12 月更文挑战」的第 7 天,点击查看活动详情

最近在学习 Javascript 数据结构与算法相关知识,数据结构与算法对程序员来说就像是内功心法,只有不断修炼自己的内功,才能从容不迫的应对市场上各种让人应接不暇的框架,达到以不变应万变。学习数据结构与算法的过程中不仅要能看懂更要多写多练,今天就来手写下二叉搜索树数据结构。

二叉树中的节点最多只能有两个子节点: 一个是左侧子节点,另一个是右侧子节点。 二叉搜索树是二叉树的一种,但是只允许你在左侧节点存储比父节点小的值,在右侧节点存储比父节点大的值

二叉搜索树基本操作方法:

  • insert(key):向树中插入一个新的键。
  • search(key):在树中查找一个键。如果节点存在,则返回 true;如果不存在,则返回 false。
  • inOrderTraverse():通过中序遍历方式遍历所有节点。
  • preOrderTraverse():通过先序遍历方式遍历所有节点。
  • postOrderTraverse():通过后序遍历方式遍历所有节点。
  • min():返回树中最小的值/键。
  • max():返回树中最大的值/键。
  • remove(key):从树中移除某个键。

创建 BinarySearchTree 类

class Node<K> {
  left: Node<K>; // 左侧子节点引用
  right: Node<K>; // 右侧子节点引用

  constructor(public key: K) {}

  toString() {
    return `${this.key}`;
  }
}

class BinarySearchTree {
  protected root: Node<T>;
  constructor() {}
}

向二叉搜索树中插入一个键

class BinarySearchTree {
  // ...

  insert(key: T) {
    if (!this.root) {
      this.root = new Node(key);
    } else {
      this.insertNode(this.root, key);
    }
  }
  protected insertNode(node: Node<T>, key: T) {
    // 这里我们默认key是数值
    if (key < node.key) {
      // 新节点小于当前节点 插入左子树
      if (!node.left) {
        node.left = new Node(key);
      } else {
        this.insertNode(node.left, key);
      }
    } else {
      // 新节点大于当前节点 插入右子树
      if (!node.right) {
        node.right = new Node(key);
      } else {
        this.insertNode(node.right, key);
      }
    }
  }
}

中序遍历

中序遍历是一种以上行顺序访问 BST 所有节点的遍历方式,也就是以从最小到最大的顺序访问所有节点。先访问左节点 在访问根节点 最后访问右节点

class BinarySearchTree {
  // ...
  inOrderTraverse(callback: Function) {
    this.inOrderTraverseNode(this.root, callback);
  }

  private inOrderTraverseNode(node: Node<T>, callback: Function) {
    if (node != null) {
      this.inOrderTraverseNode(node.left, callback);
      callback(node.key);
      this.inOrderTraverseNode(node.right, callback);
    }
  }
}

先序遍历

先序遍历是以优先于后代节点的顺序访问每个节点的。先访问根节点 在访问左节点 最后访问右节点

class BinarySearchTree {
  // ...
  preOrderTraverse(callback: Function) {
    this.preOrderTraverseNode(this.root, callback);
  }

  private preOrderTraverseNode(node: Node<T>, callback: Function) {
    if (node != null) {
      callback(node.key);
      this.preOrderTraverseNode(node.left, callback);
      this.preOrderTraverseNode(node.right, callback);
    }
  }
}

后续遍历

后序遍历则是先访问左节点,再访问右点,最后访问根节点

class BinarySearchTree {
  // ...
  postOrderTraverse(callback: Function) {
    this.postOrderTraverseNode(this.root, callback);
  }

  private postOrderTraverseNode(node: Node<T>, callback: Function) {
    if (node != null) {
      this.postOrderTraverseNode(node.left, callback);
      this.postOrderTraverseNode(node.right, callback);
      callback(node.key);
    }
  }
}

搜索最大值和最小值

对于寻找最小值,总是沿着树的左边;而对于寻找最大值,总是沿着树的右边。

class BinarySearchTree {
  // ...
  min() {
    return this.minNode(this.root);
  }

  protected minNode(node: Node<T>) {
    let current = node;
    while (current != null && current.left != null) {
      current = current.left;
    }
    return current;
  }

  max() {
    return this.maxNode(this.root);
  }

  protected maxNode(node: Node<T>) {
    let current = node;
    while (current != null && current.right != null) {
      current = current.right;
    }
    return current;
  }
}

搜索一个特定的值

如果要找的键比当前的节点小,那么继续在左侧的子树上搜索。 如果要找的键比当前的节点大,那么就从右侧子节点开始继续搜索, 否则就说明要找的键和当前节点的键相等,返回 true 来表示找到了这个键

class BinarySearchTree {
  // ...
  search(key: T) {
    return this.searchNode(this.root, key);
  }

  private searchNode(node: Node<T>, key: T): boolean {
    if (node == null) {
      return false;
    }

    if (key < node.key) {
      return this.searchNode(node.left, key);
    } else if (key > node.key) {
      return this.searchNode(node.right, key);
    }
    return true;
  }
}

移除一个节点

第一种情况:该节点是一个没有左侧或右侧子节点的叶节点 第二种情况: 移除有一个左侧或右侧子节点的节点 这种情况下,需要跳过这个节点,直接将父节点指向它的指针指向子节点。 第三种情况:

要移除的节点有两个子节点——左侧子节点和右侧子节点 当找到了要移除的节点后,需要找到它右边子树中最小的节点(它的继承者——行。 然后,用它右侧子树中最小节点的键去更新这个节点的值。通过这一步,我们改变了这个节点的键,也就是说它被移除了。 但是,这样在树中就有两个拥有相同键的节点了,这是不行的。要继续把右侧子树中的最小节点移除,毕竟它已经被移至要移除的节点的位置了。 最后,向它的父节点返回更新后节点的引用

class BinarySearchTree {
  // ...
  remove(key: T) {
    this.root = this.removeNode(this.root, key);
  }

  protected removeNode(node: Node<T> | null, key: T) {
    if (node == null) {
      return null;
    }

    if (key < node.key) {
      node.left = this.removeNode(node.left, key);
      return node;
    } else if (key > node.key) {
      node.right = this.removeNode(node.right, key);
      return node;
    } else {
      // 第一种情况
      if (node.left == null && node.right == null) {
        node = null;
        // 现在节点的值已经是null 了,父节点指向它的指针也会接收到这个值,这也是我们为什么要在函数中返回节点的值
        return node;
      }
      // 第二种情况
      if (node.left == null) {
        node = node.right;
        return node;
      } else if (node.right == null) {
        node = node.left;
        return node;
      }

      // 第三种情况
      const aux = this.minNode(node.right);
      node.key = aux.key;
      node.right = this.removeNode(node.right, aux.key);
      return node;
    }
  }
}

使用场景

leetcode 练习题:104

二叉树的最大深度 给定一个二叉树,找出其最大深度。 二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

var maxDepth = function (root) {
  if (root === null) {
    return 0;
  } else {
    const leftHeight = maxDepth(root.left);
    const rightHeight = maxDepth(root.right);
    return Math.max(leftHeight, rightHeight) + 1;
  }
};

解题思路
如果我们知道了左子树和右子树的最大深度 l 和 r,那么该二叉树的最大深度即为max(l,r) + 1而左子树和右子树的最大深度又可以以同样的方式进行计算。

因此我们可以用「深度优先搜索」的方法来计算二叉树的最大深度。具体而言,在计算当前二叉树的最大深度时,可以先递归计算出其左子树和右子树的最大深度,然后在 O(1)时间内计算出当前二叉树的最大深度。递归在访问到空节点时退出。