之前讲的HashMap机制遗漏了一个Tree的操作,我们在这里补上。如果是从头看到这里那么这一章也会非常容易。
后续讲解内容为源码实现,这里使用的是JDK8的版本。HashMap使用的树结SEO靠我构是红黑树,而红黑树是一个平衡二叉树,节点都是按某种规则有序存储的,红黑树的特点就是有以下几点:
每个结点不是红色就是黑色根节点是黑色的如果一个结点是红色的,则它的两个孩子结点是黑色的(节点与孩子节点不SEO靠我能是两个红色,即一线不能有两红)对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点每个叶子结点都是黑色的(此处的叶子结点指的是空结点)我们上面的图就是一个常见的红黑树结构,SEO靠我接下来我们根据图来理解一下规则。
第一点与第二点已经标注出来了
第三点我们可以看到节点3、7都是红色节点,而它的子节点都是黑色的
第四点我们使用了蓝色线与绿色线来算到根节点经过的黑色节点数,这里需要注意是叶SEO靠我子节点到根节点,或者根节点到叶子节点
第五点我们用了大写字母N来表示为空节点(NIL)
除了这些之外要注意的是插入的节点一定是红色当然除了这些还有中序遍历、左旋、右旋、节点变色的概念,这些都超级复杂(指记SEO靠我住公式)需要记住一些公式。就像魔方一样我们有公式在某一个步骤使用一个公式就能回复一面颜色,数学的某一题使用一个公式就能解出结果一样,这些都是经过了研究与超多案例校验提取出来的稳定公式,我们只需要套用即SEO靠我可。
根据上面的图与简单介绍,我们知道了红黑树的有顺序的,那么我们要顺序遍历出每个节点应该怎么遍历呢?我们通过一个图来看一下,这里简单理解就可以了。
文字描述
从根节点开始遍历,所以当前处理节点就是SEO靠我跟节点,如果左边有节点不为空就先遍历左边节点 (递归)。
这是当前处理节点就是左边节点,再次判断是否有左边节点不为空,如果有就循环拿左边节点处理(递归)。
一直到左边节点为空,这时的当前节点就是开头最小的SEO靠我节点了。处理完左边节点之后,当前节点就是下一个值。
接下来查看当前节点的右边节点是否有内容,有就把当前节点设置为右节点(递归),然后根据上面步骤进行处理。
右边节点也到空了返回处理父节点了(递归)左右旋转与节点变色是对不符合红黑树规则进行调整的基础手段,节点变色如名称只要改一个属性就可以了,当时左右旋转是对一个子树的变化,接下来我们分两种情况看一下是如何旋转的。
上面的图片就是左旋操作,文字描述SEO靠我
当前节点是1,将右边节点3的左边节点2赋值给当前节点1。
将右边节点3的左边节点赋值为当前节点1
并维护好各自的父节点值上面的图片就是左旋操作,文字描述
当前节点是7,将左边节点4的右边节点5赋值给当前节点SEO靠我7的左边节点。
将左边节点4的右边节点赋值为当前节点7
并维护好各自的父节点值上面看完了一些基本的红黑树操作之后我们就可以来看红黑树是怎么维护节点的。
首先需要明确以上几个概念,接下来我们SEO靠我列举插入的几种情况分析:
当前节点插入是根节点位置 -> 当前节点为黑色当前节点插入位置的父节点是黑色 -> 无冲突,不需要维护当前节点插入位置的父节点是红色 -> 冲突,需要维护
3.1 情况一、叔叔节SEO靠我点是红色
3.1.1 先将父节点变黑3.1.2 将爷爷节点变为红
3.1.3 将叔叔节点变为黑
3.1.4 让爷爷节点为当前节点往上做判断
3.2 情况二、叔叔节点是黑色或为空
3.2.1 当前插入节点在父节点SEO靠我右边,并且双红冲突,这里是L(left)R(right)双红冲突,将它转换为LL双红冲突,那么当前节点就是节点3了
3.2.2 LR冲突或者LL冲突都进入这里。先让爷爷节点右旋
3.2.3 最后节点变色即SEO靠我可
插入的树维护到这里就看完了,接下来了删除维护,我们也分几种情况来分析:
删除节点为叶子节点,且为红色(这里删除50)
删除节点为叶子节点,且为黑色,这里分几种情况,大家可以根据红黑树的性质判断删除后违反SEO靠我红黑树的哪几点规则。
2.1.兄弟节点B也为黑色,兄弟节点没有子节点。2.2. 兄弟节点也为黑色,兄弟节点的左子节点为红色。
2.3. 兄弟节点也为黑色,兄弟节点的右子节点为红色。2.4. 兄弟节点也为黑SEO靠我色,兄弟节点的左右子节点都为红色。
2.5. 兄弟节点为红色,兄弟节点的左右子节点都为黑色。
3. 删除节点只有左子节点,只有左子节点。
4. 删除节点只有左子节点,只有右子节点,与上面步骤三一样处理。
5.SEO靠我 删除节点有两个子节点。
到这里就讲完了维护红黑树的方式,是不是感觉非常麻烦的!这里可以感受到我们很多东西都是站在巨人的肩膀上进行的,免去了我们很多的步骤,那么说回来这些我理解的方法不一定都是完美的,只SEO靠我要我们按从小到大的解决方式,可能大家能找到更好的解决方法!
这里给大家找了一篇红黑树删除节点后维护的文章,其实我写完这一段觉得描述不太完美并且不够清晰的,可能是文笔有限吧。所以找了一篇我认为非常详细且容SEO靠我易理解的文章给大家!枫铃树的红黑树详解(下)
最后这里有一个网站推荐给大家,可以自己试一下动态添加删除节点时红黑树是怎样维护的。红黑树在看源码之前我们来思考一下,我们之前学过数组与链表的SEO靠我数据结构,那么树和这两个比有什么特点呢?有什么优势呢?
首先我们对比数组与链表可以知道,数组插入麻烦但是查找非常快,而链表插入很简单但查找非常慢。那么树呢?树是链表的变体实现,所以插入很快,那么为了克服SEO靠我查找慢的问题就引入了二分查找法的实现,把每个节点都分成两半,这样每次查找都能快速丢弃不需要查找的那一半。所以它的查找速度也是非常快的。
ok,那进入源码环节,先来看看TreeNode中定义的成员。
// SEO靠我父节点 TreeNode<K,V> parent;// 左边节点 TreeNode<K,V> left;// 右边节点 TreeNode<K,V> riSEO靠我ght;// 上一个节点 TreeNode<K,V> prev; // 红黑节点 boolean red;可以简单的看出是一个树的结构了,接下来我们回顾一下树化的条件:SEO靠我 table某个插槽长度达到阈值(8)就进行树化,但是这样描述就是准确的了吗?
// tab就是存储的那个表格 // hash为当前插入插槽的hash final voiSEO靠我d treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;// 如果tab为空 或者 table的个数小于这个常量值就直接扩SEO靠我容不进行树化// static final int MIN_TREEIFY_CAPACITY = 64 if (tab == null || (n = tab.length) < MIN_TREEIFSEO靠我Y_CAPACITY)resize();// 拿到需要树化的那个插槽,转换为树节点然后进行树化操作。else if ((e = tab[index = (n - 1) & hash]) != nullSEO靠我) {TreeNode<K,V> hd = null, tl = null;do {TreeNode<K,V> p = replacementTreeNode(e, null);if (tl == nSEO靠我ull)hd = p;else {p.prev = tl;tl.next = p;}tl = p;} while ((e = e.next) != null);if ((tab[index] = hdSEO靠我) != null)hd.treeify(tab);}}看来我们这里树化的条件处理插槽的长度大于等于阈值(8)之外,table的长度还要大于等于另一个阈值(64)。
上面的代码就是左旋转的代码了,结合注释与旋转后的结果图对比看源码十分简单。
// pp pp SEO靠我 // | | // p l // / \ -> / \ // l r ll p // / \ / \ // llSEO靠我 lr lr r static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,TreeNode<K,V> p) {// p 是要SEO靠我旋转的节点// root 根节点TreeNode<K,V> l, pp, lr;// 拿到需要旋转后做父亲的节点if (p != null && (l = p.left) != null) {// 左SEO靠我节点的右节点旋转后变为当前节点的左边节点 p->lrif ((lr = p.left = l.right) != null)lr.parent = p;// 旋转后p节点变为l节点的右子节点,那么l节SEO靠我点就是父节点,维护l与p节点父节点的关系// 如果p的父节点是根节点,说明旋转后l节点就是根节点if ((pp = l.parent = p.parent) == null)(root = l).reSEO靠我d = false;// 不是根节点就确定是在爷爷节点的左边还是右边进行赋值维护父子关系else if (pp.right == p)pp.right = l;elsepp.left = l;// 维SEO靠我护指针关系l.right = p;p.parent = l;}return root; }看完左旋转再看右旋转的代码就是反过来而已,结合注释与旋转后的结果图对比看源码十分简单。
删除节点维护树这个比较难一点会往上处理,所以需要先掌握红黑树的维护再来看代码。
以上就是树化的代码了,里面有个比较重要的SEO靠我方法balanceInsertion(root, x);里面实现了插入维护红黑树的方法。
// 遍历节点组成链表返回头节点。 final Node<K,V> untreeify(HashSEO靠我Map<K,V> map) {Node<K,V> hd = null, tl = null;for (Node<K,V> q = this; q != null; q = q.next) {Node<SEO靠我K,V> p = map.replacementNode(q, null);if (tl == null)hd = p;elsetl.next = p;tl = p;}return hd; SEO靠我 }小case
和树化的方法差不多,主要是前面操作理解后这里就会超级简单。
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] SEO靠我tab,boolean movable) {int n;if (tab == null || (n = tab.length) == 0)return;int index = (n - 1) & haSEO靠我sh;TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;TreeNode<K,V> succ = (TreeNode<SEO靠我K,V>)next, pred = prev;// 删除的节点没有上一个节点,说明是根节点。if (pred == null)tab[index] = first = succ;// 移除节点elseSEO靠我pred.next = succ;if (succ != null)succ.prev = pred;// 空if (first == null)return;// 获取根节点if (root.parSEO靠我ent != null)root = root.root();if (root == null|| (movable&& (root.right == null|| (rl = root.left) SEO靠我== null|| rl.left == null))) {// 有一边可能为空,节点数太小了 tab[index] = first.untreeify(map); return;}TreeNode<SEO靠我K,V> p = this, pl = left, pr = right, replacement;// 移除节点有两个子节点if (pl != null && pr != null) {// 找到后SEO靠我继节点TreeNode<K,V> s = pr, sl;while ((sl = s.left) != null) s = sl;// 获取后继节点的颜色// 后继节点变色为删除节点的颜色// 删除节SEO靠我点的颜色变为后继节点的颜色boolean c = s.red; s.red = p.red; p.red = c;// sr = 后继节点的右子节点// pp = 当前节点的父节点TreeNode<KSEO靠我,V> sr = s.right;TreeNode<K,V> pp = p.parent;// 如果后继节点就是删除节点的右子节点就调换后继节点与父节点的位置if (s == pr) { p.pareSEO靠我nt = s;s.right = p;}// 后继节点在更深处else {TreeNode<K,V> sp = s.parent;// 将当前节点移动到后继节点的位置if ((p.parent = sSEO靠我p) != null) {// 后继节点如果是左节点if (s == sp.left)// 删除节点转移到左边sp.left = p;elsesp.right = p;}// 转移后后继节点的右子节点SEO靠我就是删除节点的右子节点咯if ((s.right = pr) != null)pr.parent = s;}// 调整删除节点p与后继节点s的指针p.left = null;if ((p.right SEO靠我= sr) != null)sr.parent = p;if ((s.left = pl) != null)pl.parent = s;if ((s.parent = pp) == null)rootSEO靠我 = s;else if (p == pp.left)pp.left = s;elsepp.right = s;if (sr != null)replacement = sr;elsereplacemSEO靠我ent = p;}// 删除节点只有一个左子节点else if (pl != null)replacement = pl;// 删除节点只有一个右子节点else if (pr != null)replSEO靠我acement = pr;// 删除节点就是叶子节点elsereplacement = p;// 删除节点位置后续还有子节点就会进入if (replacement != p) {// 真正移除删除节点SEO靠我并维护指针TreeNode<K,V> pp = replacement.parent = p.parent;if (pp == null)root = replacement;else if (p =SEO靠我= pp.left)pp.left = replacement;elsepp.right = replacement;p.left = p.right = p.parent = null;}// 如果SEO靠我删除节点是红的可以直接删除,如果是黑的需要维护树TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);// 没有后面节SEO靠我点就删掉if (replacement == p) {TreeNode<K,V> pp = p.parent;p.parent = null;if (pp != null) {if (p == pp.SEO靠我left)pp.left = null;else if (p == pp.right)pp.right = null;}}if (movable)moveRootToFront(tab, r); SEO靠我 }对比着红黑树规则与注释来看应该没什么问题。主要就是balanceDeletion(root, replacement)这个方法的调用。
到这里就把TreeNode主要的方法都看完了,还SEO靠我有一些边边角角的逻辑就留给大家来补了。这个红黑树的逻辑确实很绕,只看是很难学明白的,需要自己画画图,然后根据红黑树规则想想我要怎样变化才可以维护好呢?根据这个规律是否能适用在相同情况但变化不同的树结构SEO靠我上。
网站备案号:浙ICP备17034767号-2