[数据结构与算法] 树结构之二叉排序树、平衡二叉树、多路查找树
BST
介绍
需求: 给你一个数列 (7, 3, 10, 12, 5, 1, 9),要求能够高效的完成对数据的查询和添加
解决方案——使用数组
- 数组未排序, 优点:直接在数组尾添加,速度快。 缺点:查找速度慢.
- 数组排序,优点:可以使用二分查找,查找速度快,缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需整体移动,速度慢。
- 使用链式存储-链表. 不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动。
在此基础上,我们引入了二叉排序树, 它能够高效的实现数组的添加和删除
二叉排序树介绍
二叉排序树:BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
- 特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点
- 比如针对前面的数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树以及添加一个元素2后二叉排序树为:
创建和遍历
- 创建节点类, 定义相关属性构造方法以及ToSting()方法
- 编写BST的创建方法, 判断传入的节点是否为空 传入的节点的值小于当前节点, 插入到左子树(判断非空), 左子树递归调用创建节点的方法 传入的节点的值大于当前节点, 插入到右子树(判断非空), 右子树递归调用创建节点的方法
- 编写中序遍历方法
- 创建二叉排序树类,定义根节点. 根据根节点是否为空的情况, 编写二叉排序树的创建和遍历方法
- 测试代码
public class BinarySortTreeDemo {
public static void main(String[] args) {
int[] arr = {7, 3, 10, 12, 13, 5, 1, 9};
BinarySortTree binarySortTree = new BinarySortTree();
//循环将节点添加到二叉树
for (int i = 0; i < arr.length; i++) {
binarySortTree.add(new Node(arr[i]));
}
//中序遍历二叉树
System.out.println("中序遍历二叉树");
binarySortTree.midOrder();
}
}
/**
* 创建二叉排序树
*/
class BinarySortTree{
//创建根节点
private Node root;
//创建二叉排序树的添加方法
public void add(Node node){
if (root==null){
root = node;
}else {
root.add(node);
}
}
//创建二叉排序树的中序遍历方法
public void midOrder(){
if (root==null){
System.out.println("该树为空,无法进行中序遍历");
}else {
root.midOder();
}
}
}
/**
* 1.创建节点
*/
class Node{
int value;
Node left;
Node right;
public Node(int value){
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
//二叉排序树的生成
public void add(Node node){
if (node==null){//如果为空,直接返回
return;
}
//传入的节点的值小于当前节点
if (node.value<this.value){//插入到左子树
if (this.left==null){
this.left = node;
}else {
//递归调用添加的方法
this.left.add(node);
}
}else {
//传入节点的值大于当前节点
if (this.right==null){
this.right = node;
}else {
//右子树递归调用添加的方法
this.right.add(node);
}
}
}
public void midOder(){
if (this.left!=null){
this.left.midOder();
}
System.out.println(this);
if (this.right!=null){
this.right.midOder();
}
}
}
测试结果
删除
二叉排序树的删除情况比较复杂,有下面三种情况需要考虑
- 删除叶子节点 (比如:2, 5, 9, 12)
- 删除只有一颗子树的节点 (比如:1)=>删除的节点的值=子树节点
- 删除有两颗子树的节点. (比如:7, 3,10 )=>删除的节点的值=任意一边的最小值
实现思路
public class BinarySortTreeDemo {
public static void main(String[] args) {
int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
BinarySortTree binarySortTree = new BinarySortTree();
//循环将节点添加到二叉树
for (int i = 0; i < arr.length; i++) {
binarySortTree.add(new Node(arr[i]));
}
//中序遍历二叉树
System.out.println("中序遍历二叉树");
binarySortTree.midOrder();
//测试删除叶子节点
binarySortTree.delNode(2);
binarySortTree.delNode(5);
binarySortTree.delNode(9);
binarySortTree.delNode(12);
//测试删除含有两个叶子节点的节点
binarySortTree.delNode(7);
binarySortTree.delNode(3);//原来3的位置放的是2
binarySortTree.delNode(10);//原来10的位置放的是9
binarySortTree.delNode(1);//原来10的位置放的是9
System.out.println("执行删除操作完毕");
binarySortTree.midOrder();
}
}
/**
* 创建二叉排序树
*/
class BinarySortTree{
//创建根节点
private Node root;
//创建二叉排序树的添加方法
public void add(Node node){
if (root==null){
root = node;
}else {
root.add(node);
}
}
//创建二叉排序树的中序遍历方法
public void midOrder(){
if (root==null){
System.out.println("该树为空,无法进行中序遍历");
}else {
root.midOder();
}
}
/**
* 查找要删除的节点
* @param value
* @return 如果有返回该节点,,如果没有返回null
*/
public Node search(int value){
if (root==null){
return null;
}else {
return root.search(value);
}
}
/**
* 查找要删除的父节点
* @param value
* @return 如果有返回该节点,如果没有返回null
*/
public Node searchParent(int value){
if (root==null){
return null;
}else {
return root.searchParent(value);
}
}
/**
* 删除对应的节点
* @param value
*/
public void delNode(int value){
if (root==null){
return;
}else {
//如果根节点不会空则进行节点的删除操作
//1. 查找要删除的节点targetNode
Node targetNode = search(value);
//如果没有找到要删除的节点
if (targetNode==null){
return;
}
//如果要查找的二叉树只有一个节点
if (root.left==null && root.right==null){
root = null;
return;
}
//2.去找到targetNode的父节点
Node parent = searchParent(value);
//a.如果要删除的节点都是叶子节点
if (targetNode.left==null && targetNode.right==null){
if (parent.left!=null && parent.left.value==value){//是左子节点
parent.left = null;
}else if(parent.right!=null && parent.right.value==value){//是右子节点
parent.right = null;
}
}else if(targetNode.left!=null && targetNode.right!=null){//b.删除有两个子树的节点
//在右树中找最小的
int minVal = delRightTreeMin(targetNode.right);
targetNode.value = minVal;
System.out.println("minVal = " + minVal);
}else {//c.删除只有一个子树的节点
//x如果要删除的节点有左子节点
if (targetNode.left!=null){
if (parent!=null){//***防止出现在删除最后两个节点时,首先删除根节点的情况
if (parent.left.value==value){//如果targetNode是parent的左子节点
parent.left = targetNode.left;
}else {//如果targetNode是parent的右子节点
parent.right = targetNode.left;
}
}else {
root = targetNode.left;
}
}else {//x如果要删除的节点有右子节点
if (parent!=null){//***防止出现在删除最后两个节点时,首先删除根节点的情况
if (parent.left.value==value){//如果targetNode是parent的左子节点
parent.left = targetNode.right;
}else {//如果targetNode是parent的右子节点
parent.right = targetNode.right;
}
}else {
root = targetNode.right;
}
}
}
}
}
/**
* 编写方法:
* 1.返回以node为根节点的二叉树的最小节点的值
* 2.删除以node为根节点的二叉树的最小节点
* @param node
* @return
*/
public int delRightTreeMin(Node node){
Node target = node;
// 循环的查找左子节点,就会找到最小值
while (target.left!=null){
target = target.left;
}
// 如果左子节点为空,说明找到了最小节点
// 删除最小节点
delNode(target.value);
return target.value;
}
}
/**
* 1.创建节点
*/
class Node{
int value;
Node left;
Node right;
public Node(int value){
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
//二叉排序树的生成
public void add(Node node){
if (node==null){//如果为空,直接返回
return;
}
//传入的节点的值小于当前节点
if (node.value<this.value){//插入到左子树
if (this.left==null){
this.left = node;
}else {
//递归调用添加的方法
this.left.add(node);
}
}else {
//传入节点的值大于当前节点
if (this.right==null){
this.right = node;
}else {
//右子树递归调用添加的方法
this.right.add(node);
}
}
}
public void midOder(){
if (this.left!=null){
this.left.midOder();
}
System.out.println(this);
if (this.right!=null){
this.right.midOder();
}
}
/**
* 查找要删除的节点
* @param value 要删除的节点的值
* @return 如果找到返回该节点,否则返回null
*/
public Node search(int value){
if (value<this.value){//如果小于父节点,则进入左子树
if (this.left==null){
return null;
}
return this.left.search(value);//左子树递归调用查找的方法
}else if(value>this.value){//如果大于父节点,则进行入右子树
if (this.right==null){
return null;
}
return this.right.search(value);//右子树递归调用查找方法
}else {//如果查找的值等于父节点,则直接返回该节点
return this;
}
}
/**
* 查找要删除的父节点
* @param value 要删除的节点的值
* @return 要删除的父节点, 否则返回null
*/
public Node searchParent(int value){
//如果当前节点就是要删除的父节点, 则直接返回
if ( (this.left!=null && this.left.value==value) || (this.right!=null && this.right.value==value) ){
return this;
}else {
// 如果需要找的值小于当前节点则递归调用左子树(判空)
if (this.left!=null && value<this.value){
return this.left.searchParent(value);
// 如果需要找到值大于当前节点则递归调用右子树(判空)
}else if (this.right!=null && value>this.value){
return this.right.searchParent(value);
}else {
return null;//没有找到父节点
}
}
}
}
测试结果
AVL树
看一个案例(说明二叉排序树可能的问题) 给你一个数列{1,2,3,4,5,6},要求创建一个二叉排序树(BST), 并分析问题所在
介绍
平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树, 可以保证查询效率较高。 具有以下特点:
- 它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
- 平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。
举例: 下图1,2是AVL树,3不是. 因为左右两个子树高度差为2
AVL树的左旋转
AVL左旋转思路图解
// 返回左子树的高度
public int leftHeight() {
if (left == null) {
return 0;
}
return left.height();
}
// 返回右子树的高度
public int rightHeight() {
if (right == null) {
return 0;
}
return right.height();
}
// 返回以该结点为根结点的树的高度
public int height() {
return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
}
/**
* 相当于上面使用三目运算符实现的返回根节点的树的高度的方法
* @return
*/
/* public int height(){
int leftVal = 0;
int rightVal = 0;
//如果左树不为空,则递归调用该方法
if (left!=null){
leftVal=left.height();
}
//如果右树不为空,则递归调用该方法
if (right!=null){
rightVal = right.height();
}
// 返回时,需要将本层的高度加上
return Math.max(leftVal, rightVal)+1;
}*/
//左旋转方法
private void leftRotate() {
//创建新的结点,以当前根结点的值
Node newNode = new Node(value);
//把新的结点的左子树设置成当前结点的左子树
newNode.left = left;
//把新的结点的右子树设置成当前结点的右子树的左子树
newNode.right = right.left;
//把当前结点的值替换成右子结点的值
value = right.value;
//把当前结点的右子树设置成当前结点右子树的右子树
right = right.right;
//把当前结点的左子树(左子结点)设置成新的结点
left = newNode;
}
AVL树的右旋转
AVl右旋转思路图解
//右旋转方法
private void rightRotate() {
Node newNode = new Node(value);
newNode.right = right;
newNode.left = left.right;
value = left.value;
left = left.left;
right = newNode;
}
AVL树的双旋转
AVL树的双旋转的图解
多路查找树
二叉树的操作效率较高,但是也存在问题, 请看下面的二叉树
二叉树需要加载到内存的,如果二叉树的节点少,没有什么问题,但是如果二叉树的节点很多(比如1亿), 就存在如下问题:
- 问题1:在构建二叉树时,需要多次进行i/o操作(海量数据存在数据库或文件中),节点海量,构建二叉树时,速度有影响
- 问题2:节点海量,也会造成二叉树的高度很大,会降低操作速度.
如果对二叉查找树/二叉排序树和平衡二叉树等概念理解不清楚, 建议看看下面 理解完全二叉树、平衡二叉树、二叉查找树
多叉树
在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树(multiway tree)
后面我们讲解的2-3树,2-3-4树就是多叉树,多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。
举例说明(下面2-3树就是一颗多叉树)
B树
B树通过重新组织节点,降低树的高度,并且减少i/o读写次数来提升效率。
如下图B树通过重新组织节点, 降低了树的高度.
- 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页得大小通常为4k),这样每个节点只需要一次I/O就可以完全载入
- 将树的度M设置为1024,在600亿个元素中最多只需要4次I/O操作就可以读取到想要的元素, B树(B+)广泛应用于文件存储系统以及数据库系统中
2-3树
2-3树是最简单的B树结构, 具有如下特点:
- 2-3树的所有叶子节点都在同一层.(只要是B树都满足这个条件)
- 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点.
- 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点.
- 2-3树是由二节点和三节点构成的树
插入规则:
- 2-3树的所有叶子节点都在同一层.(只要是B树都满足这个条件)
- 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点.
- 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点
- 当按照规则插入一个数到某个节点时,不能满足上面三个要求,就需要拆,先向上拆,如果上层满,则拆本层,拆后仍然需要满- 足上面3个条件。
- 对于三节点的子树的值大小仍然遵守(BST 二叉排序树)的规则
下图是模拟2-3树创建的结构图
除了23树,还有234树等,概念和23树类似,也是一种B树。它除了每个节点所容纳的元素个数不同外, 创建方式和23树一样 如图
B树的介绍
前面已经介绍了2-3树和2-3-4树,他们就是B树(英语:B-tree 也写成B-树),这里我们再做一个说明,我们在学习Mysql时,经常听到说某种类型的索引是基于B树或者B+树的,如下图:
树的说明:
- B树的阶:节点的最多子节点个数。比如2-3树的阶是3,2-3-4树的阶是4
- B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点
- 关键字集合分布在整颗树中, 即叶子节点和非叶子节点都存放数据.
- 搜索有可能在非叶子结点结束
- 其搜索性能等价于在关键字全集内做一次二分查找
B+树
B+树是B树的变体,也是一种多路搜索树。
B+树的说明:
- B+树的搜索与B树也基本相同,区别是B+树只有达到叶子结点才命中(B树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找
- 所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的。
- 不可能在非叶子结点命中 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
- 更适合文件索引系统
- B树和B+树各有自己的应用场景,不能说B+树完全比B树好,反之亦然.
B* 树
B* 树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针。(和B树,B+树区别在此)
B*树的说明:
- B* 树定义了非叶子结点关键字个数至少为(2/3)*M(M为树的度),即块的最低使用率为2/3,而B+树的块的最低使用率为B+树的1/2。
- 从第1个特点我们可以看出,B*树分配新结点的概率比B+树要低,空间使用率更高
- python 网页特征提取XPATH(两天玩转) 第一天
- 和开发同学讨论的一个技术问题(r8笔记第73天)
- 剖析Oracle中oerr命令(r8笔记第70天)
- 甜品店切蛋糕问题(动态规划,Go语言实现)
- SQL—复制表结构及其数据
- python连接SQL报错:1366, "Incorrect string value: '\xF0\x9F\x98\x81'
- PCIE的简单配置(r8笔记第82天)
- 7个深度神经网络可视化工具,不可错过!
- Pwnhub之奇妙的巨蟒 Writeup
- WINDOW 安装mysql5.7数据库,并设置密码及相关报错
- go channel 通信通道
- SQl 语句(常见) 新建,删除,修改表,新增字段,修改默认值
- SQL处理表结构的基本方法整理(创建表,关联表,复制表)
- Go web之旅(路由篇)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 聊聊java中的StampedLock并发锁原理
- tomcat对AQS的扩展:使用LimitLatch控制连接数
- kubernete编排技术五:DaemonSet
- 深度剖析github上15.1k Star项目:redux-thunk
- 在不影响程序使用的情况下添加shellcode
- [K8s 1.9实践]Kubeadm 1.9 HA 高可用 集群 本地离线镜像部署
- ansible模块command、shell、raw、script
- systemd - CentOS 7进程守护&监控
- Java 8的新特性还不了解?快进来!
- 【Vulnhub】Play XML Entities
- 一切皆是映射:詳解 Kotlin Map 集合類
- 10大高性能开发宝石,我要消灭一半程序员!
- 面试官:你说你会RabbitMQ,那聊聊它的交换机(Exchange)吧
- Kubeadm 1.9 HA 高可用集群本地离线镜像部署【已验证】
- kubernetes(k8s)集群安装calico