- 向一个父结点为3-结点的3-结点中插入新键
当插入的结点是3-结点的时候,将该结点拆分,中间元素提升至父结点,但是此时父结点是一个3-结点, 插入之后,父结点变成了4-结点,然后继续将中间元素提升至其父结点,直至遇到一个父结点是2-结点,然后将其变为3-结点,不需要继续进行拆分
- 分解根结点
当插入结点到根结点的路径上全部是3-结点的时候,最终的根结点会变成一个临时的4-结点,此时,就需要将根结点拆分为两个2-结点,树的高度加1
4)性质
通过对2-3树插入操作的分析,发现在插入的时候,2-3树需要做一些局部的变换来保持2-3树的平衡
一棵完全平衡的2-3树具有以下性质:
-
任意 空链接 到根结点的路径长度都是 相等 的
-
4-结点变换为3-结点时,树的高度不会发生变化,只有当根结点是临时的4-结点,分解根结点时树高+1
-
2-3树与普通二叉查找树最大的区别在于,普通的二叉查找树是自顶向下生长,而2-3树是 自底向上 生长
5)实现
-
直接实现2-3树比较复杂,因为:
-
需要处理不同的结点类型,非常繁琐
-
需要多次比较操作来将结点下移
-
需要上移来拆分4-结点
-
拆分4-结点的情况有很多种
2-3查找树实现起来比较复杂,在某些情况插入后的平衡操作可能会使得效率降低。但是2-3查找树作为一种比较重要的概念和思路对于后面要讲到的红黑树、B树和B+树非常重要
1)与红黑树等价关系
2)转化为红黑树
一棵2-3-4树可以有多棵红黑树,一个红黑树只有一个2-3-4树
3)添加
思路:先添加、再调整
- 新增结点都是红色
- 都是从叶子节点新增
需要调整的四种情况:
- 左左:/
* 一根斜线(此时最好用手比划一下),顶点表示祖节点,中间点表示父节点,底点表示插入节点
- 右右: \
* 一根反斜线,表示同上
- 左右:<
* 一个小于号,顶点表示祖节点,中间尖点表示父节点,底点表示插入节点
- 右左:>
* 一个大于号,表示同上
4)添加后调整
TreeMap 源码
在线演示:https://www.cs.usfca.edu/~galles/visualization/RedBlack.html
5)删除
思路:先删除、再调整
6)删除后调整
见另一篇博文
2-3树能保证在插入元素之后,树依然保持平衡状态,它的最坏情况下所有子结点都是2-结点,树的高度为lgN,相比于普通的二叉查找树,最坏情况下树的高度为N,确实保证了最坏情况下的时间复杂度,但是2-3树实现起来过于复杂,所以介绍一种2-3树思想的简单实现:红黑树
-
红黑树主要是对2-3树进行编码,红黑树背后的基本思想是:
-
用 标准的二叉查找树 (完全由2-结点构成)和一些 额外的信息 (替换3-结点)来表示2-3树
-
将树中的链接分为两种类型
-
红链接:将两个2-结点连接起来构成一个3-结点
-
黑链接:则是2-3树中的普通链接
-
确切的说,将 3-结点 表示为由一条 左斜的红色链接 相连的两个2-结点
两个2-结点:其中一个是另一个的左子结点
- 优点:无需修改就可以直接使用标准的二叉查找树的get方法
1)定义
红黑树是 含有红黑链接 并满足下列条件的 二叉查找树:
-
红链接均为 左链接
-
没有任何一个结点同时和两条红链接相连
-
该树是完美 黑色平衡 的,即任意空链接到根结点的路径上的黑链接数量相同
下面是红黑树与2-3树的对应关系:
以下是另一个版本解读
2)结点API
因为每个结点(根结点除外)都只会有一条指向自己的链接(从它的父结点指向它),可以在之前的Node结点中添加一个布尔类型的变量 color 来表示链接的颜色。如果指向它的链接是红色的,那么该变量的值为true,如果链接是黑色的,那么该变量的值为false
package chapter10;
/**
-
@author 土味儿
-
Date 2021/9/10
-
@version 1.0
-
红黑树结点类
*/
public class Node<K,V> {
/**
- 键 key
*/
private K key;
/**
- 值 value
*/
private V value;
/**
- 左子结点
*/
private Node left;
/**
- 右子结点
*/
private Node right;
/**
-
指向该结点的父结点链接颜色
-
红色:true
-
黑色:false
*/
private boolean color;
/**
-
构造器
-
@param key
-
@param value
-
@param left
-
@param right
-
@param color
*/
public Node(K key, V value, Node left, Node right, boolean color) {
this.key = key;
this.value = value;
this.left = left;
this.right = right;
this.color = color;
}
}
3)平衡化
在对红黑树进行一些增删改查的操作后,很有可能会出现红色的右链接或者两条连续红色的链接,而这些都不满足红黑树的定义,所以需要对这些情况通过旋转进行修复,让红黑树保持平衡
1、左旋
当某个结点的左子结点为黑色,右子结点为红色,此时需要左旋
左黑右红
前提:当前结点为h,它的右子结点为x
-
左旋过程
-
让x的左子结点变为h的右子结点:h.right = x.left
-
让h成为x的左子结点:x.left = h
-
把h的color属性赋给x的color属性值:x.color = h.color
-
把 h 的color变为:RED
左旋初始:
左旋过程:
左旋结束:
2、右旋
当某个结点的左子结点是红色,且左子结点的左子结点也是红色,需要右旋
左子左孙都红
前提:当前结点为h,它的左子结点为x
-
右旋过程
-
让x的右子结点成为h的左子结点:h.left = x.right
-
让h成为x的右子结点:x.right = h
-
把h的color属性赋给x的color属性值:x.color = h.color
-
把 h 的color变为:RED
右旋后x结点仍然与两条红链接相连,可以通过后续颜色反转解决
右旋初始:
右旋过程:
右旋结束:
4)向单个2-结点插入新键
一棵只含有一个键的红黑树只含有一个2-结点。插入另一个键后, 可能就需要旋转
- 如果新键小于当前结点的键
只需要新增一个红色结点即可
新的红黑树和单个3-结点完全等价
- 如果新键大于当前结点的键
那么新增的红色结点将会产生一条红色的右链接,此时需要通过 左旋,把红色右链接变成左链接,插入操作才算完成
形成的新的红黑树依然和3-结点等价,其中含有两个键,一条红色链接
5)向底部2-结点插入新键
用和二叉查找树相同的方式向一棵红黑树中插入一个新键, 会在树的底部新增一个结点(可以保证有序性),唯一区别的地方是会用红链接将新结点和它的父结点相连。如果它的父结点是一个2-结点,那么刚才上面的两种方式仍然适用
6)颜色反转
当一个结点的左子结点和右子结点的color都为RED时,也就是出现了临时的4-结点,此时只需要把左子结点和右子结点的颜色变为BLACK,同时让当前结点的颜色变为RED即可
左右都红
7)向一棵双键树插入新键
双键树:即一个3-结点的树
分为三种子情况
- 新键大于原树中的两个键
- 新键小于原树中的两个键
- 新键介于原树中两个键之间
8)根结点颜色总是黑色
在结点Node对象中color属性表示的是父结点指向当前结点的连接的颜色,由于 根结点不存在父结点,所以每次插入操作后,都需要把根结点的颜色设置为黑色
9)向树底部3-结点插入新键
假设在树的底部的一个3-结点下加入一个新的结点
前面的3种情况都可能会出现
-
右链接:只需要转换颜色即可
-
左链接:需要进行右旋,然后再转换颜色
-
中链接:需要先左旋,然后再右旋,最后转换颜色
颜色转换会使中间结点的颜色变红,相当于将它送入了父结点。这意味着父结点中继续插入一个新键,只需要使用相同的方法解决即可,直到遇到一个2-结点或者根结点为止
10)API
11)实现
package chapter10;
/**
-
@author 土味儿
-
Date 2021/9/10
-
@version 1.0
-
红黑树
*/
public class RedBlackTree<K extends Comparable, V> {
/**
- 根结点
*/
private Node root;
/**
- 元素数量
*/
private int n;
/**
- 红色链接
*/
private static final boolean RED = true;
/**
- 黑色链接
*/
private static final boolean BLACK = false;
/**
- 构造器
*/
public RedBlackTree() {
//this.root = new Node(null, null, null, null, BLACK);
this.n = 0;
}
/**
-
判断结点 x 的父指向链接是否为红色
-
@param x
-
@return
*/
private boolean isRed(Node x) {
if (x != null) {
return x.color == RED;
}
return false;
}
/**
-
对 h 结点左旋
-
-
H X
-
/ \ // \
-
a X 左旋后> H c
-
/ \ / \
-
b c a b
-
-
@param h
-
@return
*/
private Node rotateLeft(Node h) {
// 参数有效性检测
if (h == null || h.right == null) {
return null;
}
// 当前结点为 h,它的右子结点为 x
Node x = h.right;
// 让 x 的左子结点变为 h 的右子结点:h.right = x.left
h.right = x.left;
// 让 h 成为 x 的左子结点:x.left = h
x.left = h;
// 把 h 的color赋给 x 的color值:x.color = h.color
x.color = h.color;
// 把 h 的color变为 RED
h.color = RED;
return x;
}
/**
-
对 h 结点右旋
-
右旋后仍然有结点与两条红链接相连,需要颜色反转
-
-
H X
-
// \ // \\
-
X c 右旋后> a H
-
// \ / \
-
a b b c
-
-
@param h
-
@return
*/
private Node rotateRight(Node h) {
// 参数有效性检测
if (h == null || h.left == null) {
return null;
}
// 当前结点为 h ,它的左子结点为 x
Node x = h.left;
// 让 x 的右子结点变为 h 的左子结点
h.left = x.right;
// 让 h 成为 x 的右子结点
x.right = h;
// 把 h 的color赋给 x 的color:x.color = h.color
x.color = h.color;
// 把 h 的color变为:RED
h.color = RED;
return x;
}
/**
-
对 h 结点颜色反转
-
相当于拆分4-结点
-
-
| ||
-
H ===> H
-
// \\ / \
-
a b a b
-
-
@param h
*/
private void flipColors(Node h) {
if (h == null) {
return;
}
// 让 h 的左右子结点颜色变为黑色
h.left.color = BLACK;
h.right.color = BLACK;
// 让 h 的颜色变为红色
h.color = RED;
}
/**
-
插入/修改元素
-
@param key
-
@param value
*/
public void put(K key, V value) {
root = put(root, key, value);
// 根结点总是黑色
root.color = BLACK;
}
/**
-
在 h 上插入元素,并返回新树
-
@param h
-
@param key
-
@param value
-
@return
*/
private Node put(Node h, K key, V value) {
// 如果 h 为 null
if (h == null) {
// 数量加1
n++;
// 新建结点并返回
return new Node(key, value, null, null, RED);
}
// 比较 key 与 h 结点的键的大小
int cmp = key.compareTo(h.key);
if (cmp < 0) {
// 小于:向左子结点添加(递归)
h.left = put(h.left, key, value);
} else if (cmp > 0) {
// 大于:向右子结点添加(递归)
h.right = put(h.right, key, value);
} else {
// 等于:值替换
h.value = value;
}
// 左旋:左黑右红
if (!isRed(h.left) && isRed(h.right)) {
h = rotateLeft(h);
}
// 右旋:左子左孙都红
if (h.left != null && isRed(h.left) && isRed(h.left.left)) {
h = rotateRight(h);
}
// 颜色反转:左右都红
if (isRed(h.left) && isRed(h.right)) {
flipColors(h);
}
// 返回新树h
return h;
}
/**
-
得到 key 的值
-
@param key
-
@return
*/
public V get(K key) {
return get(root, key);
}
/**
-
在 h 中得到 key 的值
-
@param h
-
@param key
-
@return
*/
private V get(Node h, K key) {
if (h == null) {
return null;
}
// 比较 key 与 h 键的大小
int cmp = key.compareTo(h.key);
if (cmp < 0) {
// 小于:递归查找左子树
return get(h.left, key);
} else if (cmp > 0) {
// 大于:递归查找右子树
return get(h.right, key);
} else {
// 等于:返回 value
return h.value;
}
}
/**
-
元素数量
-
@return
*/
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
总结
阿里伤透我心,疯狂复习刷题,终于喜提offer 哈哈~好啦,不闲扯了
1、JAVA面试核心知识整理(PDF):包含JVM,JAVA集合,JAVA多线程并发,JAVA基础,Spring原理,微服务,Netty与RPC,网络,日志,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存,Hadoop,Spark,Storm,YARN,机器学习,云计算共30个章节。
2、Redis学习笔记及学习思维脑图
3、数据面试必备20题+数据库性能优化的21个最佳实践
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-q3h845Vx-1713549937888)]
[外链图片转存中…(img-4YrAPAht-1713549937891)]
[外链图片转存中…(img-c0chr6bh-1713549937892)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
总结
阿里伤透我心,疯狂复习刷题,终于喜提offer 哈哈~好啦,不闲扯了
[外链图片转存中…(img-MWBoCcW9-1713549937893)]
1、JAVA面试核心知识整理(PDF):包含JVM,JAVA集合,JAVA多线程并发,JAVA基础,Spring原理,微服务,Netty与RPC,网络,日志,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存,Hadoop,Spark,Storm,YARN,机器学习,云计算共30个章节。
[外链图片转存中…(img-Lvg1eDay-1713549937894)]
2、Redis学习笔记及学习思维脑图
[外链图片转存中…(img-0VIoYBfA-1713549937895)]
3、数据面试必备20题+数据库性能优化的21个最佳实践
[外链图片转存中…(img-1Izrd08E-1713549937901)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!