树是一种非顺序的数据结构,适合存储需要快速查找的数据。
文档包含以下内容:
- 树的相关概念
- 二叉树
- 树的遍历
- 树的节点添加、移除
- AVL树
1. 树的相关概念
一个数结构包含一系列父子关系的节点。每个节点都有一个父节点一级0个或多个子节点。
- 根节点:位于树顶部的节点;
- 内部节点:至少有一个子节点的节点;
- 外部节点或叶节点:没有子节点的节点;
- 子树:有节点和它的后代构成;
- 节点的深度:取决于它的祖先节点的数量;
- 树的高度:取决于所有节点深度的最大值。上图中输的高度为3。
2. 二叉树
二叉树:只有2个子节点的树。
二叉搜索树(BST) 是二叉树的一种,但是只能在左侧子节点存储(比父节点)小的值,在右侧子节点存储(比父节点)大的值。
2.1 创建BST类
- Node 类表示二叉树中的每个节点
class Node {
constructor(key){
this.key = key;// 节点值
this.left = null;// 左侧子节点引用
this.right = null; // 右侧子节点引用
}
}
复制代码
- 二叉搜索树组织方式
通过指针你表示节点之间的关系;
键是树相关术语中对节点的称呼。
- BST类基本结构
const Compare = {
LESS_THAN: -1,
BIGGER_THAN: 1,
EQUALS: 0,
};
function defaultCompare(a, b) {
if (a === b) {
return Compare.EQUALS;
}
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
class BinarySearchTree {
constructor(compareFn = defaultCompare) {
this.compareFn = compareFn; // 比较节点值
this.root = null; // Node类型的根节点
}
}
复制代码
2.2 向二叉搜索树中插入一个键
/**
* 向二叉搜索树中插入一个键
* @param {*} key 插入的值
*/
insert(key) {
if (this.root == null) {
// 树为空时;
this.root = new Node(key);
} else {
// 树不为空时
this.insertNode(this.root, key);
}
}
insertNode(node, key) {
// 如果新节点的键小于当前节点的键,检查当前节点的左侧子节点
if (this.compareFn(key, node?.key) === Compare.LESS_THAN) {
if (node.left == null) {
node.left = new Node(key);
} else {
// 如果有左侧子节点
this.insertNode(node.left, key);
}
} else {
// 如果节点的键>当前节点的键,同时当前节点没有右侧子节点,那么插入新的节点。
if (node.right == null) {
node.right = new Node(key);
} else {
// 如果有右侧子节点
this.insertNode(node.right, key);
}
}
}
复制代码
执行以下代码,生成二叉树;
let bst = new BinarySearchTree();
bst.insert(11);
bst.insert(15);
bst.insert(7);
bst.insert(5);
bst.insert(9);
bst.insert(3);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);
复制代码
如下图:
再次插入6的结果:
3. 树的遍历
遍历树是指访问数的每个节点并对他们进行某种操作的过程。
遍历树的三种方式:中序、先序、后序。
3.1 中序遍历
定义:从最小到最大的顺序访问所有节点。
应用:对树进行排序操作。
/**
* 中序遍历
* @param {Function} callback 对遍历到的每个节点进行操作
*/
inOrderTraverse(callback) {
inOrderTraverseNode(this.root, callback);
function inOrderTraverseNode(node, callback) {
if (node != null) {
// 停止递归的条件
// 访问左侧子节点
inOrderTraverseNode(node.left, callback);
callback(node.key);
// 访问右侧子节点
inOrderTraverseNode(node.right, callback);
}
}
}
复制代码
访问路径:
bst.inOrderTraverse((key) => {
console.log(key);
});
复制代码
3.2 先序遍历
定义:先于后代节点的顺序访问每个节点。
应用:打印一个结构化文档。
实现方法:
/**
* 先序遍历
*/
preOrderTraverse(callback) {
preOrderTraverseNode(this.root, callback);
function preOrderTraverseNode(node, callback) {
if (node != null) {
callback(node.key); // 先访问节点本身
preOrderTraverseNode(node.left, callback);
preOrderTraverseNode(node.right, callback);
}
}
}
复制代码
访问:
bst.preOrderTraverse((key) => {
console.log(key);
});
复制代码
路径:
先序遍历和中序遍历区别:先序遍历会先访问节点本身,在访问它的左侧子节点,最后访问右测子节点。
3.3 后序遍历
定义:先访问节点的后代节点,在访问节点本身。
应用:计算目录及其子目录中所有文件作战空间的大小。
实现方法:
/**
* 后序遍历
*/
postOrderTraverse(callback) {
postOrderTraverseNode(this.root, callback);
function postOrderTraverseNode(node, callback) {
if (node != null) {
postOrderTraverseNode(node.left, callback);
postOrderTraverseNode(node.right, callback);
callback(node.key); // 最后访问节点本身
}
}
}
复制代码
访问:
bst.postOrderTraverse((key) => {
console.log(key);
});
复制代码
访问路径图:
4. 搜索树中的值
4.1 最大值和最小值
如下图中的树:
有上图中可发现:最后一层最左侧的节点是树中的最小键。最右侧的节点的树中的最大键。
树的最小键 实现方法:
/**
* 树的最小键(值)
*/
min() {
return this.minNode(this.root);
}
minNode(node) {
let current =node;
while (current!=null&¤t.left!=null) {
current = current.left;
}
return current;
}
复制代码
执行方法:
console.log(bst.min()); // Node { key: 3, left: undefined, right: undefined }
复制代码
树的最大键
实现方法:
max() {
return this.maxNode(this.root);
}
maxNode(node) {
let current =node;
while (current!=null&¤t.right!=null) {
current = current.right;
}
return current;
}
复制代码
执行方法:
console.log(bst.max()); // Node { key: 25, left: undefined, right: undefined }
复制代码
结论:寻找最小值,总是沿着树的左边;寻找最大值,总是沿着树的右边。
4.2 搜索特定的值
- 在bst中的搜索 实现方法:
search(key) {
return this.searchNode(this.root, key);
}
searchNode(node, key) {
if (node == null) {
return false;
}
if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
return this.searchNode(node.left, key);
} else if (this.compareFn(key, node.key) === Compare.BIGGER_THAN) {
return this.searchNode(node.right, key);
} else {
return true;
}
}
复制代码
4.3 移除一个节点
实现方法:
remove(key){
this.root = this.removeNode(this.root, key);
}
removeNode(node, key) {
// root为空
if (node == null) {
return null;
}
// 移除的key小于node.key
if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
// 沿着树的左边继续向下找
node.left = this.removeNode(node.left, key);
return node;
}else if (this.compareFn(key, node.key) === Compare.BIGGER_THAN) {
// 移除的key > node.key
node.right = this.removeNode(node.right, key);
return node;
}else {
// key = node.key
// (1)移除一个叶节点
if (node.left== null&& node.right== null) {
node =null;
return node;
}
// (2) 移除有一个左侧或右侧子节点的节点
if (node.left== null) {
node = node.right;
return node;
}else if(node.right== null) {
node = node.left;
return node;
}
// (3) 移除有两个子节点的节点
const aux = this.minNode(node.right);
node.key = aux.key;
node.right = this.removeNode(node.right, key);
return node;
}
}
复制代码