JavaScript 二叉搜索树实现详解:从节点构造到增删查改

作者:小括号

时间:2026-10-23

浏览:182

本文详细解析 JavaScript 中二叉搜索树(BST)的实现原理。内容涵盖节点类与树类的构造、基于递归的插入算法、三种遍历方式(前序、中序、后序)的栈执行机制,以及查找和删除操作的逻辑判断。通过具体代码示例,深入理解递归在树结构操作中的应用及时间复杂度优化。

文章目录

二叉树与二叉搜索树基础

二叉树是一种基础的数据结构。

在二叉树中,每个节点最多只能拥有两个子节点。

二叉搜索树是二叉树的一种特殊形式。

二叉搜索树在普通二叉树的基础上增加了排序约束:插入值时,若新值小于当前节点,则尝试插入左子节点;若大于当前节点,则尝试插入右子节点。若目标子节点已存在,则继续按照上述规则向下比较,直至找到空位插入新节点。

二叉搜索树具备特定的性能特征。

得益于其有序结构,二叉搜索树在插入、删除和查找操作上的时间复杂度均为 O(h),其中 h 为树的高度。为了保持高效性能,树的高度应尽可能小,即左右子树应尽量保持平衡。

二叉搜索树的节点与树类构造

构建二叉搜索树需先定义节点结构。

根据二叉树特性,每个节点需包含左子节点引用、右子节点引用以及存储的值。因此,首先定义节点类如下:


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

随后构建二叉搜索树的主类。


class Tree{
 constructor(param = null) {
  if (param) {
   this.root = new Node(param);
  } else {
   this.root = null;
  }
 }
}

此处 this.root 属性指向当前树对象的根节点。

二叉搜索树的插入算法实现

接下来实现二叉搜索树的新增功能。

利用左子树节点值小于当前节点、右子树节点值大于当前节点的特性,可编写新增算法,具体实现如下:


insert(key) {
 if (this.root === null) {
  this.root = new Node(key);
 } else {
  this._insertNode(this.root, key);
 }
}
_insertNode(node, key) {
 if (key < node.key) {
  if (node.left === null) {
   node.left = new Node(key);{1}
  } else {
   this._insertNode(node.left, key);{2}
  }
 } else if (key > node.key) {
  if (node.right === null) {
   node.right = new Node(key);{3}
  } else {
   this._insertNode(node.right, key);{4}
  }
 }
}

上述代码首先比较新增 key 与当前节点 key 的大小。若较小,则递归遍历左子节点,直至找到为 null 的左子节点位置;若较大,则同理处理右子节点。代码中 {1}{2}{3}{4} 标记处之所以能直接修改 this.root 的值,是因为 JavaScript 中函数参数按值传递,但当传递非基本类型(如对象)时,传递的是内存地址的副本,因此对对象属性的修改会直接反映在原始对象上。

二叉搜索树的遍历机制与递归解析

二叉搜索树支持多种遍历方式。

主要包括先序遍历、中序遍历和后序遍历三种模式。


inOrderTraverse(callback) {
 this._inOrderTraverse(this.root, callback);
}
_inOrderTraverse(node, callback) {
 if (node) {
  this._inOrderTraverse(node.left, callback);
  callback(node.key);
  this._inOrderTraverse(node.right, callback);
 }
}

以上代码展示了中序遍历的实现逻辑。

理解递归机制至关重要。函数执行可抽象为栈数据结构,系统维护一个调用栈来存储执行环境。每次递归时,当前执行环境入栈并记录执行位置。

以示例代码为例,执行流程如下:从节点 11 开始,执行 {1} 入栈,进入节点 7 并执行 {1} 入栈,接着到节点 5 执行 {1} 入栈,再到节点 3 执行 {1} 入栈。此时发现节点 3 的左子节点为 null,开始出栈,弹出节点 3 的执行环境,执行 {2} 和 {3}。由于节点 3 的右子节点也为 null,{3} 的递归执行完毕。接着弹出节点 5,执行 {2} 和 {3};弹出节点 7,执行 {2} 和 {3}。当执行 {3} 时发现节点 7 有右子节点,则继续执行 {1} 进入节点 8,再执行 {1}。由于节点 8 无左子节点,{1} 执行完毕,接着执行 {2} 和 {3},以此类推完成遍历。

前序遍历与中序遍历的区别在于访问当前节点的时机,其执行顺序为 2 1 3(即先访问节点本身,再遍历左右子树)。

后序遍历同理,其执行顺序为 1 3 2(即先遍历左右子树,最后访问节点本身)。

可以看出,无论前序、中序还是后序遍历,核心逻辑都是先递归遍历左节点,左节点遍历完毕出栈后再遍历右节点。三者的唯一区别在于访问当前节点本身的时机不同。

二叉搜索树的查找与删除操作

二叉搜索树的查找操作相对简单。

根据左子节点值小于当前节点、右子节点值大于当前节点的原则,通过循环或递归判断即可实现查找。


search(value) {
 if (this.root) {
  if (value === this.root.key) {
   return true;
  } else {
   return this._searchNode(value, this.root);
  }
 }
 throw new Error('this.root 不存在');
}
_searchNode(value, node) {
 if (!node) {
  return false;
 }
 if (value === node.key) {
  return true;
 }
 if (value > node.key) {
  return this._searchNode(value, node.right);
 } else if (value < node.key) {
  return this._searchNode(value, node.left);
 }
}

二叉搜索树的删除操作较为复杂,需根据不同情况分别处理。

具体判断逻辑如下:

首先判断目标节点是否有左子树。若无左子树,则直接将右子树的根节点替换被删除节点。

若存在左子树,则将右子树中的最小节点替换被删除节点,以维持树的有序性。


remove(key) {
 this._removeNode(this.root, key);
}
_removeNode(node, value) {
 if (!node) {
  return null;
 }
 if (value > node.key) {
  node.right = this._removeNode(node.right, value);
 } else if (value < node.key) {
  node.left = this._removeNode(node.left, value);
 } else {
  // 如果没有左子树,那么将右子树根节点作为替换节点
  if (!node.left) {
   return node.right;
   // 如果存在左子树,那么取右子树最小节点作为替换节点
  } else if (node.left) {
   return this._minNode(node.right);
  }
 }
}

总结与反思。

通过实现二叉搜索树,重新深入理解了递归机制。此前对递归的理解仅停留在理论层面,此次实践加深了对递归执行过程的认识。

这让人联想到数学学习:理论公式易于记忆,但若仅看公式,掌握程度或许仅为满分中的 2 分。因为实际问题千变万化,只有将理论付诸实践,经过反复调试与验证,才能真正理解其内在奥秘。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章