一步一步数据结构-红黑树

846 阅读9分钟

Tips:

  1. 红黑树是搜索二叉树的变种,可以先行理解AVLTree
  2. 根据多方资料查找,很多人说2-3树和2-3-4树有助于理解红黑树(个人未进行深入了解,有兴趣可以自行搜索相关资料,或者等我再一次诈尸) 红黑树是2-3-4树的一种等同。换句话说,对于每个2-3-4树,都存在至少一个数据元素是同样次序的红黑树。在2-3-4树上的插入和删除操作也等同于在红黑树中颜色翻转和旋转。这使得2-3-4树成为理解红黑树背后的逻辑的重要工具,这也是很多介绍算法的教科书在红黑树之前介绍2-3-4树的原因,尽管2-3-4树在实践中不经常使用。 (wiki)
  3. 红黑树与AVL树不同,不具备部分AVL树的特性,如:每个节点的左右节点的高度差值不超过1
  4. 关于为什么新增节点一定是红色,个人未找到合理解释(目前仅理解为红色可以避免部分需要修复的情况),可能会在2-3-4树中找到原因

demo代码
主要参考于TreeMap

性质

  1. 节点是红色或黑色。
  2. 根是黑色。
  3. 所有叶子都是黑色(叶子是NIL节点)。
  4. 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
  5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

节点颜色修复

旋转可以参考AVLTree中的介绍,但需注意在AVLTree中介绍的左旋与右旋的定义,本文中以方向定左右,上文以子树位置定左右。

插入

wiki百科中详细罗列了几种情况,此处我将会直接搬运,并且稍加个人理解,各位读者阅读时,请捎带“眼镜”以防被我的漏洞所误导。

  • 性质1和性质3总是保持着。
  • 性质4只在增加红色节点、重绘黑色节点为红色,或做旋转时受到威胁。
  • 性质5只在增加黑色节点、重绘红色节点为黑色,或做旋转时受到威胁。

关于这三点的理解,除了第一点外,剩余两点十分抽象,如果仅靠这个去尝试理解,需要自己手动模拟红黑树的插入。下面我将由wiki列出的5中情况代入,去阐述插入所遇到的问题。

注意:新增节点的插入,一定是叶子节点(在树的调整修复前,读者可以手动尝试,我将举简单例子)
插入: 1,5,2,7,4,6,9,3
红黑树例1

情形1:新节点N位于树的根上,没有父节点。在这种情形下,我们把它重绘为黑色以满足性质2。因为它在每个路径上对黑节点数目增加一,性质5匹配。

此处是指插入的新节点是树的根,因此只需要保证性质2`根是黑色`即可满足条件

情形2:新节点的父节点P是黑色,所以性质4没有失效(新节点是红色的)。在这种情形下,树仍是有效的。性质5也未受到威胁,尽管新节点N有两个黑色叶子子节点;但由于新节点N是红色,通过它的每个子节点的路径就都有同通过它所取代的黑色的叶子的路径同样数目的黑色节点,所以依然满足这个性质。

因为插入节点都是红色,因此并未破坏性质。

情形3:如果父节点P和叔父节点U二者都是红色,(此时新插入节点N做为P的左子节点或右子节点都属于情形3,这里右图仅显示N做为P左子的情形)则我们可以将它们两个重绘为黑色并重绘祖父节点G为红色(用来保持性质5)。现在我们的新节点N有了一个黑色的父节点P。因为通过父节点P或叔父节点U的任何路径都必定通过祖父节点G,在这些路径上的黑节点数目没有改变。但是,红色的祖父节点G可能是根节点,这就违反了性质2,也有可能祖父节点G的父节点是红色的,这就违反了性质4。为了解决这个问题,我们在祖父节点G上递归地进行情形1的整个过程。(把G当成是新加入的节点进行各种情形的检查)

情形3
注意:在余下的情形下,我们假定父节点P是其父亲G的左子节点。如果它是右子节点,情形4和情形5中的左和右应当对调。

情形4:父节点P是红色而叔父节点U是黑色或缺少,并且新节点N是其父节点P的右子节点而父节点P又是其父节点的左子节点。在这种情形下,我们进行一次左旋转调换新节点和其父节点的角色;接着,我们按情形5处理以前的父节点P以解决仍然失效的性质4。注意这个改变会导致某些路径通过它们以前不通过的新节点N(比如图中1号叶子节点)或不通过节点P(比如图中3号叶子节点),但由于这两个节点都是红色的,所以性质5仍有效。

