这篇文章我们来讲怎么用 Typescript 来实现一个二叉搜索树(BST)
前言
先了解一下二叉搜索树的定义
二叉搜索树,又称二叉排序树,是一种基于二叉树结构的特殊数据结构,其核心特征是通过定义 “节点值的有序性”,实现高效的查找、插入和删除操作(理想情况下时间复杂度为 O(logn))。它既是二叉树的子类,也是 “有序查找” 场景的经典实现。
核心定义:节点值的有序规则
二叉搜索树的本质是通过对每个节点的左右子树施加严格的值大小约束,确保整棵树的 “中序遍历序列是严格递增的”。具体规则如下:
对于树中的任意一个节点,需同时满足以下两个条件:
- 左子树约束:该节点的所有左子树节点的值,都小于该节点的值;
- 右子树约束:该节点的所有右子树节点的值,都大于该节点的值。
注意:不同场景下可能存在 “允许等于” 的变体(如左子树节点值≤当前节点值),但标准二叉搜索树默认 “严格不等” ,以避免重复值导致的逻辑歧义(如删除时难以判断重复值的位置)。
结构基础:依赖二叉树的特性
二叉搜索树首先需满足二叉树的基本定义,即每个节点最多有两个子节点,分别称为 “左子节点” 和 “右子节点”,具体包括:
- 树由 “节点” 组成,每个节点包含三部分:左指针(Left Pointer) 、值(Value) 、右指针(Right Pointer) ;
- 有且仅有一个 “根节点(Root)”,是树的入口,无父节点;
- 除根节点外,每个节点有且仅有一个父节点;
- 没有父节点的节点称为 “叶子节点(Leaf)”(左、右指针均为
null)。
关键性质:由定义推导的核心特征
基于上述节点值规则,二叉搜索树天然具备以下可直接利用的性质,也是其高效操作的基础:
1. 中序遍历的有序性
2. 查找的单向性
3. 极值的可定位性
示例:合法与非法 BST 对比
| 类型 | 结构描述 | 是否为合法 BST | 原因分析 |
|---|---|---|---|
| 合法 BST | 根 5,左子树 3(左 1、右 4),右子树 7(左 6) | 是 | 所有节点满足 “左子树值<自身<右子树值”,中序遍历为1,3,4,5,6,7(递增) |
| 非法 BST | 根 5,左子树 7(左 6),右子树 3(右 4) | 否 | 根节点 5 的左子树存在 7(7>5),违反 “左子树值<当前节点值” 的规则 |
| 非法 BST | 根 5,左子树 3(右 6),右子树 8 | 否 | 节点 3 的右子树存在 6(6>5,而 5 是 3 的父节点),违反 “右子树值需小于根 5” 的隐含约束 |
与 “平衡二叉树” 的关系(延伸说明)
标准 BST 存在一个缺陷:若插入的节点值始终递增或递减(如依次插入 1,2,3,4,5),会退化为 “链表结构”(所有节点只有右子树),此时查找、插入的时间复杂度会降至 (O(n))(与链表无异)。
为解决此问题,衍生出 “平衡二叉树”(如 AVL 树、红黑树)—— 它们是满足 “平衡条件” 的二叉搜索树,即在 BST 定义的基础上,额外约束 “左右子树的高度差不超过 1”(或类似规则),确保树始终保持 “矮胖” 结构,避免退化。因此,平衡二叉树是 BST 的 “优化子类”,而非独立结构。
综上,二叉搜索树的核心是 “节点值的有序约束”,其所有特性和应用(如高效查找、有序遍历)均基于这一核心定义展开,是后续学习平衡树、字典(如 C++ map、Java TreeMap)等高级结构的基础。
代码开发
1. 首先我们需要先定义节点的类型
class TreeNode<T> {
value: T;
left: TreeNode<T> | null;
right: TreeNode<T> | null;
constructor(value: T) {
this.value = value;
this.left = null;
this.right = null;
}
}
和上面所描述一致,包含值,左右两个指针
接下来开发我们的二叉搜索树
class BinarySearchTree<T> {
private root: TreeNode<T> | null;
private compare: (a: T, b: T) => number;
constructor(compareFn?: (a: T, b: T) => number) {
this.root = null;
// 默认比较函数,适用于数字和字符串
this.compare = compareFn || ((a: T, b: T) => {
if (a < b) return -1;
if (a > b) return 1;
return 0;
});
}
root属性就是我们的二叉搜索树根节点,是整个树最上层的节点
compare是比较函数,当我们想要插入节点时,就需要通过比较函数来判断插入的节点应该放在左边还是右边
2. 插入节点
设置我们的第一个方法,插入节点方法
// 插入节点
insert(value: T): void {
const newNode = new TreeNode(value);
if (this.root === null) {
this.root = newNode;
} else {
this.insertNode(this.root, newNode);
}
}
当根节点为空时,直接给根节点赋值即可,如果不为空,就需要进行一些复杂的操作,执行逻辑如下:
将新节点与根节点进行比较
如果新节点比根节点小
2.1 判断左指针指向的节点是否为null,如果是,就插入,
2.2 如果不是,将根节点换成左指针节点,再次执行 1 步骤
- 如果新节点比根节点大或等于
2.1 判断右指针指向的节点是否为null,如果是,就插入,
2.2 如果不是,将根节点换成右指针节点,再次执行 1 步骤
如下图所示,我们现在要插入一个 数值为13的节点
第一步 13 和根节点 10 进行比较
发现比10大,然后我们进入下一层,和10的右指针指向的元素进行比较
发现比13 比17小,然后就13就需要和17的左指针指向的节点进行比较,但是呢,17左指针为null,所以就将17左指针直接指向13
private insertNode(node: TreeNode<T>, newNode: TreeNode<T>): void {
if (this.compare(newNode.value, node.value) < 0) {
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);
}
}
}
3. 查找节点
查找节点和插入节点有异曲同工之处,都需要进行递归调用
// 查找节点
search(value: T): boolean {
return this.searchNode(this.root, value);
}
private searchNode(node: TreeNode<T> | null, value: T): boolean {
if (node === null) {
return false;
}
const comparison = this.compare(value, node.value);
if (comparison < 0) {
return this.searchNode(node.left, value);
} else if (comparison > 0) {
return this.searchNode(node.right, value);
} else {
return true;
}
}
搜索和插入动图演示
4. 删除节点
删除节点有些复杂
-
通过递归的方式找到要删除的节点
-
接下来看该节点有几个子节点,分情况讨论
2.1 如果没有子节点,表示该节点是一个叶子节点,我们可以直接返回null,然后父节点的指针也就变成了null
2.2 如果只有一个子节点,那就判断到底是左子节点,还是右子节点,然后返回出去,此时父节点的指针也就指向了当前节点的子节点
2.3 这个情况也是最复杂的情况,
首先需要找到要删除节点的后继节点,也就是右子树中的最小节点。查找后继节点的过程是:先访问要删除节点的右子节点,然后不断往左子节点方向走,直到找到没有左子节点的节点。这个节点就是后继节点,它的值刚好大于被删除节点的值。
找到后继节点后,将其值复制到被删除节点的位置。然后删除后继节点本身。由于后继节点最多只有一个右子节点(不可能有左子节点,否则就不是最小节点了),所以删除后继节点可以按照情况一或情况二的方式处理。
我们以这张图为例,演示一下最复杂的情况
假如说我们要删除17
第一步:找到17
第二步:发现17 有两个子节点,然后我们转到17的右子树,在右子树里面找到元素最小的节点
第三步:我们可以在右子树里面一直向左子树进行遍历,找到最小的20
第四步:然后将17的数值替换成20
第五步:然后我们去遍历红色的20节点的右子树,去删除数值为20的元素,此时的20一定不是一个双子树节点
// 删除节点
remove(value: T): void {
this.root = this.removeNode(this.root, value);
}
private removeNode(node: TreeNode<T> | null, value: T): TreeNode<T> | null {
if (node === null) {
return null;
}
const comparison = this.compare(value, node.value);
if (comparison < 0) {
node.left = this.removeNode(node.left, value);
return node;
} else if (comparison > 0) {
node.right = this.removeNode(node.right, value);
return node;
} else {
// 情况1: 叶子节点
if (node.left === null && node.right === null) {
return null;
}
// 情况2: 只有一个子节点
if (node.left === null) {
return node.right;
} else if (node.right === null) {
return node.left;
}
// 情况3: 有两个子节点
// 找到右子树的最小节点
const minRight = this.findMinNode(node.right);
node.value = minRight.value;
node.right = this.removeNode(node.right, minRight.value);
return node;
}
}
private findMinNode(node: TreeNode<T>): TreeNode<T> {
if (node.left === null) {
return node;
}
return this.findMinNode(node.left);
}
5. 遍历
在二叉树的遍历中,中序、前序、后序遍历属于深度优先遍历(DFS) ,而层序遍历属于广度优先遍历(BFS) 。它们的核心区别在于访问节点的顺序不同,以下是具体解释:
前序遍历
顺序:根节点 → 左子树 → 右子树(先访问根,再递归遍历左右)
特点:第一个访问的节点一定是整棵树的根节点。
中序遍历
顺序:左子树 → 根节点 → 右子树(先递归遍历左子树,再访问根,最后递归遍历右子树)
特点:对于二叉搜索树(BST) ,中序遍历结果是严格递增的有序序列(这是 BST 的核心特性)。
后序遍历
顺序:左子树 → 右子树 → 根节点(最后访问根节点)
特点:最后一个访问的节点一定是整棵树的根节点。
层序遍历
顺序:从根节点开始,按 “层” 依次访问(同一层从左到右)
特点:需要借助队列实现,先访问的节点其子女也先被访问(类似 “按行打印”)。
以这样一颗二叉树为例
前序遍历:
10 → 7 → 4 → 8 → 17 → 13 → 22
中序遍历:
4 → 7 → 8 → 10 → 13 → 17 → 22
后续遍历:
4 → 8 → 7 → 13 → 22 → 17 → 10
层序遍历:
10 → 7 → 17 → 4 → 8 → 13 → 22
// 中序遍历
inOrderTraversal(callback: (value: T) => void): void {
this.inOrderTraversalNode(this.root, callback);
}
private inOrderTraversalNode(node: TreeNode<T> | null, callback: (value: T) => void): void {
if (node !== null) {
this.inOrderTraversalNode(node.left, callback);
callback(node.value);
this.inOrderTraversalNode(node.right, callback);
}
}
// 前序遍历
preOrderTraversal(callback: (value: T) => void): void {
this.preOrderTraversalNode(this.root, callback);
}
private preOrderTraversalNode(node: TreeNode<T> | null, callback: (value: T) => void): void {
if (node !== null) {
callback(node.value);
this.preOrderTraversalNode(node.left, callback);
this.preOrderTraversalNode(node.right, callback);
}
}
// 后序遍历
postOrderTraversal(callback: (value: T) => void): void {
this.postOrderTraversalNode(this.root, callback);
}
private postOrderTraversalNode(node: TreeNode<T> | null, callback: (value: T) => void): void {
if (node !== null) {
this.postOrderTraversalNode(node.left, callback);
this.postOrderTraversalNode(node.right, callback);
callback(node.value);
}
}
// 层序遍历
levelOrderTraversal(callback: (value: T) => void): void {
if (this.root === null) {
return;
}
const queue: TreeNode<T>[] = [this.root];
while (queue.length > 0) {
const node = queue.shift()!;
callback(node.value);
if (node.left !== null) {
queue.push(node.left);
}
if (node.right !== null) {
queue.push(node.right);
}
}
}
6. 获取最小值
// 获取最小值
findMin(): T | null {
if (this.root === null) {
return null;
}
let current = this.root;
while (current.left !== null) {
current = current.left;
}
return current.value;
}
7. 获取最大值
// 获取最大值
findMax(): T | null {
if (this.root === null) {
return null;
}
let current = this.root;
while (current.right !== null) {
current = current.right;
}
return current.value;
}
8. 检查树的高度
// 获取树的高度
getHeight(): number {
return this.getNodeHeight(this.root);
}
private getNodeHeight(node: TreeNode<T> | null): number {
if (node === null) {
return 0;
}
const leftHeight = this.getNodeHeight(node.left);
const rightHeight = this.getNodeHeight(node.right);
return Math.max(leftHeight, rightHeight) + 1;
}
9. 检查树是否为空
// 检查树是否为空
isEmpty(): boolean {
return this.root === null;
}
10. 清空树
// 清空树
clear(): void {
this.root = null;
}
完整源代码
class TreeNode<T> {
value: T;
left: TreeNode<T> | null;
right: TreeNode<T> | null;
constructor(value: T) {
this.value = value;
this.left = null;
this.right = null;
}
}
class BinarySearchTree<T> {
private root: TreeNode<T> | null;
private compare: (a: T, b: T) => number;
constructor(compareFn?: (a: T, b: T) => number) {
this.root = null;
// 默认比较函数,适用于数字和字符串
this.compare = compareFn || ((a: T, b: T) => {
if (a < b) return -1;
if (a > b) return 1;
return 0;
});
}
// 插入节点
insert(value: T): void {
const newNode = new TreeNode(value);
if (this.root === null) {
this.root = newNode;
} else {
this.insertNode(this.root, newNode);
}
}
private insertNode(node: TreeNode<T>, newNode: TreeNode<T>): void {
if (this.compare(newNode.value, node.value) < 0) {
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(value: T): boolean {
return this.searchNode(this.root, value);
}
private searchNode(node: TreeNode<T> | null, value: T): boolean {
if (node === null) {
return false;
}
const comparison = this.compare(value, node.value);
if (comparison < 0) {
return this.searchNode(node.left, value);
} else if (comparison > 0) {
return this.searchNode(node.right, value);
} else {
return true;
}
}
// 删除节点
remove(value: T): void {
this.root = this.removeNode(this.root, value);
}
private removeNode(node: TreeNode<T> | null, value: T): TreeNode<T> | null {
if (node === null) {
return null;
}
const comparison = this.compare(value, node.value);
if (comparison < 0) {
node.left = this.removeNode(node.left, value);
return node;
} else if (comparison > 0) {
node.right = this.removeNode(node.right, value);
return node;
} else {
// 情况1: 叶子节点
if (node.left === null && node.right === null) {
return null;
}
// 情况2: 只有一个子节点
if (node.left === null) {
return node.right;
} else if (node.right === null) {
return node.left;
}
// 情况3: 有两个子节点
// 找到右子树的最小节点
const minRight = this.findMinNode(node.right);
node.value = minRight.value;
node.right = this.removeNode(node.right, minRight.value);
return node;
}
}
private findMinNode(node: TreeNode<T>): TreeNode<T> {
if (node.left === null) {
return node;
}
return this.findMinNode(node.left);
}
// 中序遍历
inOrderTraversal(callback: (value: T) => void): void {
this.inOrderTraversalNode(this.root, callback);
}
private inOrderTraversalNode(node: TreeNode<T> | null, callback: (value: T) => void): void {
if (node !== null) {
this.inOrderTraversalNode(node.left, callback);
callback(node.value);
this.inOrderTraversalNode(node.right, callback);
}
}
// 前序遍历
preOrderTraversal(callback: (value: T) => void): void {
this.preOrderTraversalNode(this.root, callback);
}
private preOrderTraversalNode(node: TreeNode<T> | null, callback: (value: T) => void): void {
if (node !== null) {
callback(node.value);
this.preOrderTraversalNode(node.left, callback);
this.preOrderTraversalNode(node.right, callback);
}
}
// 后序遍历
postOrderTraversal(callback: (value: T) => void): void {
this.postOrderTraversalNode(this.root, callback);
}
private postOrderTraversalNode(node: TreeNode<T> | null, callback: (value: T) => void): void {
if (node !== null) {
this.postOrderTraversalNode(node.left, callback);
this.postOrderTraversalNode(node.right, callback);
callback(node.value);
}
}
// 层序遍历
levelOrderTraversal(callback: (value: T) => void): void {
if (this.root === null) {
return;
}
const queue: TreeNode<T>[] = [this.root];
while (queue.length > 0) {
const node = queue.shift()!;
callback(node.value);
if (node.left !== null) {
queue.push(node.left);
}
if (node.right !== null) {
queue.push(node.right);
}
}
}
// 获取最小值
findMin(): T | null {
if (this.root === null) {
return null;
}
let current = this.root;
while (current.left !== null) {
current = current.left;
}
return current.value;
}
// 获取最大值
findMax(): T | null {
if (this.root === null) {
return null;
}
let current = this.root;
while (current.right !== null) {
current = current.right;
}
return current.value;
}
// 获取树的高度
getHeight(): number {
return this.getNodeHeight(this.root);
}
private getNodeHeight(node: TreeNode<T> | null): number {
if (node === null) {
return 0;
}
const leftHeight = this.getNodeHeight(node.left);
const rightHeight = this.getNodeHeight(node.right);
return Math.max(leftHeight, rightHeight) + 1;
}
// 检查树是否为空
isEmpty(): boolean {
return this.root === null;
}
// 清空树
clear(): void {
this.root = null;
}
}