js中的数据结构(三) | 二叉树

291 阅读4分钟

| 常用数据结构

  1. 数组(Array)
  2. 栈(Stack)先进后出 (LIFO)
  3. 队列(Queue)先进先出(FIFO)
  4. 链表(Linked List)
  5. 树(Tree)
  6. 图(Graph)
  7. 堆(Heap)
  8. 散列表(Hash 哈希表

| 二叉树

既要保持顺序,又要快速查找、插入和删除,看来有序数组和散列表都不行
那还有什么数据结构可以选择?看看二叉树吧 二叉树也是基于结点的数据结构,与链表的区别在于树的结点可以有多个链指向

image.png

二叉树的特性

  • 每个结点的子结点数量最多为2,最少为0
  • 如有两个子结点,其中一个子节点大于结点,另一子节点小于结点
/**
 * 实现一个树结点
 * 1. 最多有左右子结点
 * 2. 存储值
 */
class TreeNode {
  value = null;
  leftChildNode = null;
  rightChildNode = null;
  constructor(value) {
    this.value = value;
  }
}

插入

有序数组查找需要O(log N),插入需要O(N),而二叉树都是只要O(logN)

插入规则

  1. 插入的值大于当前结点值放右边。
  2. 插入的值小于当前结点值放左边。
  3. 重复上面的操作
 /**
   * 插入结点
   * @param {*} value
   * @param {*} node
   * @returns
   */
  insert(value, node = null) {
    if (!this.firstNode) {
      this.firstNode = new TreeNode(value);
    }
    let newNode = new TreeNode(value);
    node = node || this.firstNode;

    if (value < node.value) {
      // 如果没有左结点
      if (!node.leftChildNode) return (node.leftChildNode = newNode);
      this.insert(value, node.leftChildNode);
    } else if (value > node.value) {
      // 如果没有右结点
      if (!node.rightChildNode) return (node.rightChildNode = newNode);
      this.insert(value, node.rightChildNode);
    }
  }

查询

平均情况下二叉树的查询效率是O(log N)
最坏情况下,二叉树的查找需要O(N)

查找规则

  1. 检视该结点的值。
  2. 如果正是所要找的值,返回值
  3. 如果要找的值小于当前结点的值,则在该结点的左子树查找。
  4. 如果要找的值大于当前结点的值,则在该结点的右子树查找。
  /**
   * 根据值查找结点
   * @param {*} value
   * @param {*} node
   * @returns
   */
  search(value, node) {
    if (!node || value == node.value) {
      return node;
    }

    if (value < node.value) {
      return this.search(value, node.leftChildNode);
    }

    if (value > node.value) {
      return this.search(value, node.rightChildNode);
    }
  }

删除

平均情况下二叉树的删除效率也是O(log N)

删除规则

  1. 删除的结点没有子结点,直接删掉
  2. 删除的结点有一个子结点,删掉它,将子结点填到被删除结点的位置上
  3. 删除的结点有两个子结点,则将该结点替换成其后继结点(比删除结点大的全部结点中最小的结点)
  4. 后继结点带有右子结点,则在后继结点填补被删除结点以后,用此右子结点替代后继结点的父节点的左子结点。
   /**
   * 根据值删除结点
   * @param {*} value
   * @param {*} node
   * @returns
   */
  delete(value, node) {
    if (!node) return node;
    
    // 去左结点找
    if (value < node.value) {
      node.leftChildNode = this.delete(value, node.leftChildNode);
      return node;
    }

    // 去右结点找
    if (value > node.value) {
      node.rightChildNode = this.delete(value, node.rightChildNode);
      return node;
    }

    // 找到了
    if (value === node.value) {
      // 如果只有左子节点,则让当前左子节点接替位置
      if (!node.rightChildNode) {
        return node.leftChildNode;
      }
      // 如果只有右子节点,则让当前右子节点接替位置
      else if (!node.leftChildNode) {
        return node.rightChildNode;
      } else {
        // 如果左右都有,当前结点值则为后继结点的值
        node.right = this.lift(node.rightChildNode, node);
        return node;
      }
    }
  }

  // 获取后继结点的值(比删除结点大的全部结点中最小的结点)
  lift(node, nodeToDelete) {
    if (node.leftChildNode) {
      node.leftChildeNode = lift(node.leftChildNode, nodeToDelete);
      return node;
    } else {
      node.rightChildNode = lift(node.rightChildNode, nodeToDelete);
    }
  }

| 最后

假若你要用有序数组里的数据来创建二叉树,最好先把数据洗乱。
因为只有用随意打乱的数据创建出来的树才有可能是比较平衡的。
要是插入的都是已排序的数据,那么这棵树就失衡了,它用起来也会比较低效。
如图

image.png
从中查找5,效率会是O(N)

整体代码

/**
 * 实现一个树结点
 * 1. 最多有左右子结点
 * 2. 存储值
 */
class TreeNode {
  value = null;
  leftChildNode = null;
  rightChildNode = null;
  constructor(value) {
    this.value = value;
  }
}

class Tree {
  firstNode = null;
  constructor(value) {
    value && (this.firstNode = new TreeNode(value));
  }

  /**
   * 插入结点
   * @param {*} value
   * @param {*} node
   * @returns
   */
  insert(value, node = null) {
    if (!this.firstNode) {
      this.firstNode = new TreeNode(value);
    }
    let newNode = new TreeNode(value);
    node = node || this.firstNode;

    if (value < node.value) {
      // 如果没有左结点
      if (!node.leftChildNode) return (node.leftChildNode = newNode);
      this.insert(value, node.leftChildNode);
    } else if (value > node.value) {
      // 如果没有右结点
      if (!node.rightChildNode) return (node.rightChildNode = newNode);
      this.insert(value, node.rightChildNode);
    }
  }

  /**
   * 根据值查找结点
   * @param {*} value
   * @param {*} node
   * @returns
   */
  search(value, node) {
    if (!node || value == node.value) {
      return node;
    }

    if (value < node.value) {
      return this.search(value, node.leftChildNode);
    }

    if (value > node.value) {
      return this.search(value, node.rightChildNode);
    }
  }

   /**
   * 根据值删除结点
   * @param {*} value
   * @param {*} node
   * @returns
   */
  delete(value, node) {
    if (!node) return node;
    
    // 去左结点找
    if (value < node.value) {
      node.leftChildNode = this.delete(value, node.leftChildNode);
      return node;
    }

    // 去右结点找
    if (value > node.value) {
      node.rightChildNode = this.delete(value, node.rightChildNode);
      return node;
    }

    // 找到了
    if (value === node.value) {
      // 如果只有左子节点,则让当前左子节点接替位置
      if (!node.rightChildNode) {
        return node.leftChildNode;
      }
      // 如果只有右子节点,则让当前右子节点接替位置
      else if (!node.leftChildNode) {
        return node.rightChildNode;
      } else {
        // 如果左右都有,当前结点值则为后继结点的值
        node.right = this.lift(node.rightChildNode, node);
        return node;
      }
    }
  }

  // 获取后继结点的值(比删除结点大的全部结点中最小的结点)
  lift(node, nodeToDelete) {
    if (node.leftChildNode) {
      node.leftChildeNode = lift(node.leftChildNode, nodeToDelete);
      return node;
    } else {
      node.rightChildNode = lift(node.rightChildNode, nodeToDelete);
    }
  }
}

let binaryTree = new Tree();
let treeNodeValue = [54, 25, 36, 47, 36, 88, 11, 86, 60];
treeNodeValue.forEach((item) => binaryTree.insert(item));

console.log(binaryTree.search(88, binaryTree.firstNode));
console.log(binaryTree.delete(88, binaryTree.firstNode));
console.log(binaryTree);

| 参考

数据结构与算法图解