中文

探索二叉搜索树 (BST) 的基础知识,学习如何使用 JavaScript 高效地实现它们。本指南为全球开发者提供了 BST 结构、操作及实用范例。

二叉搜索树:一份全面的 JavaScript 实现指南

二叉搜索树(BST)是计算机科学中的一种基础数据结构,广泛用于高效地搜索、排序和检索数据。其层次结构使得许多操作的时间复杂度能够达到对数级别,使其成为管理大型数据集的强大工具。本指南全面概述了 BST,并演示了如何用 JavaScript 实现它,以满足全球开发者的需求。

理解二叉搜索树

什么是二叉搜索树?

二叉搜索树是一种基于树的数据结构,其中每个节点最多有两个子节点,分别称为左子节点和右子节点。BST 的关键属性是,对于任何给定节点:

此属性确保 BST 中的元素始终有序,从而实现高效的搜索和检索。

核心概念

在 JavaScript 中实现二叉搜索树

定义节点类

首先,我们定义一个 `Node` 类来表示 BST 中的每个节点。每个节点将包含一个用于存储数据的 `key`,以及指向其子节点的 `left` 和 `right` 指针。


class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

定义二叉搜索树类

接下来,我们定义 `BinarySearchTree` 类。该类将包含根节点以及用于插入、搜索、删除和遍历树的方法。


class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  // Methods will be added here
}

插入

`insert` 方法将带有给定键的新节点添加到 BST 中。插入过程通过将新节点放置在相对于现有节点的适当位置来维护 BST 属性。


insert(key) {
  const newNode = new Node(key);

  if (this.root === null) {
    this.root = newNode;
  } else {
    this.insertNode(this.root, newNode);
  }
}

insertNode(node, newNode) {
  if (newNode.key < node.key) {
    if (node.left === null) {
      node.left = newNode;
    } else {
      this.insertNode(node.left, newNode);
    }
  } else {
    if (node.right === null) {
      node.right = newNode;
    } else {
      this.insertNode(node.right, newNode);
    }
  }
}

示例: 向 BST 中插入值


const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);

搜索

`search` 方法检查 BST 中是否存在具有给定键的节点。它遍历树,将键与当前节点的键进行比较,并相应地移动到左子树或右子树。


search(key) {
  return this.searchNode(this.root, key);
}

searchNode(node, key) {
  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);
  } else {
    return true;
  }
}

示例: 在 BST 中搜索一个值


console.log(bst.search(9));  // Output: true
console.log(bst.search(2));  // Output: false

删除

`remove` 方法从 BST 中删除具有给定键的节点。这是最复杂的操作,因为它需要在删除节点的同时保持 BST 的属性。需要考虑三种情况:


remove(key) {
  this.root = this.removeNode(this.root, key);
}

removeNode(node, key) {
  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 {
    // key is equal to node.key

    // case 1 - a leaf node
    if (node.left === null && node.right === null) {
      node = null;
      return node;
    }

    // case 2 - node has only 1 child
    if (node.left === null) {
      node = node.right;
      return node;
    } else if (node.right === null) {
      node = node.left;
      return node;
    }

    // case 3 - node has 2 children
    const aux = this.findMinNode(node.right);
    node.key = aux.key;
    node.right = this.removeNode(node.right, aux.key);
    return node;
  }
}

findMinNode(node) {
  let current = node;
  while (current != null && current.left != null) {
    current = current.left;
  }
  return current;
}

示例: 从 BST 中移除一个值


bst.remove(7);
console.log(bst.search(7)); // Output: false

树的遍历

树的遍历涉及以特定顺序访问树中的每个节点。有几种常见的遍历方法:


inOrderTraverse(callback) {
  this.inOrderTraverseNode(this.root, callback);
}

inOrderTraverseNode(node, callback) {
  if (node !== null) {
    this.inOrderTraverseNode(node.left, callback);
    callback(node.key);
    this.inOrderTraverseNode(node.right, callback);
  }
}

preOrderTraverse(callback) {
  this.preOrderTraverseNode(this.root, callback);
}

preOrderTraverseNode(node, callback) {
  if (node !== null) {
    callback(node.key);
    this.preOrderTraverseNode(node.left, callback);
    this.preOrderTraverseNode(node.right, callback);
  }
}

postOrderTraverse(callback) {
  this.postOrderTraverseNode(this.root, callback);
}

postOrderTraverseNode(node, callback) {
  if (node !== null) {
    this.postOrderTraverseNode(node.left, callback);
    this.postOrderTraverseNode(node.right, callback);
    callback(node.key);
  }
}

示例: 遍历 BST


const printNode = (value) => console.log(value);

bst.inOrderTraverse(printNode);   // Output: 3 5 8 9 10 11 12 13 14 15 18 20 25
bst.preOrderTraverse(printNode);  // Output: 11 5 3 9 8 10 15 13 12 14 20 18 25
bst.postOrderTraverse(printNode); // Output: 3 8 10 9 12 14 13 18 25 20 15 11

最小值和最大值

由于其有序性,在 BST 中查找最小值和最大值非常简单。


min() {
  return this.minNode(this.root);
}

minNode(node) {
  let current = node;
  while (current !== null && current.left !== null) {
    current = current.left;
  }
  return current;
}

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

