树-BST基本实现
之前的数组, 栈, 链表, 队列等都是顺序数据结构, 这里来介绍一个非顺序数据结构, 树. 树在处理有层级相关的数据时非常有用, 还有在存储数据如数据库查询实现等场景也是高频使用.
作为一种分层数据的抽象模型, 在现实中最常见的例子是族谱, 公司组织架构图等.
我个人觉得树, 图等非顺序的数据结构是不太好直观理解的, 它的实现都需要用到函数的递归. 就关于函数递归调用栈搞不清楚的话, 那也就很难理解代码了.
树的基本内容包括:
- 树相关的术语
- 创建二叉搜索树
- 树的三种遍历
- 添加和移除节点
- AVL平衡树
树的核心术语
- 节点: 树的每个元素都称为 "节点". 节点的祖先包括父节点, 祖父节点等; 节点的后代包括子节点, 孙节点等
- 根节点: 一颗树顶部最上层有个唯一的根节点元素, 称为根节点
- 内/外部节点: 至少有一个子节点的称为 "内部节点", 否则称 "外部节点 或 叶节点"
- 边: 节点和节点之间的 "连线" 成为边, 类似链表中的指针
- 子树: 节点和其子节点的集合, 构成一颗子树
- 节点深度: 节点深度指其祖先节点的数量
- 树高度: 所有节点深度的最大值
搜索二叉树
二叉树中的节点最多只能有两个子节点: 左子节点 和 右子节点.
这种只有2个路径的结构可以让我们写出更高效的数据插入, 查询, 删除节点的算法. 比如二分法, 关系型数据库的查询设计等, 就使用非常高频.
二叉搜索树 (BST) 是二叉树的一种, 强制要求:
- 比父节点小的值, 存左侧子节点
- 比父节点大的值, 存右侧子节点
// 节点类
class Node {
constructor(key) {
this.key = key // 节点值
this.left = null // 左节点指针
this.right = null // 右节点指针
}
}
// 二叉搜索树类
class BSTtree {
constructor() {
this.root = null // 根节点对象
}
}
主要实现的方法是有:
- insert(key): 向树中插入一个新的键
- search(key): 搜索 key, 如果存在返回 true, 否则返回 false
- inOrderTraverse(): 中序遍历所有节点
- preOrderTraverse(): 先序遍历所有节点
- postOrderTraverse(): 后序遍历所有节点
- min() : 返回树中最小值
- max(): 返回树中最大值
- remove(key): 从树中移除某个键
BST - 插入键
这里我们先来实现一个辅助的方法 insertNode(node, key)
.
它的作用是帮我们找到新节点应该插入到哪个最合适的层级位置, 因此它是一个 递归函数
.
**当插入的 key 小于 node.key 时: **
- 如果 node.left 是叶子节点时, 则直接插入
- 否则就递归往左下层子树查找
当插入的 key 大于 node 时:
- 如果 node.right 是叶子节点时, 则直接插入
- 否则就递归往右下层子树查找
// 从某节点开始, 插入新的 key
insertNode(node, key) {
if (key < node.key) {
// 左子树处理
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)
}
}
}
这样插入的逻辑就是变成从根节点开始进行判断:
- 根节点无值, 则新增的 key 节点就是根节点
- 根节点有值, 则将根节点开始进行 insertNode(root, key) 找到合适位置插入
// 插入一个 key
insert(key) {
if this.root == null {
// 空树的话那就是根节点
this.root = new Node(key)
} else {
// 有值则找到合适位置插入
this.insertNode(this.root, key)
}
}
二叉树遍历
二叉树的遍历方式基本就3种, 先序遍历, 后序遍历, 中序遍历.
先序步骤: 根 => 左 => 右
应用: 复制树, 序列化树, 前缀表达式解析.
中序步骤:左 => 根 => 右
应用: 排序, 中缀表达式解析 (a + b * c)
后序步骤:左 => 右 => 根
应用: 释放内存, 后缀表达式解析 (栈), 修改树.
1
/ \
2 3
/ / \
4 5 6
先序, 遍历的结果是: 1, 2, 4, 3, 5, 6
后序, 遍历的结果是: 4, 2, 5, 6, 3, 1
中序, 遍历的结果是: 4, 2, 1, 5, 3, 6
先序遍历
即按照 根 -> 左 -> 右
的顺序, 先会访问节点本身(父节点), 然后再访问左侧节点, 最后右侧节点.
这里对于节点访问处理可以传递一个通用的 回调函数 callback
, 同时用一个辅助方法 preOrderTraverseNode(node, callback)
来接受一个节点和回调函数.
// 辅助方法: 节点遍历
preOrderTraverseNode(node, callback) {
// 基本情况
if (node == null) return
// 递归情况: 先访问自身
callback(node.key)
// 再访问左树, 再是右树
this.preOrderTraverseNode(node.left, callback)
this.preOrderTraverseNode(node.right, callback)
}
// 先序遍历: 从根节点开始输入
preOrderTraverse(callback) {
this.preOrderTraver(this.root, callback)
}
中序遍历
即按照 左 -> 根 -> 右
的顺序, 先访问子节点(叶子), 然后再访问根节点, 最后右侧节点.
同样节点访问处理可以传递一个通用的 回调函数 callback
, 同时用一个辅助方法 inOrderTraverseNode(node, callback)
来接受一个节点和回调函数.
// 辅助方法: 节点遍历
inOrderTraverseNode(node, callback) {
// 基本情况
if (node == null) return
// 递归情况: 先左侧到叶子节点
this.inOrderTraverse(node.left, callback)
// 访问节点
callback(node.key)
// 再是右侧节点
this.inOrderTraverse(node.right, callback)
}
// 中序遍历
inOrderTraverse(callback) {
this.inOrderTraverse(this.root, callback)
}
后序遍历
即按照 左 -> 右 -> 根
的顺序, 先访问子节点(叶子), 然后再访问根节点, 最后右侧节点.
同样节点访问处理可以传递一个通用的 回调函数 callback
, 同时用一个辅助方法 postOrderTraverseNode(node, callback)
来接受一个节点和回调函数.
// 辅助方法: 节点遍历
postOrderTraverseNode(node, callback) {
// 基本情况
if (node == null) return
// 递归情况:
this.postOrderTraverse(node.left, callback)
this.postOrderTraverse(node.right, callback)
// 访问节点
callback(node.key)
}
// 后序遍历
postOrderTraverse(callback) {
this.postOrderTraverse(this.root, callback)
}
最大最小值
// BST 树的最小值
min() {
return this.minNode(this.root)
}
minNode(node) {
let current = node
// 因为是 BST 树, 小的值一定在左侧, 从树一层层往下到底就行
while (current != null && current.left != null) {
current = current.left
}
return current
}
// BST 树的最大值
max() {
return this.maxNode(this.root)
}
maxNode(node) {
let current = node
while (current != null && current.right != null) {
current = current.right
}
return current.key
}
树中查询值
因为是 BST 树, 左侧深度递归查找就是越来越小, 右侧深度递归查找的值就越来越大, 否则就基本条件, 没有找到喽.
// 查询值
searchNode(node, key) {
// 基本情况, 没有找到就返回 false
if (node == null) return false
// 若 key 小于当前节点, 则继续左侧深度递归搜索
if (key < node.key) return this.searchNode(node.left, key)
// 若 key 大于当前节点, 则继续右侧深度递归搜索
else if (key > node.key) return this.searchNode(node.right, key)
// 找到就返回
else return false
}
树中移除值
这个算是非常复杂的逻辑了, 既要分析不同场景, 不同场景下又要进行递归, 就很难搞.
首先要做的就是要通过对子树分析, 来确定将被删节点的位置,
然后根据将要被删的节点位置, 分情况进行讨论处理.
* 如果不为 null,我们需要在树中找到要移除的键
* 如果要找的键比当前节点的值小, 就沿着树的左边找到下一个节点
* 如果要找的键比当前节点的值大, 就那么就沿着树的右边找到下一个节点
也就是说我们要分析它的子树。如果我们找到了要找的键(键和 node.key 相等),就需要处理三种不同的情况:
- 移除一个叶节点 (无腿)
- 移除一个有左侧 或者 右侧子节点的节点 (一条腿)
- 移除有2个子节点的节点 (2条腿)
// 移除BST树中的节点
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 的位置, 即 key == node.key 时候, 分3中情况讨论
// 情况-01: 移除一个叶节点
if (node.left == null && node.right == null) {
node = null
// 通过返回 null 将父节点的指针指向 null
return node
}
// 情况-02: 移除一个有左或右节点的节点(1条腿)
// 跳过子节点, 直接将父节点指向其子节点
if (node.left == null) {
node = node.right
return node
} else if (node.right == null) {
node = node.left
return node
}
// 情况03 移除有两个子节点的节点 (2条腿)
// a. 找到它右子树中最小节点, 下下层元素 grandson
// b. 用 grandson 去替换掉被删节点的值 (改了键, 则移除了)
// c. 继续将右侧子树的最小节点移除 (删掉重复键节点)
// d. 向父节点返回更新的引用
const grandson = this.minNode(node.right)
node.key = grandson.key
node.right = this.removeNode(node.right, grandson.key)
return node
}
}
测试
// 测试
const tree = new BSTtree()
tree.insert(11)
tree.insert(7);
tree.insert(15);
tree.insert(5);
tree.insert(3);
tree.insert(9);
tree.insert(8);
tree.insert(10);
tree.insert(13);
tree.insert(12);
tree.insert(14);
tree.insert(20);
tree.insert(18);
tree.insert(25);
tree.insert(6);
// 中序遍历
function callback(value) {
console.log('callback-log: ', value);
}
// 中序遍历
console.log('中序遍历:');
tree.inOrderTraverse(callback)
console.log();
// 先序遍历
console.log('先序遍历:');
tree.preOrderTraverse(callback)
console.log();
// 后序遍历
console.log('后序遍历:');
tree.postOrderTraverse(callback)
// 树的最小值
console.log();
console.log('树的最小值是: ', tree.min());
// 树的最大值
console.log();
console.log('树的最大值是: ', tree.max());
// 查找值
console.log();
console.log(tree.search(1));
console.log(tree.search(8));
// 删除值
console.log('删除值 25', tree.remove(25));
tree.inOrderTraverse(callback)
console.log('删除值 15', tree.remove(15));
tree.inOrderTraverse(callback)
输出:
PS F:\algorithms> node .\bst_tree.js
中序遍历:
callback-log: 3
callback-log: 5
callback-log: 6
callback-log: 7
callback-log: 8
callback-log: 9
callback-log: 10
callback-log: 11
callback-log: 12
callback-log: 13
callback-log: 14
callback-log: 15
callback-log: 18
callback-log: 20
callback-log: 25
先序遍历:
callback-log: 11
callback-log: 7
callback-log: 5
callback-log: 3
callback-log: 6
callback-log: 9
callback-log: 8
callback-log: 10
callback-log: 15
callback-log: 13
callback-log: 12
callback-log: 14
callback-log: 20
callback-log: 18
callback-log: 25
后序遍历:
callback-log: 3
callback-log: 6
callback-log: 5
callback-log: 8
callback-log: 10
callback-log: 9
callback-log: 7
callback-log: 12
callback-log: 14
callback-log: 13
callback-log: 18
callback-log: 25
callback-log: 20
callback-log: 15
callback-log: 11
树的最小值是: Node { key: 3, left: null, right: null }
树的最大值是: 25
false
true
删除值 25 undefined
callback-log: 3
callback-log: 5
callback-log: 6
callback-log: 7
callback-log: 8
callback-log: 9
callback-log: 10
callback-log: 11
callback-log: 12
callback-log: 13
callback-log: 14
callback-log: 15
callback-log: 18
callback-log: 20
删除值 15
callback-log: 3
callback-log: 5
callback-log: 6
callback-log: 7
callback-log: 8
callback-log: 9
callback-log: 10
callback-log: 11
callback-log: 12
callback-log: 13
callback-log: 14
callback-log: 18
callback-log: 20
PS F:\algorithms>
至此, 关于树, 二叉搜索树的基本实现就到这里啦.