推荐一个数据结构可视化工具:
https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
https://visualgo.net/zh
什么是树
什么平衡二叉树
红黑树的规则
Java实现红黑树
Java中的hashMap中的红黑树的实现
数据结构- 树
树是一种非线性结构的数据结构,然后你要知道一大堆关于树的术语:度,叶子节点,根节点,父节点,子节点,深度,高度。就不深入了
平衡二叉树
二叉树:个人理解每个节点最多含有两个子树的树称为二叉树,左子节点的值小于父节点,右子节点的值大于父节点
平衡二叉树 (当且仅当任何节点的两棵子树的高度差不大于1的二叉树;):如果我们每次值插入到树的左子树上,假如树的高度为h,那么查询二叉树最多查询次数为h,平衡二叉树就是当任何一个叶节点的到根节点的高度之差为最大值为1(这个值我们也可以规定)
红黑树(RB-Tree)
红黑树在平衡二叉树的基础上给每个节点多加上一种颜色的属性 该属性可以为红或者黑
R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
红黑树的规则
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
注意:
(01) 特性(3)中的叶子节点,是只为空(NIL或null)的节点。
(02) 特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。
通过上面的定义,可以看到红黑树本质上还是一颗二叉查找树,所以,对红黑树的插入删除操作都可以分为两阶段来完成,首先,将红黑树看成一颗普通的二叉查找树完成插入删除操作,然后,通过旋转以及颜色调整来使得操作后的树满足红黑树的所有特性即可。
红黑树的基本操作: 左旋和右旋
左旋和右旋,关于节点的移动,Y节点我认为有4种方式去连接y节点
Y节点的左旋(逆时针)
Y节点逆时针旋转(左旋)Y节点就会和G节点会产生一个碰撞,因为二叉树的原因,右子树的节点是大于父节点的,所以G节点就会是Y的右子节点,Y左旋后成为Z的左节点.
右旋(节点顺时针旋转)
(120)节点顺时针,就会和(110 )有一个碰撞因为二叉树的原因,(110)会成为(120 )的左子节点
通过节点的左旋和右旋调整树,使得树能调整修正成二叉树
红黑树的基本操作 添加
如果我们往一个红黑树中添加一个节点,为了不破坏红黑的特性,那添加规则呢,如果添加的节点破坏了原有的红黑树特性了,红黑树如何去调整呢
- 首先,将红黑树当作一颗二叉查找树,将节点插入(小的放左子树,大的放右子树);
- 然后,将节点着色为红色(默认添加的节点颜色属性为红色);
- 最后,通过旋转和重新颜色等方法来修正该树,使之重新成为一颗红黑树。详细描述如下:
调整策略
根据被插入节点的父节点的情况,可以将"当节点被着色为红色节点,并插入二叉树"划分为三种情况来处理。
① 情况说明:被插入的节点是根节点。
处理方法:直接把此节点涂为黑色。
② 情况说明:被插入的节点的父节点是黑色。
处理方法:什么也不需要做。节点被插入后,仍然是红黑树。
③ 情况说明:被插入的节点的父节点是红色。
处理方法:那么,该情况与红黑树的“特性(5)”相冲突。这种情况下,被插入节点是一定存在非空祖父节点的;进一步的讲,被插入节点也一定存在叔叔节点(即使叔叔节点为空,我们也视之为存在,空节点本身就是黑色节点)。理解这点之后,我们依据"叔叔节点的情况",将这种情况进一步划分为3种情况(Case)。
被插入的节点的父节点是红色
这个3种情况,不是当我们遇到其中一种case并通过策略就能将树修正成一个红黑树,比如当我遇到case 1 ,调用策略,它可能导致case 3的情况发生。就相当我们代码中的
核心思路 :将红色的节点移到根节点;然后,将根节点设为黑色
(Case 1)叔叔是红色
(01) 将“父节点”设为黑色。
(02) 将“叔叔节点”设为黑色。
(03) 将“祖父节点”设为“红色”。
(04) 将“祖父节点”设为“当前节点”(红色节点);即,之后继续对“当前节点”进行操作。
插入节点(45)
(Case 2)叔叔是黑色,且当前节点是右孩子
(01) 将“父节点”作为“新的当前节点”。
(02) 以“新的当前节点”为支点进行左旋。
插入(45)节点
触发case 1 (50)(70)变黑 ;(60)变红 -->触发case 2 ;(40) 作为当前节
(Case 3)叔叔是黑色,且当前节点是左孩子
(01) 将“父节点”设为“黑色”。
(02) 将“祖父节点”设为“红色”。
(03) 以“祖父节点”为支点进行右旋。
红黑树的基本操作: 删除
将红黑树内的某一个节点删除。需要执行的操作依次是:
- 首先,将红黑树当作一颗二叉查找树,将该节点从二叉查找树中删除;
- 然后,通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。
调整策略
情况①:如果N没有孩子,且如果N是红色,直接删除N;如果X是黑色,则以N为当前节点进行下面策略旋转调色,最后删掉N
情况②:如果N只有一个孩子C,交换N和C的数值,再对新N进行删除。根据红黑树特性,此时N不可能为红色,因为红色节点要么没有孩子,要么有两个黑孩子。此时以新N为当前节点进行情况①的判断
情况③:如果N有两个孩子,则从后继中找到最小节点D,交换N和D的数值,再对新N进行删除。此时以新N为当前节点进行情况①或②的判断
旋转调色
(N=旋转调色的当前节点,P=N的父亲,W=N的兄弟,Nf=N的远侄子,Nn=N的近侄子)
情况1:N是根或者N是红色,则:直接将N设为黑色
情况2:N不是根且N是黑色,且W为红色,则:将W设为黑色,P设为红色,对P进行旋转(N为P的左子时进行左旋,N为P的右子时进行右旋),将情况转化为情况1、2、3、4、5
情况3:N不是根且N是黑色,且W为黑色,且W的左右子均为黑色,则:将W设为红色,将P设为当前节点进行旋转调色,将情况转化为情况1、2、3、4、5
情况4:N不是根且N是黑色,且W为黑色,且Nf为黑色,Nn为红色,则:交换W与Nn的颜色,并对W进行旋转(N为P的左子进行右旋,N为P的右子进行左旋),旋转后N的新兄弟W有一个红色WR,则转换为情况5
情况5:N不是根且N是黑色,且W为黑色,且Nf为红色,Nn为黑色,则:将W设为P的颜色,P和Nf设为黑色,并对P进行旋转(N为P的左子进行左旋,N为P的右子进行右旋),N设为根
删除节点(20)策略1 , 旋转调色的第4种情况,
第5种情况
太多了例子了,就选用一个简单的描述
Java实现红黑树
代码太多了:https://github.com/Hqinjun/DailyStudy/blob/master/src/main/java/com/hqinjun/Collections/BRTreeTest.java
JDK1.8 hashmap 中对红黑树的使用
推荐 https://www.cnblogs.com/finite/p/8251587.html
在hashmap的put 的方法中会对哈希表中链表的长度有一个判断大于8的时候会对链表转换成一个树
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
{
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else
{
Node<K, V> e;
K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断当前链表是否是树节点
else if (p instanceof TreeNode)
// 如果当前的bucket里面已经是红黑树的话,执行红黑树的添加操作
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
else
{
for (int binCount = 0;; ++binCount)
{
if ((e = p.next) == null)
{
p.next = newNode(hash, key, value, null);
// TREEIFY_THRESHOLD = 8,判断如果当前bucket的位置链表长度大于8的话就将此链表变成红黑树。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null)
{ // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
treeifyBin()方法 将链表树化,通过node节点来生产相应的treenode节点,这时只具备了tree的数据结构还没有开始修正成红黑树
final void treeifyBin(Node<K, V>[] tab, int hash)
{
int n, index;
Node<K, V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// resize()方法这里不过多介绍,感兴趣的可以去看上面的链接。
resize();
// 通过hash求出bucket的位置。
else if ((e = tab[index = (n - 1) & hash]) != null)
{
TreeNode<K, V> hd = null, tl = null;
do
{
// 将每个节点包装成TreeNode。
TreeNode<K, V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else
{
// 将所有TreeNode连接在一起此时只是链表结构。
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
// 对TreeNode链表进行树化。
hd.treeify(tab);
}
}
TreeNode是Hashmap类的静态内部类,
static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V>
{
TreeNode<K, V> parent; // red-black tree links
TreeNode<K, V> left;
TreeNode<K, V> right;
TreeNode<K, V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K, V> next)
{
super(hash, key, val, next);
}、
//其他方法省略
}
treeify:遍历数组节点,一个一个的插入到红黑书中,如果插入的节点破坏了原有的红黑树特性,就通过旋转变色来修正红黑树
final void treeify(Node<K, V>[] tab)
{
TreeNode<K, V> root = null;
// 以for循环的方式遍历刚才我们创建的链表。
for (TreeNode<K, V> x = this, next; x != null; x = next)
{
// next向前推进。
next = (TreeNode<K, V>) x.next;
x.left = x.right = null;
// 为树根节点赋值。
if (root == null)
{
x.parent = null;
x.red = false;
root = x;
} else
{
// x即为当前访问链表中的项。
K k = x.key;
int h = x.hash;
Class<?> kc = null;
// 此时红黑树已经有了根节点,上面获取了当前加入红黑树的项的key和hash值进入核心循环。
// 这里从root开始,是以一个自顶向下的方式遍历添加。
// for循环没有控制条件,由代码内break跳出循环。
for (TreeNode<K, V> p = root;;)
{
// dir:directory,比较添加项与当前树中访问节点的hash值判断加入项的路径,-1为左子树,+1为右子树。
// ph:parent hash。
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null && (kc = comparableClassFor(k)) == null)
|| (dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
// xp:x parent。
TreeNode<K, V> xp = p;
// 找到符合x添加条件的节点。
if ((p = (dir <= 0) ? p.left : p.right) == null)
{
x.parent = xp;
// 如果xp的hash值大于x的hash值,将x添加在xp的左边。
if (dir <= 0)
xp.left = x;
// 反之添加在xp的右边。
else
xp.right = x;
// 维护添加后红黑树的红黑结构。
root = balanceInsertion(root, x);
// 跳出循环当前链表中的项成功的添加到了红黑树中。
break;
}
}
}
}
// Ensures that the given root is the first node of its bin,自己翻译一下。
moveRootToFront(tab, root);
}
balanceInsertion:往树插入节点,判断插入的节点是否会破坏了原有的红黑树特性,因此需要通过旋转变色来修正红黑树,使其能重新满足红黑树特性
static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root, TreeNode<K, V> x)
{
// 正如开头所说,新加入树节点默认都是红色的,不会破坏树的结构。
x.red = true;
// 这些变量名不是作者随便定义的都是有意义的。
// xp:x parent,代表x的父节点。
// xpp:x parent parent,代表x的祖父节点
// xppl:x parent parent left,代表x的祖父的左节点。
// xppr:x parent parent right,代表x的祖父的右节点。
// 循环- 当前节点
for (TreeNode<K, V> xp, xpp, xppl, xppr;;)
{
// 如果x的父节点为null说明只有一个节点,该节点为根节点,根节点为黑色,red = false。
if ((xp = x.parent) == null)
{
x.red = false;
return x;
}
// 进入else说明不是根节点。
// 如果父节点是黑色,那么大吉大利(今晚吃鸡),红色的x节点可以直接添加到黑色节点后面,返回根就行了不需要任何多余的操作。
// 如果父节点是红色的,但祖父节点为空的话也可以直接返回根此时父节点就是根节点,因为根必须是黑色的,添加在后面没有任何问题。
else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 一旦我们进入到这里就说明了两件是情
// 1.x的父节点xp是红色的,这样就遇到两个红色节点相连的问题,所以必须经过旋转变换。
// 2.x的祖父节点xpp不为空。
// 判断如果父节点是否是祖父节点的左节点
if (xp == (xppl = xpp.left))
{
// 父节点xp是祖父的左节点xppr
// 判断祖父节点的右节点不为空并且是否是红色的
// 此时xpp的左右节点都是红的,所以直接进行上面所说的第三种变换,将两个子节点变成黑色,将xpp变成红色,然后将红色节点x顺利的添加到了xp的后面。
// 这里大家有疑问为什么将x = xpp?
// 这是由于将xpp变成红色以后可能与xpp的父节点发生两个相连红色节点的冲突,这就又构成了第二种旋转变换,所以必须从底向上的进行变换,直到根。
// 所以令x = xpp,然后进行下下一层循环,接着往上走。
if ((xppr = xpp.right) != null && xppr.red)
{
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
// 进入到这个else里面说明。
// 父节点xp是祖父的左节点xppr。
// 祖父节点xpp的右节点xppr是黑色节点或者为空,默认规定空节点也是黑色的。
// 下面要判断x是xp的左节点还是右节点。
else
{
// x是xp的右节点,此时的结构是:xpp左->xp右->x。这明显是第二中变换需要进行两次旋转,这里先进行一次旋转。
// 下面是第一次旋转。
if (x == xp.right)
{
//右旋转
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 针对本身就是xpp左->xp左->x的结构或者由于上面的旋转造成的这种结构进行一次旋转。
if (xp != null)
{
xp.red = false;
if (xpp != null)
{
xpp.red = true;
//左旋转
root = rotateRight(root, xpp);
}
}
}
}
// 这里的分析方式和前面的相对称只不过全部在右测不再重复分析。
else
{
if (xppl != null && xppl.red)
{
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
} else
{
if (x == xp.left)
{
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null)
{
xp.red = false;
if (xpp != null)
{
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}