maxNode(node) {
  let current = node;
  while (current !== null && current.right !== null) {
    current = current.right;
  }
  return current;
}

示例: 查找最小值和最大值


console.log(bst.min().key); // Output: 3
console.log(bst.max().key); // Output: 25

二叉搜索树的实际应用

二叉搜索树用于多种应用,包括:

性能考量

BST 的性能取决于其结构。在最佳情况下,平衡的 BST 允许插入、搜索和删除操作的时间复杂度为对数级别。然而,在最坏情况下(例如,一个倾斜的树),时间复杂度可能降至线性时间。

平衡树与非平衡树

平衡 BST 是指其中任何节点的左、右子树的高度差最多为一的树。自平衡算法,如 AVL 树和红黑树,确保树保持平衡,从而提供一致的性能。不同地区可能需要根据服务器负载进行不同级别的优化;平衡有助于在全球高使用率下保持性能。

时间复杂度

高级 BST 概念

自平衡树

自平衡树是能自动调整其结构以保持平衡的 BST。这确保树的高度保持在对数级别,为所有操作提供一致的性能。常见的自平衡树包括 AVL 树和红黑树。

AVL 树

AVL 树通过确保任何节点的左、右子树之间的高度差最多为一来维持平衡。当这种平衡被破坏时,会执行旋转操作来恢复平衡。

红黑树

红黑树使用颜色属性(红色或黑色)来维持平衡。它们比 AVL 树更复杂,但在某些情况下性能更好。

JavaScript 代码示例:完整的二叉搜索树实现


class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  insert(key) {
    const newNode = new Node(key);

    if (this.root === null) {
      this.root = newNode;
    } else {
      this.insertNode(this.root, newNode);
    }
  }

  insertNode(node, newNode) {
    if (newNode.key < node.key) {
      if (node.left === null) {
        node.left = newNode;
      } else {
        this.insertNode(node.left, newNode);
      }
    } else {
      if (node.right === null) {
        node.right = newNode;
      } else {
        this.insertNode(node.right, newNode);
      }
    }
  }

  search(key) {
    return this.searchNode(this.root, key);
  }

  searchNode(node, key) {
    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);
    } else {
      return true;
    }
  }

  remove(key) {
    this.root = this.removeNode(this.root, key);
  }

  removeNode(node, key) {
    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 {
      // key is equal to node.key

      // case 1 - a leaf node
      if (node.left === null && node.right === null) {
        node = null;
        return node;
      }

      // case 2 - node has only 1 child
      if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
        return node;
      }

      // case 3 - node has 2 children
      const aux = this.findMinNode(node.right);
      node.key = aux.key;
      node.right = this.removeNode(node.right, aux.key);
      return node;
    }
  }

  findMinNode(node) {
    let current = node;
    while (current != null && current.left != null) {
      current = current.left;
    }
    return current;
  }

  min() {
    return this.minNode(this.root);
  }

  minNode(node) {
    let current = node;
    while (current !== null && current.left !== null) {
      current = current.left;
    }
    return current;
  }

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

  maxNode(node) {
    let current = node;
    while (current !== null && current.right !== null) {
      current = current.right;
    }
    return current;
  }

  inOrderTraverse(callback) {
    this.inOrderTraverseNode(this.root, callback);
  }

  inOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.inOrderTraverseNode(node.left, callback);
      callback(node.key);
      this.inOrderTraverseNode(node.right, callback);
    }
  }

  preOrderTraverse(callback) {
    this.preOrderTraverseNode(this.root, callback);
  }

  preOrderTraverseNode(node, callback) {
    if (node !== null) {
      callback(node.key);
      this.preOrderTraverseNode(node.left, callback);
      this.preOrderTraverseNode(node.right, callback);
    }
  }

  postOrderTraverse(callback) {
    this.postOrderTraverseNode(this.root, callback);
  }

  postOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.postOrderTraverseNode(node.left, callback);
      this.postOrderTraverseNode(node.right, callback);
      callback(node.key);
    }
  }
}

// Example Usage
const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);

const printNode = (value) => console.log(value);

console.log("In-order traversal:");
bst.inOrderTraverse(printNode);

console.log("Pre-order traversal:");
bst.preOrderTraverse(printNode);

console.log("Post-order traversal:");
bst.postOrderTraverse(printNode);

console.log("Minimum value:", bst.min().key);
console.log("Maximum value:", bst.max().key);

console.log("Search for 9:", bst.search(9));
console.log("Search for 2:", bst.search(2));

bst.remove(7);
console.log("Search for 7 after removal:", bst.search(7));

结论

二叉搜索树是一种功能强大且用途广泛的数据结构,具有众多应用。本指南全面概述了 BST,涵盖了其结构、操作以及在 JavaScript 中的实现。通过理解本指南中讨论的原理和技术,全球开发者可以有效地利用 BST 来解决软件开发中的各种问题。从管理全球数据库到优化搜索算法,BST 的知识对任何程序员来说都是一笔宝贵的财富。

当您在计算机科学的旅程中继续前行时,探索像自平衡树及其各种实现这样的高级概念将进一步增强您的理解和能力。继续练习和试验不同的场景,以掌握有效使用二叉搜索树的艺术。