HashMap底层红黑树原理(超详细图解)+手写红黑树代码
在看完了小刘老师和黑马的源码视频之后,我整理了一篇HashMap的底层源码文章,学海无涯,这几天看了对红黑树的讲解,故将其整理出来
HashMap底层源码解析上
HashMap底层源码解析下
视频链接
小刘老师讲源码
传智播客-黑马程序员
文章目录
- 树结构回顾
- 什么是树
- 树结构的常用术语
- 二叉树
- 二叉搜索树
- 二叉搜索树-查找节点
- 二叉搜索树-插入节点
- 二叉搜索树-遍历节点
- 二叉搜索树-查找最大值和最小值
- 二叉搜索树-删除节点
- 二叉搜索树-时间复杂度分析:
- 普通二叉搜索树的致命缺陷
- AVL树简介
- 为什么有了二叉平衡树还需要红黑树
- 红黑树原理
- 红黑树的性质
- 红黑树查找
- 红黑树插入
- 红黑树插入节点情景分析
- 情景1:红黑树为空树
- 情景2:插入结点的Key已存在
- 情景3:插入结点的父结点为黑结点
- 情景4:插入结点的父结点为红色
- 插入场景4.1:叔叔结点存在并且为红结点
- 插入场景4.2:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的左子节点
- 插入场景4.2.1:新插入结点,为其父结点的左子结点(LL红色情况)
- 情景4.2.2:新插入结点,为其父结点的右子结点(LR红色情况)
- 情景4.3:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的右子结点
- 插入场景4.3.1:新插入结点,为其父结点的右子结点(RR红色情况)
- 插入场景4.3.2:新插入结点,为其父结点的左子结点(RL红色情况)
- 下面我们具体分析一个案例
- 手写红黑树
- 手写红黑树代码
- 对代码进行测试
树结构回顾
什么是树
树(tree)是一种抽象数据类型(ADT),用来模拟具有树状结构性质的数据集合,它是由n(n>0)个有限节点通过连接它们的边组成一个具有层次关系的集合。把它叫做“树”是因为他看起来像一颗倒挂的树,也就是说它是根朝上,而叶向下的。
树有很多种,向上面的一个节点有多余两个的子节点的树,称为多路树,而每个节点最多只能有两个子节点的一种形式称为二叉树
- 节点:上图的圆圈,比如A,B,C等都是表示节点,节点一般代表一些实体,在java面向对象编程中,节点一般代表对象
- 边:连接节点的线称为边,边表示节点的关联关系,一般从一个节点到另一个节点的唯一方法就是沿着一条顺着有边的道路前进,在java当中通常表示引用
树结构的常用术语
路径:顺着节点的边从一个节点走到另一个节点,所经过的节点的顺序排列就称为‘路径’
根:树顶端的节点称为根,一棵树只有一个根,如果要把一个节点和边的集合称为树,那么从根到其他任何一个节点都必须有且只有一条路径,A是根节点。
父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点
子节点:一个节点含有的子树的节点称为该节点的子节点,F,G是C的子节点
兄弟节点:具有相同父节点的节点互称为兄弟节点,F,G互为兄弟节点
叶节点:没有子节点的节点称为叶节点,也叫叶子结点,比如上图的H,E,F,G都是叶子结点
子树:每个节点都可以作为子树的根,它和它所有的子节点,子节点的子节点等都包含在子树中
节点的层次:从根开始定义,根为第一层,根的子节点为第二层,以此类推。
深度:对于任意节点n,n的深度为从根到n的唯一路径长,根的深度为0(从上往下看)
高度:对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0;(从下往上看)
二叉树
树的每个节点最多只能有两个子节点
上图的第一幅图B节点有DEF三个子节点,就不是二叉树,称为多路树
而第二幅图每个节点最多只有两个节点,是二叉树,并且二叉树的子节点称为“左子节点”和“右子节点”
二叉搜索树
如果我们给二叉树加一个额外的条件,就可以得到一种被称作二叉搜索树的特殊二叉树
二叉搜索树要求:
若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值
若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值
它的左、右子树也分别为二叉排序树
二叉搜索树-查找节点
查找某个节点,我们必须从根节点开始查找
- 查找值比当前节点值大,则搜索右子树
- 查找值等于当前节点值,停止搜索(终止条件)
- 查找值小于当前节点值,则搜索左子树
二叉搜索树-插入节点
要插入节点,必须先找到插入的位置,与查找操作相似,由于二叉搜索树的特殊性
待插入节点也需要从根节点开始进行比较,小于根节点则与根节点左子树比较
反之则与右子树比较,直到左子树为空或右子树为空,则插入到相应为空的位置
二叉搜索树-遍历节点
遍历树是根据一种特定的顺序访问树的每一个节点,比较常用的有前序遍历,中序遍历和后序遍历。而二叉搜索树最常用的是中序遍历
- 中序遍历:左子树->根节点->右子树
- 前序遍历:根节点->左子树->右子树
- 后序遍历:左子树->右子树->根节点
二叉搜索树-查找最大值和最小值
要找最小值,先找根的左节点,然后一直找这个左节点的左节点,直到没有左节点,那么这个节点就是最小值,同理要找最大值,一直找根节点的右节点,直到没有右节点,则就是最大值
二叉搜索树-删除节点
删除节点是二叉搜索树中最复杂的操作,删除的节点有三种情况,前两种比较简单,但是第三种却很复杂
- 该节点是叶节点(没有子节点)
- 该节点有一个子节点
- 该节点有两个子节点
①删除没有子节点的节点
要删除叶节点,只需要改变该节点的父节点引用该节点的值,即将其引用改为null即可
②删除有一个子节点的节点
删除有一个子节点的节点,我们只需要将其父节点原本指向该节点的引用,改为指向该节点的子节点即可
③删除有两个子节点的节点
当删除的节点存在两个子节点,那么删除之后,两个子节点的位置我们就没办法处理了,既然处理不了,我们就想到一种办法,用另一个节点来代替被删除的节点,那么用哪一个节点来代替呢?
我们直到二叉搜索树中的节点是按照关键字
来进行排列的,某个节点的关键字次高节点
是它的中序遍历后继节点。
用后继节点来代替删除的节点,显然该二叉树还是有序的
那么如何找到删除节点的中序后继节点呢?
其实我们稍微分析,这实际上就是要找比删除节点关键值大的节点集合中最小的节点,只有这样代替删除节点后才能满足二叉搜索树的特性
④删除有必要吗?
通过上面的删除分类讨论,我们发现删除其实是挺复杂的,那么其实我们可以不用真正的删除该节点,只需要在Node类中增加一个标识字段isDelete,当该字段为true,标识该节点已经删除,反之则没有删除,这样删除节点就不会改变树的结构了。
影响就是查询时需要判断一下节点是否删除
二叉搜索树-时间复杂度分析:
二分查找算法时间复杂度推算过程
从上表可以看出,N(2*k)肯定是大于等于1,也就是N/(2^K)>=1,我们计算时间复杂度是按照最坏的情况进行计算,也就是查到剩余最后一个数才查到我们想要的数据,也就是
N/(2^k)=1 => 2^K = N => K = log2(N) =>二分查找算法时间复杂度:O(log2(N))=>O(logN)
普通二叉搜索树的致命缺陷
这颗二叉搜索树的时间复杂度为O(N)
可以看到这颗二叉搜索树的左子树实际上已经成为了链表
怎么解决二叉搜索树退化成线性链表的问题?
如果插入元素时,树可以自动调整两边平衡,会保持不错的查找性能
AVL树简介
AVL树有什么特点?
- 具有二叉搜索树的全部特性
- 每个节点的左子树和右子树的高度差至多等于1
平衡树基于这种特点就可以保证不会出现大量节点偏向于一边的情况了(插入或删除时,会发生左旋,右旋操作,使这棵树再次左右保持一定的平衡)
为什么有了二叉平衡树还需要红黑树
虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在O(logn),不过却不是最佳的
因为平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋
和右旋
来进行调整,使之再次称为一颗符合要求的平衡树
显然,如果在那种插入删除很频繁的场景中,平衡树需要频繁的进行调整,这会使平衡树的性能大打折扣,为了解决这个问题,便有了红黑树
红黑树原理
红黑树的性质
红黑树属于二叉搜索树的一个特殊分支
红黑树并不是一个完美平衡二叉查找树
,从图上可以看到,根节点P的左子树显然比右子树高
但左子树和右子树的黑结点的层数是相等的,也即任意一个节点到每个叶子结点的路径都包含数量相同的黑结点
所以我们叫红黑树这种平衡为黑色完美平衡
前面讲到红黑树能自平衡,它靠的是什么?
三种操作:左旋
,右旋
和变色
- 变色:结点的颜色由红变黑或由黑变红
- 左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子树变为旋转结点的右子树,右子结点的右子树保持不变
- 右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子树变为旋转结点的左子树,左子结点的左子树保持不变
左旋图示:
右旋图示:
红黑树查找
因为红黑树是一颗二叉平衡树,而且查找不会破坏树的平衡,所以查找跟二叉平衡树无异
例如在此红黑树中查找指定元素:
查找50
50<80,找到左子结点40
50>40,找到40的右子结点60
50<60,找到60的左子结点50
红黑树插入
插入操作包括两部分工作
- 查找插入的位置
- 插入后自平衡
注意:插入结点,必须为红色,理由很简单,红色在父结点(如果存在)为黑色结点时,红黑树的黑色平衡没被破坏,不需要做自平衡操作,但如果插入结点是黑色,那么插入位置所在的子树黑色结点总是多1,破坏了红黑树的黑高性质,必须做自平衡。
在开始每个情景的讲解前,我们还是先来约定下
红黑树插入节点情景分析
情景1:红黑树为空树
最简单的一种情景,直接把插入结点作为根节点就行
注意,根据红黑树性质2,:根节点是黑色。还需要把插入结点设为黑色
情景2:插入结点的Key已存在
处理:更新当前节点的值,为插入结点的值
情景3:插入结点的父结点为黑结点
由于插入的结点是红色的,当插入结点的父结点为黑色时,并不会影响红黑树的平衡,直接插入即可,无需做自平衡
情景4:插入结点的父结点为红色
再次回想下红黑树的性质2:根节点是黑色,如果插入结点的父结点为红结点,那么该父结点不可能为根节点,所以插入结点总是存在祖父结点,这一点很关键,因为后续的旋转操作肯定需要祖父结点的参与
插入场景4.1:叔叔结点存在并且为红结点
依据红黑树性质4可知。红色结点不能相连=>祖父结点肯定为黑结点
因为不可以同时存在两个相连的红结点,那么此时该插入子树的红黑树层数的情况是:黑红红,显然最简单的处理方式是把其改为红黑红处理:
- 将P和U结点改为黑色
- 将PP改为红色
- 将PP设置为当前结点,继续后续处理
可以看到,我们把PP节点设为红色了,如果PP的父结点是黑色,那么无需再做任何处理;
但如果PP的父结点是红色,则违反红黑树性质了,所以需要将PP设置为当前结点,继续做插入操作自平衡处理,直到平衡为止
插入场景4.2:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的左子节点
注意:单纯从插入前来看,叔叔结点非红即空(NIL结点),否则的话破坏了红黑树性质5,此路径会比其他路径多一个黑色结点。
插入场景4.2.1:新插入结点,为其父结点的左子结点(LL红色情况)
处理:
- 变颜色:将P设置为黑色,将PP设置为红色
- 对PP节点进行右旋
情景4.2.2:新插入结点,为其父结点的右子结点(LR红色情况)
处理:
- 对P进行左旋
- 将P设置为当前结点,得到LL红色情况
- 按照LL红色情况处理(1.变颜色2.右旋PP)
情景4.3:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的右子结点
该情景对应情景4.2,只是方向反转,直接看图
插入场景4.3.1:新插入结点,为其父结点的右子结点(RR红色情况)
处理:
- 变颜色:将P设置为黑色,将PP设置为红色
- 对PP结点进行左旋
插入场景4.3.2:新插入结点,为其父结点的左子结点(RL红色情况)
处理:
- 对P进行右旋
- 将P设置为当前结点,得到RR红色情况
- 按照RR红色情况处理(1.变颜色2.左旋PP)
注:上图第一步图片内容错误,应该为右旋
下面我们具体分析一个案例
- 要求,在红黑树中插入结点7
- 找到插入位置,7<19,7>5,7<15,7<8,故7应该放在8的左子结点
- 7的父结点和叔叔结点都为红色,且78为双红,违反了性质4,属于上面提到的插入情景4.1
- 将8,18结点变黑,将15结点变红,将当前结点设置为15
- 此时5,15为红色,出现双红,违反了性质4,,但和我们提到的情景4.2.2类似,以5结点进行左旋
- 左旋之后,5与15结点为双红,符合情景4.2.1,故父结点变为黑色,祖父结点变为红色,变色完毕以根结点19为基准,进行右旋
手写红黑树
- 创建RBTree,定义颜色
- 创建RBNode
- 辅助方法定义:parentOf(node),isRed(node),isBlack(node),setRed(node),setBlack(node),InOrderPrint()
- 左旋方法定义leftRotate(node)
- 右旋方法定义rightRotate(node)
- 公开插入接口方法定义:insert(K key,V value)
- 内部插入接口方法定义:insert(RBNode node)
- 修正插入导致红黑树失衡的方法定义:insertFlxUp(RBNode node)
- 测试红黑树正确性
手写红黑树代码
package HashMapProject;import com.sun.org.apache.regexp.internal.RE;
import org.omg.CORBA.NO_IMPLEMENT;/*** Description* User:* Date:* Time:* - 创建RBTree,定义颜色* - 创建RBNode* - 辅助方法定义:parentOf(node),isRed(node),isBlack(node),setRed(node),setBlack(node),InOrderPrint()* - 左旋方法定义leftRotate(node)* - 右旋方法定义rightRotate(node)* - 公开插入接口方法定义:insert(K key,V value)* - 内部插入接口方法定义:insert(RBNode node)* - 修正插入导致红黑树失衡的方法定义:insertFlxUp(RBNode node)* - 测试红黑树正确性*/
//RBTree中node节点里面存的就是key和value
public class RBTree<K extends Comparable<K>, V> {private static final boolean RED = true;private static final boolean BLACK = false;/** 树根的引用 */private RBNode root;public RBNode getRoot() {return root;}/*** 获取当前结点的父结点* @param node* @return*/private RBNode parentOf(RBNode node){if (node!=null){return node.parent;}return null;}/*** 结点是否为红色* @param node* @return*/private boolean isRed(RBNode node){return node!=null&&node.color == RED;}/***结点是否为黑色* @param node* @return*/private boolean isBlack(RBNode node){return node!=null&&node.color==BLACK;}/*** 将当前结点颜色设置为红色* @param node*/private void setRed(RBNode node){if (node!=null){node.color=RED;}}/*** 将当前结点颜色设置为黑色* @param node*/private void setBlack(RBNode node){if (node!=null){node.color=BLACK;}}public void InOrderPrint(){inOrderPrint(this.root);}/*** 中序打印,左中右* @param node*/private void inOrderPrint(RBNode node){if (node!=null){inOrderPrint(node.left);System.out.println("key:"+node.key+",value:"+node.value);inOrderPrint(node.right);}}/*** 公开的插入方法* @param key* @param value*/public void insert (K key,V value){RBNode node = new RBNode();//新节点一定是红色,为了不违反黑高性质node.setColor(RED);node.setKey(key);node.setValue(value);insert(node);}private void insert(RBNode node){//第一步:查找当前node的父节点RBNode parent = null;RBNode x = this.root;while (x!=null){parent = x;//cmp>0 说明node.key大于x.key,需要到x的右子树寻找//cmp=0 说明node.key等于x.key,需要进行替换操作//cmp<0 说明node.key小于x.key,需要到x的左子树寻找int cmp = node.key.compareTo(x.key);if (cmp>0){x=x.right;}else if (cmp<0){x=x.left;}else {x.setValue(node.getValue());return;}}node.parent=parent;//判断node与parent的key谁大,以此来判断是在parent的左子树还是右子树//防止红黑树时空的,这样parent就为空,所以这里if判断一下if (parent!=null){int cmp = node.key.compareTo(parent.key);if (cmp>0){//当前node的key比parent的key大,需要把node放入parent的右子节点parent.right=node;}else {//没有等于0的情况,等于0的情况在while中已经做了替换//当前node的key比parent的key小,需要把node放入parent的左子节点parent.left=node;}}else {//红黑树为空,则node直接为root节点this.root=node;}//需要调用修复红黑树平衡的方法insertFlxUp(node);}/**插入后修复红黑树平衡的方法* |---情景1:红黑树为空树* |---情景2:插入结点的key已经存在,不需要处理,已经在插入时做了替换* |---情景3:插入结点的父节点为黑色,因为你所插入的路径,黑色结点没有变化,所以红黑树依然平衡,所以不需要处理** 真正需要处理的是情景4的所属情况* |---情景4:插入结点的父节点为红色* |---情景4.1:叔叔结点存在,并且为红色(父-叔 双红)* 将父亲结点和叔叔结点染成黑色,将祖父结点染成红色,并设置祖父结点为当前结点* |---情景4.2:叔叔结点不存在,或者为黑色,父结点为爷爷结点的左子树* |---情景4.2.1:插入结点为其父结点的左子结点(LL情况)* 将父亲结点染成黑色,将祖父结点染成红色进行右旋* |---情景4.2.2:插入结点为其父结点的右子结点(LR情况)* 以父亲结点进行左旋,按照4.2.1情景处理* |---情景4.3:叔叔结点不存在,或者为黑色,父结点为爷爷结点的右子树* |---情景4.3.1:插入结点为其父结点的左子结点(RL情况)* 以父亲结点进行右旋,按4.3.2情景处理* |---情景4.3.2:插入结点为其父结点的右子结点(RR情况)* 将父亲结点染成黑色,将祖父结点染成红色,进行左旋*/private void insertFlxUp(RBNode node){this.root.setColor(BLACK);//拿到父亲结点RBNode parent = parentOf(node);RBNode gparent = parentOf(parent);//父节点是红色if (parent!=null&&isRed(parent)){//进入情景4RBNode uncle = null;if (parent==gparent.left){uncle=gparent.right;if (uncle!=null&&isRed(uncle)){//情景4.1
// 将父亲结点和叔叔结点染成黑色,将祖父结点染成红色,并设置祖父结点为当前结点setBlack(parent);setBlack(uncle);setRed(gparent);insertFlxUp(gparent);return;}if (uncle==null || isBlack(uncle)){//情景4.2if (node==parent.left){//情景4.2.1(LL)
// 将父亲结点染成黑色,将祖父结点染成红色进行右旋setBlack(parent);setRed(gparent);rightRotate(gparent);return;}if (node==parent.right){//情景4.2.2(LR)
// 以父亲结点进行左旋,按照4.2.1情景处理leftRotate(parent);insertFlxUp(parent);return;}}}else {uncle=gparent.left;if (uncle!=null&&isRed(uncle)){//情景4.1
// 将父亲结点和叔叔结点染成黑色,将祖父结点染成红色,并设置祖父结点为当前结点setBlack(parent);setBlack(uncle);setRed(gparent);insertFlxUp(gparent);return;}//情景4.3if (node==parent.left){//情景4.3.1(RL)
// 以父亲结点进行右旋,按4.3.2情景处理rightRotate(parent);insertFlxUp(parent);return;}if (node==parent.right){//情景4.3.2(RR)
// 将父亲结点染成黑色,将祖父结点染成红色,进行左旋setBlack(parent);setRed(gparent);leftRotate(gparent);return;}}}}/*** 左旋都做了哪些事* p p* | |* x y* / \ ----> / \* lx y x ry* / \ / \* ly ry lx ly* 1.将y的左子结点的父结点更新为x,将x的右子节点更新为y的左子结点* 2.当x的父节点不为空时,更新y的父节点为x的父节点,并将x的父节点的指定子树(当前x的子树位置指定为y* 3.将x的父节点更新为y,将y的左子结点更新为x*/public void leftRotate(RBNode x){RBNode y = x.right;
// 1.将y的左子结点的父结点更新为x,将x的右子节点更新为y的左子结点x.right=y.left;if (y.left!=null){y.left.parent=x;}
// 2.当x的父节点不为空时,更新y的父节点为x的父节点,并将x的父节点的指定子树(当前x的子树位置指定为yif (x.parent!=null){y.parent=x.parent;if (x == x.parent.left){x.parent.left=y;}else {x.parent.right=y;}}else {//说明x是rootthis.root=y;this.root.parent=null;}
// 3.将x的父节点更新为y,将y的左子结点更新为xx.parent=y;y.left=x;}/*** 右旋都做了哪些事* p p* | |* y x* / \ ----> / \* x ry lx y* / \ / \* lx ly ly ry** 1.将y的左子结点更新为x的右子节点,如果x的右子节点不为空,将x的右子节点的父节点更新为y* 2.当y的父节点不为空时,将x的父节点更新为y的父节点,将y的父节点的指定子树更新为x* 3.将y的父节点更新为x,将x的右子节点更新为y*/public void rightRotate(RBNode y){RBNode x = y.left;
// 1.将y的左子结点更新为x的右子节点,如果x的右子节点不为空,将x的右子节点的父节点更新为yy.left =x.right;if (x.right!=null){x.right.parent=y;}
// 2.当y的父节点不为空时,将x的父节点更新为y的父节点,将y的父节点的指定子树更新为xif (y.parent!=null){x.parent=y.parent;if (y == y.parent.left){y.parent.left=x;}else {y.parent.right=x;}}else {//y为根节点this.root=x;this.root.parent=null;}
// 3.将y的父节点更新为x,将x的右子节点更新为yy.parent=x;x.right=y;}//创建一个静态内部类,存放树节点static class RBNode<K extends Comparable<K>, V>{private RBNode parent;private RBNode left;private RBNode right;private boolean color;private K key;private V value;public RBNode() {}public RBNode(RBNode parent, RBNode left, RBNode right, boolean color, K key, V value) {this.parent = parent;this.left = left;this.right = right;this.color = color;this.key = key;this.value = value;}public RBNode getParent() {return parent;}public void setParent(RBNode parent) {this.parent = parent;}public RBNode getLeft() {return left;}public void setLeft(RBNode left) {this.left = left;}public RBNode getRight() {return right;}public void setRight(RBNode right) {this.right = right;}public boolean isColor() {return color;}public void setColor(boolean color) {this.color = color;}public K getKey() {return key;}public void setKey(K key) {this.key = key;}public V getValue() {return value;}public void setValue(V value) {this.value = value;}}}
为了更直观的看到红黑树的结构,我们从网上找到一个工具类**TreeOperation.java**,他可以将红黑树打印出来
package HashMapProject;// TreeOperation.java
public class TreeOperation {/*树的结构示例:1/ \2 3/ \ / \4 5 6 7*/// 用于获得树的层数public static int getTreeDepth(RBTree.RBNode root) {return root == null ? 0 : (1 + Math.max(getTreeDepth(root.getLeft()), getTreeDepth(root.getRight())));}private static void writeArray(RBTree.RBNode currNode, int rowIndex, int columnIndex, String[][] res, int treeDepth) {// 保证输入的树不为空if (currNode == null) return;// 先将当前节点保存到二维数组中res[rowIndex][columnIndex] = String.valueOf(currNode.getKey()+"-"+(currNode.isColor()?"R":"B")+"");// 计算当前位于树的第几层int currLevel = ((rowIndex + 1) / 2);// 若到了最后一层,则返回if (currLevel == treeDepth) return;// 计算当前行到下一行,每个元素之间的间隔(下一行的列索引与当前元素的列索引之间的间隔)int gap = treeDepth - currLevel - 1;// 对左儿子进行判断,若有左儿子,则记录相应的"/"与左儿子的值if (currNode.getLeft() != null) {res[rowIndex + 1][columnIndex - gap] = "/";writeArray(currNode.getLeft(), rowIndex + 2, columnIndex - gap * 2, res, treeDepth);}// 对右儿子进行判断,若有右儿子,则记录相应的"\"与右儿子的值if (currNode.getRight() != null) {res[rowIndex + 1][columnIndex + gap] = "\\";writeArray(currNode.getRight(), rowIndex + 2, columnIndex + gap * 2, res, treeDepth);}}public static void show(RBTree.RBNode root) {if (root == null) System.out.println("EMPTY!");// 得到树的深度int treeDepth = getTreeDepth(root);// 最后一行的宽度为2的(n - 1)次方乘3,再加1// 作为整个二维数组的宽度int arrayHeight = treeDepth * 2 - 1;int arrayWidth = (2 << (treeDepth - 2)) * 3 + 1;// 用一个字符串数组来存储每个位置应显示的元素String[][] res = new String[arrayHeight][arrayWidth];// 对数组进行初始化,默认为一个空格for (int i = 0; i < arrayHeight; i ++) {for (int j = 0; j < arrayWidth; j ++) {res[i][j] = " ";}}// 从根节点开始,递归处理整个树// res[0][(arrayWidth + 1)/ 2] = (char)(root.val + '0');writeArray(root, 0, arrayWidth/ 2, res, treeDepth);// 此时,已经将所有需要显示的元素储存到了二维数组中,将其拼接并打印即可for (String[] line: res) {StringBuilder sb = new StringBuilder();for (int i = 0; i < line.length; i ++) {sb.append(line[i]);if (line[i].length() > 1 && i <= line.length - 1) {i += line[i].length() > 4 ? 2: line[i].length() - 1;}}System.out.println(sb.toString());}}
}
对代码进行测试
package HashMapProject;import java.util.Scanner;/*** Description* User:* Date:* Time:*/
public class RBTreeTest {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);RBTree<String, Object> rbt = new RBTree<>();while (true){System.out.println("请输入key:");String key = scanner.next();System.out.println();rbt.insert(key,null);TreeOperation.show(rbt.getRoot());}}
}
请输入key:
11-B
请输入key:
21-B \ 2-R
请输入key:
32-B / \ 1-R 3-R
请输入key:
42-B / \ 1-B 3-B\ 4-R
请输入key:
52-B / \ 1-B 4-B/ \ 3-R 5-R
请输入key:
62-B / \ 1-B 4-R / \ 3-B 5-B\ 6-R
请输入key:
72-B / \ 1-B 4-R / \ 3-B 6-B/ \ 5-R 7-R
请输入key:
84-B / \ 2-R 6-R / \ / \ 1-B 3-B 5-B 7-B\ 8-R
请输入key:
94-B / \ 2-R 6-R / \ / \ 1-B 3-B 5-B 8-B/ \ 7-R 9-R
请输入key:
HashMap底层红黑树原理(超详细图解)+手写红黑树代码相关推荐
- 增量式编码器工作原理超详细图解
旋转编码器是由光栅盘(又叫分度码盘)和光电检测装置(又叫接收器)组成.光栅盘是在一定直径的圆板上等分地开通若干个长方形孔.由于光栅盘与电机同轴,电机旋转时,光栅盘与电机同速旋转,发光二极管垂直照射光栅 ...
- 超详细图解!【MySQL进阶篇】存储过程,视图,索引,函数,触发器
超详细图解![MySQL进阶篇]存储过程,视图,索引,函数,触发器 1.1 下载Linux 安装包 1.2 安装MySQL 1.3 启动 MySQL 服务 1.4 登录MySQL 2\. 索引 2.1 ...
- 超大超详细图解,让你掌握Spark memeoryStore内存管理的精髓
摘要:memoryStore主要是将没有序列化的java对象数组或者序列化的byteBuffer放到内存中. 本文分享自华为云社区<spark到底是怎么确认内存够不够用的?超大超详细图解!让你掌 ...
- 计算机网络之交换机的工作原理---超详细解析,谁都看得懂!!
在了解交换机的工作原理之前,我们先要了解几个概念. 一.相关概念 1.OSI七层模型是哪七层? 自上而下分别是: 应用层 表示层 会话层 传输层 网络层 数据链路层 物理层 交换机工作在数据链路层, ...
- 超级详细的手写webpack4配置来启动vue2项目(附配置作用)
基础目录结构以及各个文件的作用 初始npm项目 npm init 一路回车,一律使用默认的npm项目配置 package.json修改scripts 如下: {"name": &q ...
- 红黑树效率为甚恶魔是log_一文带你彻底读懂红黑树(附详细图解)
红黑树简介 红黑树是一种自平衡的二叉查找树,是一种高效的查找树.它是由 Rudolf Bayer 于1972年发明,在当时被称为对称二叉 B 树(symmetric binary B-trees).后 ...
- HashMap底层实现及原理
注意:文章的内容基于JDK1.7进行分析.1.8做的改动文章末尾进行讲解. 一.先来熟悉一下我们常用的HashMap: 1.HashSet和HashMap概述 对于HashSst及其子类而 ...
- 【Java基础】HashMap底层数据结构及其原理
1.简单了解一下HashMap HashMap 就是以 Key-Value 键值对的方式进行数据存储的一种数据结构,它在 JDK 1.7 和 JDK 1.8 中底层数据结构是有些不一样的.简单来说,J ...
- 数据结构 —— 单链表(超详细图解 接口函数实现)
系列文章目录 数据结构 -- 顺序表 数据结构 -- 单链表 数据结构 -- 双向链表 数据结构 -- 队列 数据结构 -- 栈 数据结构 -- 堆 数据结构 -- 二叉树 数据结构 -- 八大排序 ...
最新文章
- oracle 序列号同步,关于序列同步的问题
- Linux Cp 忽略报错信息,linux使用cp报错 Text file busy
- boost::multiprecision模块将 std::numeric_limits 用作 multiprecision.qbk 上的多精度文档片段的示例
- 服务器遭受攻击后,这样排查处理不背锅!
- shields 徽标_我的徽标素描过程
- java调用命令行命令_Java命令行界面(第4部分):命令行
- 酷狗音乐linux版_让父母也爱上音乐 酷狗音乐大字版成中老年人新宠
- Vue系列vue-router的配置使用(一)
- OpenCV中膨胀和腐蚀结构元素的创建
- 新建网站了!Github标星过万的吴恩达机器学习、深度学习课程笔记,《统计学习方法》代码实现,可以在线阅读了!...
- 【Django 2021年最新版教程20】python for循环遍历queryset
- amd核芯显卡控制面板自定义分辨率_显卡是哪个
- PowerDesigner中通过VBS脚本修改模型信息(转)
- 概率论的基本概念——《概率论与数理统计》第一章学习报告
- 《学习之道》第十章学习方法28再次提到番茄时间
- 基于分形理论和空间技术的耕地景观变化和用地质量研究
- 为什么打印还要另存为_为什么打印图片时出现文件另存为
- 数据库—Mysql相关
- 网易测试工程师笔经面经(已上岸)
- 短信系统实现原理(基于redis)