树-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>
至此, 关于树, 二叉搜索树的基本实现就到这里啦.
树-BST基本实现的更多相关文章
- PAT Advanced 1064 Complete Binary Search Tree (30) [⼆叉查找树BST]
题目 A Binary Search Tree (BST) is recursively defined as a binary tree which has the following proper ...
- PAT Advanced 1043 Is It a Binary Search Tree (25) [⼆叉查找树BST]
题目 A Binary Search Tree (BST) is recursively defined as a binary tree which has the following proper ...
- HDU-1251-统计难题(Trie树)(BST)(AVL)
字典树解法(Trie树) Accepted 1251 156MS 45400K 949 B C++ #include"iostream" #include"cstdlib ...
- PAT Advanced 1099 Build A Binary Search Tree (30) [⼆叉查找树BST]
题目 A Binary Search Tree (BST) is recursively defined as a binary tree which has the following proper ...
- Python 数据结构 树
什么是树 数是一种抽象的数据类型(ADT)或是作这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合,它是由n(n>1)的有限个节点和节点之间的边组成的一个有层次关系的集合. 树的组成 ...
- 面试题22:有序数组生成不同结构BST
对于一个含有n个数的有序数组1~N,能够产生多少种不同结果的二叉搜素树BST? 如何生成这些不同结构的BST? 有序数组如何生成平衡二叉搜索树? class Solution { public: in ...
- 为什么选择b+树作为存储引擎索引结构
为什么选择b+树作为存储引擎索引结构 在数据库或者存储的世界里,存储引擎的角色一直处于核心位置.往简单了说,存储引擎主要负责数据如何读写.往复杂了说,怎么快速.高效的完成数据的读写,一直是存储引擎要解 ...
- 《算法导论》— Chapter 12 二叉查找树
序 查找树是一种数据结构,它支持多种动态集合操作.包含Search.Minimum.Maximum.PreDecessor.Successor.Insert.Delete等.它既能够用作字典,也能够用 ...
- More Effective C++ 35个改善方法
美·Scott Meyers 侯捷 More Exceptional C++: 40 New Engineering Puzzles, Programming Problems, and Soluti ...
- java实现数据结构
数据结构与算法 :一.数据结构和算法简介 数据结构是指数据在计算机存储空间中的安排方式,而算法时值软件程序用来操作这些结构中的数据的过程.二. 数据结构和算法的重要性 几乎所有的程序都会使用到数据结构 ...
随机推荐
- WPF中实现PropertyGrid的三种方式
原文地址: https://www.cnblogs.com/zhuqil/archive/2010/09/02/Wpf-PropertyGrid-Demo.html 第一种方式:使用WindowsFo ...
- 从“技术宅”到"机器人教父",那个用机器人改变世界的年轻人
写在前面 随着民营企业座谈会的召开,有一位年轻的企业家王兴兴映入了我们的视野.没错就是那个让机器人从实验室走向舞台中央的年轻人. 大家对今年春晚的机器人扭秧歌应该都还印象深刻吧,它就出自于王兴兴创办的 ...
- 微信扫码登录授权过程中state字段的用法
问题描述 最近在实现微信扫码登录这一块,然后看到state字段上面说是可以防csrf攻击 那么现在假设一个用户扫完码后由于某些原因扫码后的响应还没到,但是该平台的回调url已被窃取,然后被人设置到某个 ...
- flutter - [02] 基本语法
题记部分 一.注释 ///这是一个注释 //这也是个注释 /* 这还是个注释 */ void main(List<String> args) { print ('你好 dart'); } ...
- 2. 在Linux 当中安装 Nginx(13步) 下载&安装&启动(详细说明+附加详细截图说明)
2. 在Linux 当中安装 Nginx(13步) 下载&安装&启动(详细说明+附加详细截图说明) @ 目录 2. 在Linux 当中安装 Nginx(13步) 下载&安装&a ...
- [AI/GPT/LLOps/AI中台] Dify : 开源AI大模型应用开发平台(Apache 2.0)
概述:Dify Dify 是一款开源的大语言模型(LLM) 应用开发平台. 它融合了后端即服务(Backend as Service)和 LLMOps 的理念,使开发者可以快速搭建生产级的生成式 AI ...
- antd+vue 中select组件的自定义后缀图标不显示问题记录
根据项目需求,需要使用select组件,并自定义后缀图标,但是设置了没的效果,这是我的代码和效果图 后来查看代码发现需要给select组件加上showArrow属性,然后实现了效果,看效果图 这里记录 ...
- python3 报错ModuleNotFoundError: No module named 'apt_pkg'
前言 apt update无法执行,python3 报错 ModuleNotFoundError: No module named 'apt_pkg' 这是因为将 python 版本升级后的问题 正确 ...
- 如何利用 PostgreSQL 的 JSONB API 作为扩展的轻量级 JSON 解析器
前言 在基于 C 语言的 PostgreSQL 扩展开发中,您可能会遇到需要处理 JSON 等结构化数据的情况.通常,您可能会在扩展中引入第三方 JSON 解析库,例如 cJSON 或 libjans ...
- .NET 10 Preview 2 增强了 Blazor 和.NET MAUI
.NET 团队 3.18 发布了.NET 10 Preview 2(https://devblogs.microsoft.com/dotnet/dotnet-10-preview-2/)..NET 1 ...