情形4

情形5:父节点P是红色而叔父节点U是黑色或缺少,新节点N是其父节点的左子节点,而父节点P又是其父节点G的左子节点。在这种情形下,我们进行针对祖父节点G的一次右旋转;在旋转产生的树中,以前的父节点P现在是新节点N和以前的祖父节点G的父节点。我们知道以前的祖父节点G是黑色,否则父节点P就不可能是红色(如果P和G都是红色就违反了性质4,所以G必须是黑色)。我们切换以前的父节点P和祖父节点G的颜色,结果的树满足性质4。性质5也仍然保持满足,因为通过这三个节点中任何一个的所有路径以前都通过祖父节点G,现在它们都通过以前的父节点P。在各自的情形下,这都是三个节点中唯一的黑色节点。

情形5

删除

节点删除我们只需要考虑被删除的节点有一个子节点或者本身为叶子节点(叶子节点也可看做只有一个子节点),因被删除节点有两个子节点时,删除方案是使用前驱节点(左子树最大)或者后继节点(右子树最小)进行替换,然后删除替换节点(替换节点要么只有一个子节点,要么是叶子节点)。
情况一:删除红色节点,父节点、子节点都为黑色。不会破坏性质。
情况一
情况二:删除黑色节点,子节点为红色。子节点提升,颜色修复红转黑,满足红黑树性质。
情况二
情况三(四):删除黑色节点,子节点也为黑色(需要注意双子节点情况,删除节点为替换节点)
不再单独列出情况四
例2
需注意,删除中的例子无节点52

private void remove(Node<T> d) {

//该if判断即是将双子节点情况转为单子节点情况
if (d.left != null && d.right != null) {
// 左右节点皆不为空,将待删除节点元素赋值为后继节点元素
// 注意,此时未带来节点颜色
Node<T> s = successor(d);
d.element = s.element;
// 待删除节点指针指向后继节点
d = s;
}

Node<T> replacement = (d.left != null ? d.left : d.right);

if (replacement != null) {
// 左右节点至少有一个节点不为空
replacement.parent = d.parent;
if (d.parent == null) {
// 树仅有两个节点,d为根结点
root = replacement;
} else if (d == d.parent.left) {
// 待删除节点为父节点的左节点
d.parent.left = replacement;
} else {
// 待删除节点为父节点的右节点
d.parent.right = replacement;
}

// 断开待删除节点的所有链接
// 当待删除节点左右节点皆不为空时,此时d指向后继节点
d.left = d.right = d.parent = null;

// Fix replacement
// 当待删除节点为红色时,无需调整节点颜色
if (d.color == BLACK) {
// 此时原节点已被删除,replacement是替换后节点
fixAfterDeletion(replacement);
}
} else if (null == d.parent) {
// 当前树仅有根结点
root = null;
} else {
if (d.color == BLACK)
// 待删除节点为叶子节点
fixAfterDeletion(d);

if (d.parent != null) {
// 针对删除节点为叶子节点情况
if (d == d.parent.left)
d.parent.left = null;
else if (d == d.parent.right)
d.parent.right = null;
d.parent = null;
}
}

}

// 待删除节点已被删除替换后,进行颜色修复
private void fixAfterDeletion(Node<T> x) {
// 只有节点为黑色才需要修复
while (x != root && colorOf(x) == BLACK) {
// 左节点
if (x == leftOf(parentOf(x))) {
Node<T> sib = rightOf(parentOf(x));
// 兄弟节点为红色,自己是黑色,设定为情况1
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}

if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
// 兄弟节点的子节点都为黑色,设定为情况2
setColor(sib, RED);
x = parentOf(x);
} else {
if (colorOf(rightOf(sib)) == BLACK) {
// 兄弟节点的子节点只有右节点为黑色,设定为情况3
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
// 兄弟节点的右节点为红色,设定为情况4
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
} else { // symmetric
// 镜像对称
Node<T> sib = leftOf(parentOf(x));

if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateRight(parentOf(x));
sib = leftOf(parentOf(x));
}

if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
if (colorOf(leftOf(sib)) == BLACK) {
setColor(rightOf(sib), BLACK);
setColor(sib, RED);
rotateLeft(sib);
sib = leftOf(parentOf(x));
}
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(sib), BLACK);
rotateRight(parentOf(x));
x = root;
}
}
}

setColor(x, BLACK);
}

情况三/四
放在代码后是希望能先阅读代码再看过程,能更易理解