赞
踩
在看完了小刘老师和黑马的源码视频之后,我整理了一篇HashMap的底层源码文章,学海无涯,这几天看了对红黑树的讲解,故将其整理出来
视频链接
小刘老师讲源码
传智播客-黑马程序员
树(tree)是一种抽象数据类型(ADT),用来模拟具有树状结构性质的数据集合,它是由n(n>0)个有限节点通过连接它们的边组成一个具有层次关系的集合。把它叫做“树”是因为他看起来像一颗倒挂的树,也就是说它是根朝上,而叶向下的。
树有很多种,向上面的一个节点有多余两个的子节点的树,称为多路树,而每个节点最多只能有两个子节点的一种形式称为二叉树
路径:顺着节点的边从一个节点走到另一个节点,所经过的节点的顺序排列就称为‘路径’
根:树顶端的节点称为根,一棵树只有一个根,如果要把一个节点和边的集合称为树,那么从根到其他任何一个节点都必须有且只有一条路径,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树有什么特点?
平衡树基于这种特点就可以保证不会出现大量节点偏向于一边的情况了(插入或删除时,会发生左旋,右旋操作,使这棵树再次左右保持一定的平衡)
虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在O(logn),不过却不是最佳的
因为平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋
和右旋
来进行调整,使之再次称为一颗符合要求的平衡树
显然,如果在那种插入删除很频繁的场景中,平衡树需要频繁的进行调整,这会使平衡树的性能大打折扣,为了解决这个问题,便有了红黑树
红黑树属于二叉搜索树的一个特殊分支
红黑树并不是一个完美平衡二叉查找树
,从图上可以看到,根节点P的左子树显然比右子树高
但左子树和右子树的黑结点的层数是相等的,也即任意一个节点到每个叶子结点的路径都包含数量相同的黑结点
所以我们叫红黑树这种平衡为黑色完美平衡
前面讲到红黑树能自平衡,它靠的是什么?
三种操作:左旋
,右旋
和变色
左旋图示:
右旋图示:
因为红黑树是一颗二叉平衡树,而且查找不会破坏树的平衡,所以查找跟二叉平衡树无异
例如在此红黑树中查找指定元素:
查找50
50<80,找到左子结点40
50>40,找到40的右子结点60
50<60,找到60的左子结点50
插入操作包括两部分工作
注意:插入结点,必须为红色,理由很简单,红色在父结点(如果存在)为黑色结点时,红黑树的黑色平衡没被破坏,不需要做自平衡操作,但如果插入结点是黑色,那么插入位置所在的子树黑色结点总是多1,破坏了红黑树的黑高性质,必须做自平衡。
在开始每个情景的讲解前,我们还是先来约定下
最简单的一种情景,直接把插入结点作为根节点就行
注意,根据红黑树性质2,:根节点是黑色。还需要把插入结点设为黑色
处理:更新当前节点的值,为插入结点的值
由于插入的结点是红色的,当插入结点的父结点为黑色时,并不会影响红黑树的平衡,直接插入即可,无需做自平衡
再次回想下红黑树的性质2:根节点是黑色,如果插入结点的父结点为红结点,那么该父结点不可能为根节点,所以插入结点总是存在祖父结点,这一点很关键,因为后续的旋转操作肯定需要祖父结点的参与
依据红黑树性质4可知。红色结点不能相连=>祖父结点肯定为黑结点
因为不可以同时存在两个相连的红结点,那么此时该插入子树的红黑树层数的情况是:黑红红,显然最简单的处理方式是把其改为红黑红处理:
注意:单纯从插入前来看,叔叔结点非红即空(NIL结点),否则的话破坏了红黑树性质5,此路径会比其他路径多一个黑色结点。
处理:
处理:
该情景对应情景4.2,只是方向反转,直接看图
处理:
处理:
注:上图第一步图片内容错误,应该为右旋
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)){ //进入情景4 RBNode 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.2 if (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.3 if (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的子树位置指定为y if (x.parent!=null){ y.parent=x.parent; if (x == x.parent.left){ x.parent.left=y; }else { x.parent.right=y; } }else { //说明x是root this.root=y; this.root.parent=null; } // 3.将x的父节点更新为y,将y的左子结点更新为x x.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的右子节点的父节点更新为y y.left =x.right; if (x.right!=null){ x.right.parent=y; } // 2.当y的父节点不为空时,将x的父节点更新为y的父节点,将y的父节点的指定子树更新为x if (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的右子节点更新为y y.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: 1 1-B 请输入key: 2 1-B \ 2-R 请输入key: 3 2-B / \ 1-R 3-R 请输入key: 4 2-B / \ 1-B 3-B \ 4-R 请输入key: 5 2-B / \ 1-B 4-B / \ 3-R 5-R 请输入key: 6 2-B / \ 1-B 4-R / \ 3-B 5-B \ 6-R 请输入key: 7 2-B / \ 1-B 4-R / \ 3-B 6-B / \ 5-R 7-R 请输入key: 8 4-B / \ 2-R 6-R / \ / \ 1-B 3-B 5-B 7-B \ 8-R 请输入key: 9 4-B / \ 2-R 6-R / \ / \ 1-B 3-B 5-B 8-B / \ 7-R 9-R 请输入key:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。