如何轻松记住红黑树的插入和删除?
Posted
技术标签:
【中文标题】如何轻松记住红黑树的插入和删除?【英文标题】:How to easily remember Red-Black Tree insert and delete? 【发布时间】:2012-03-17 05:04:44 【问题描述】:完全理解标准二叉搜索树及其操作非常容易。因为有了这样的理解,我什至不需要记住那些插入、删除、搜索操作的实现。
我现在正在学习红黑树,我了解它保持树平衡的属性。但是我觉得很难理解它的插入和删除过程。
我了解在插入新节点时,我们将节点标记为红色(因为红色是我们能做的最好的事情,以避免破坏较少的红黑树法则)。新的红色节点仍有可能打破“无连续红色节点定律”。然后我们通过以下方式修复它:
检查其叔叔的颜色,如果是红色,则将其父母和叔叔标记为黑色,然后去祖父母。
如果是右孩子,左旋转其父母
将其父代标记为黑色,将其祖代标记为红色,然后向右旋转其祖代。
完成(基本上和上面一样)。
很多地方都像上面一样描述了红黑树的插入。他们只是告诉你如何去做。但是为什么这些步骤可以修复树呢?为什么先左旋,后右旋?
谁能比 CLRS 更清楚地向我解释为什么?旋转有什么魔力?
我真的很想明白,所以一年后,我可以自己实现红黑树而不用复习书。
谢谢
【问题讨论】:
您可以考虑将红黑树转换为相应的 2-3 btree 表示。 【参考方案1】:这是我的实现:
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
*
* @author Gaurav Ratnawat
* <p>
* Red Black Tree
* <p>
* Time complexity
* Insert - O(logn)
* Delete - O(logn)
* Search - O(logn)
* <p>
* Does not work for duplicates.
* <p>
* References
* http://pages.cs.wisc.edu/~skrentny/cs367-common/readings/Red-Black-Trees/
* https://en.wikipedia.org/wiki/Red%E2%80%93black_tree
*/
public class RedBlackTree
public enum Color
RED,
BLACK
public static class Node
int data;
Color color;
Node left;
Node right;
Node parent;
boolean isNullLeaf;
private static Node createBlackNode(int data)
Node node = new Node();
node.data = data;
node.color = Color.BLACK;
node.left = createNullLeafNode(node);
node.right = createNullLeafNode(node);
return node;
private static Node createNullLeafNode(Node parent)
Node leaf = new Node();
leaf.color = Color.BLACK;
leaf.isNullLeaf = true;
leaf.parent = parent;
return leaf;
private static Node createRedNode(Node parent, int data)
Node node = new Node();
node.data = data;
node.color = Color.RED;
node.parent = parent;
node.left = createNullLeafNode(node);
node.right = createNullLeafNode(node);
return node;
/**
* Main insert method of red black tree.
*/
public Node insert(Node root, int data)
return insert(null, root, data);
/**
* Main delete method of red black tree.
*/
public Node delete(Node root, int data)
AtomicReference<Node> rootReference = new AtomicReference<>();
delete(root, data, rootReference);
if (rootReference.get() == null)
return root;
else
return rootReference.get();
/**
* Main print method of red black tree.
*/
public void printRedBlackTree(Node root)
printRedBlackTree(root, 0);
/**
* Main validate method of red black tree.
*/
public boolean validateRedBlackTree(Node root)
if (root == null)
return true;
//check if root is black
if (root.color != Color.BLACK)
System.out.print("Root is not black");
return false;
//Use of AtomicInteger solely because java does not provide any other mutable int wrapper.
AtomicInteger blackCount = new AtomicInteger(0);
//make sure black count is same on all path and there is no red red relationship
return checkBlackNodesCount(root, blackCount, 0) && noRedRedParentChild(root, Color.BLACK);
private void rightRotate(Node root, boolean changeColor)
Node parent = root.parent;
root.parent = parent.parent;
if (parent.parent != null)
if (parent.parent.right == parent)
parent.parent.right = root;
else
parent.parent.left = root;
Node right = root.right;
root.right = parent;
parent.parent = root;
parent.left = right;
if (right != null)
right.parent = parent;
if (changeColor)
root.color = Color.BLACK;
parent.color = Color.RED;
private void leftRotate(Node root, boolean changeColor)
Node parent = root.parent;
root.parent = parent.parent;
if (parent.parent != null)
if (parent.parent.right == parent)
parent.parent.right = root;
else
parent.parent.left = root;
Node left = root.left;
root.left = parent;
parent.parent = root;
parent.right = left;
if (left != null)
left.parent = parent;
if (changeColor)
root.color = Color.BLACK;
parent.color = Color.RED;
private Optional<Node> findSiblingNode(Node root)
Node parent = root.parent;
if (isLeftChild(root))
return Optional.ofNullable(parent.right.isNullLeaf ? null : parent.right);
else
return Optional.ofNullable(parent.left.isNullLeaf ? null : parent.left);
private boolean isLeftChild(Node root)
Node parent = root.parent;
return parent.left == root;
private Node insert(Node parent, Node root, int data)
if (root == null || root.isNullLeaf)
//if parent is not null means tree is not empty
//so create a red leaf node
if (parent != null)
return createRedNode(parent, data);
else //otherwise create a black root node if tree is empty
return createBlackNode(data);
//duplicate insertion is not allowed for this tree.
if (root.data == data)
throw new IllegalArgumentException("Duplicate date " + data);
//if we go on left side then isLeft will be true
//if we go on right side then isLeft will be false.
boolean isLeft;
if (root.data > data)
Node left = insert(root, root.left, data);
//if left becomes root parent means rotation
//happened at lower level. So just return left
//so that nodes at upper level can set their
//child correctly
if (left == root.parent)
return left;
//set the left child returned to be left of root node
root.left = left;
//set isLeft to be true
isLeft = true;
else
Node right = insert(root, root.right, data);
//if right becomes root parent means rotation
//happened at lower level. So just return right
//so that nodes at upper level can set their
//child correctly
if (right == root.parent)
return right;
//set the right child returned to be right of root node
root.right = right;
//set isRight to be true
isLeft = false;
if (isLeft)
//if we went to left side check to see Red-Red conflict
//between root and its left child
if (root.color == Color.RED && root.left.color == Color.RED)
//get the sibling of root. It is returning optional means
//sibling could be empty
Optional<Node> sibling = findSiblingNode(root);
//if sibling is empty or of BLACK color
if (sibling.isEmpty() || sibling.get().color == Color.BLACK)
//check if root is left child of its parent
if (isLeftChild(root))
//this creates left left situation. So do one right rotate
rightRotate(root, true);
else
//this creates left-right situation so do one right rotate followed
//by left rotate
//do right rotation without change in color. So sending false.
//when right rotation is done root becomes right child of its left
//child. So make root = root.parent because its left child before rotation
//is new root of this subtree.
rightRotate(root.left, false);
//after rotation root should be root's parent
root = root.parent;
//then do left rotate with change of color
leftRotate(root, true);
else
//we have sibling color as RED. So change color of root
//and its sibling to Black. And then change color of their
//parent to red if their parent is not a root.
root.color = Color.BLACK;
sibling.get().color = Color.BLACK;
//if parent's parent is not null means parent is not root.
//so change its color to RED.
if (root.parent.parent != null)
root.parent.color = Color.RED;
else
//this is mirror case of above. So same comments as above.
if (root.color == Color.RED && root.right.color == Color.RED)
Optional<Node> sibling = findSiblingNode(root);
if (!sibling.isPresent() || sibling.get().color == Color.BLACK)
if (!isLeftChild(root))
leftRotate(root, true);
else
leftRotate(root.right, false);
root = root.parent;
rightRotate(root, true);
else
root.color = Color.BLACK;
sibling.get().color = Color.BLACK;
if (root.parent.parent != null)
root.parent.color = Color.RED;
return root;
/**
* Using atomicreference because java does not provide mutable wrapper. Its like
* double pointer in C.
*/
private void delete(Node root, int data, AtomicReference<Node> rootReference)
if (root == null || root.isNullLeaf)
return;
if (root.data == data)
//if node to be deleted has 0 or 1 null children then we have
//deleteOneChild use case as discussed in video.
if (root.right.isNullLeaf || root.left.isNullLeaf)
deleteOneChild(root, rootReference);
else
//otherwise look for the inorder successor in right subtree.
//replace inorder successor data at root data.
//then delete inorder successor which should have 0 or 1 not null child.
Node inorderSuccessor = findSmallest(root.right);
root.data = inorderSuccessor.data;
delete(root.right, inorderSuccessor.data, rootReference);
//search for the node to be deleted.
if (root.data < data)
delete(root.right, data, rootReference);
else
delete(root.left, data, rootReference);
private Node findSmallest(Node root)
Node prev = null;
while (root != null && !root.isNullLeaf)
prev = root;
root = root.left;
return prev != null ? prev : root;
/**
* Assumption that node to be deleted has either 0 or 1 non leaf child
*/
private void deleteOneChild(Node nodeToBeDelete, AtomicReference<Node> rootReference)
Node child = nodeToBeDelete.right.isNullLeaf ? nodeToBeDelete.left : nodeToBeDelete.right;
//replace node with either one not null child if it exists or null child.
replaceNode(nodeToBeDelete, child, rootReference);
//if the node to be deleted is BLACK. See if it has one red child.
if (nodeToBeDelete.color == Color.BLACK)
//if it has one red child then change color of that child to be Black.
if (child.color == Color.RED)
child.color = Color.BLACK;
else
//otherwise we have double black situation.
deleteCase1(child, rootReference);
/**
* If double black node becomes root then we are done. Turning it into
* single black node just reduces one black in every path.
*/
private void deleteCase1(Node doubleBlackNode, AtomicReference<Node> rootReference)
if (doubleBlackNode.parent == null)
rootReference.set(doubleBlackNode);
return;
deleteCase2(doubleBlackNode, rootReference);
/**
* If sibling is red and parent and sibling's children are black then rotate it
* so that sibling becomes black. Double black node is still double black so we need
* further processing.
*/
private void deleteCase2(Node doubleBlackNode, AtomicReference<Node> rootReference)
Node siblingNode = findSiblingNode(doubleBlackNode).get();
if (siblingNode.color == Color.RED)
if (isLeftChild(siblingNode))
rightRotate(siblingNode, true);
else
leftRotate(siblingNode, true);
if (siblingNode.parent == null)
rootReference.set(siblingNode);
deleteCase3(doubleBlackNode, rootReference);
/**
* If sibling, sibling's children and parent are all black then turn sibling into red.
* This reduces black node for both the paths from parent. Now parent is new double black
* node which needs further processing by going back to case1.
*/
private void deleteCase3(Node doubleBlackNode, AtomicReference<Node> rootReference)
Node siblingNode = findSiblingNode(doubleBlackNode).get();
if (doubleBlackNode.parent.color == Color.BLACK && siblingNode.color == Color.BLACK && siblingNode.left.color == Color.BLACK
&& siblingNode.right.color == Color.BLACK)
siblingNode.color = Color.RED;
deleteCase1(doubleBlackNode.parent, rootReference);
else
deleteCase4(doubleBlackNode, rootReference);
/**
* If sibling color is black, parent color is red and sibling's children color is black then swap color b/w sibling
* and parent. This increases one black node on double black node path but does not affect black node count on
* sibling path. We are done if we hit this situation.
*/
private void deleteCase4(Node doubleBlackNode, AtomicReference<Node> rootReference)
Node siblingNode = findSiblingNode(doubleBlackNode).get();
if (doubleBlackNode.parent.color == Color.RED && siblingNode.color == Color.BLACK && siblingNode.left.color == Color.BLACK
&& siblingNode.right.color == Color.BLACK)
siblingNode.color = Color.RED;
doubleBlackNode.parent.color = Color.BLACK;
return;
else
deleteCase5(doubleBlackNode, rootReference);
/**
* If sibling is black, double black node is left child of its parent, siblings right child is black
* and sibling's left child is red then do a right rotation at siblings left child and swap colors.
* This converts it to delete case6. It will also have a mirror case.
*/
private void deleteCase5(Node doubleBlackNode, AtomicReference<Node> rootReference)
Node siblingNode = findSiblingNode(doubleBlackNode).get();
if (siblingNode.color == Color.BLACK)
if (isLeftChild(doubleBlackNode) && siblingNode.right.color == Color.BLACK && siblingNode.left.color == Color.RED)
rightRotate(siblingNode.left, true);
else if (!isLeftChild(doubleBlackNode) && siblingNode.left.color == Color.BLACK && siblingNode.right.color == Color.RED)
leftRotate(siblingNode.right, true);
deleteCase6(doubleBlackNode, rootReference);
/**
* If sibling is black, double black node is left child of its parent, sibling left child is black and sibling's right child is
* red, sibling takes its parent color, parent color becomes black, sibling's right child becomes black and then do
* left rotation at sibling without any further change in color. This removes double black and we are done. This
* also has a mirror condition.
*/
private void deleteCase6(Node doubleBlackNode, AtomicReference<Node> rootReference)
Node siblingNode = findSiblingNode(doubleBlackNode).get();
siblingNode.color = siblingNode.parent.color;
siblingNode.parent.color = Color.BLACK;
if (isLeftChild(doubleBlackNode))
siblingNode.right.color = Color.BLACK;
leftRotate(siblingNode, false);
else
siblingNode.left.color = Color.BLACK;
rightRotate(siblingNode, false);
if (siblingNode.parent == null)
rootReference.set(siblingNode);
private void replaceNode(Node root, Node child, AtomicReference<Node> rootReference)
child.parent = root.parent;
if (root.parent == null)
rootReference.set(child);
else
if (isLeftChild(root))
root.parent.left = child;
else
root.parent.right = child;
private void printRedBlackTree(Node root, int space)
if (root == null || root.isNullLeaf)
return;
printRedBlackTree(root.right, space + 5);
for (int i = 0; i < space; i++)
System.out.print(" ");
System.out.println(root.data + " " + (root.color == Color.BLACK ? "B" : "R"));
printRedBlackTree(root.left, space + 5);
private boolean noRedRedParentChild(Node root, Color parentColor)
if (root == null)
return true;
if (root.color == Color.RED && parentColor == Color.RED)
return false;
return noRedRedParentChild(root.left, root.color) && noRedRedParentChild(root.right, root.color);
private boolean checkBlackNodesCount(Node root, AtomicInteger blackCount, int currentCount)
if (root.color == Color.BLACK)
currentCount++;
if (root.left == null && root.right == null)
if (blackCount.get() == 0)
blackCount.set(currentCount);
return true;
else
return currentCount == blackCount.get();
return checkBlackNodesCount(root.left, blackCount, currentCount) && checkBlackNodesCount(root.right, blackCount, currentCount);
public static void main(String[] args)
Node root = null;
RedBlackTree redBlackTree = new RedBlackTree();
root = redBlackTree.insert(root, 10);
root = redBlackTree.insert(root, 15);
root = redBlackTree.insert(root, -10);
root = redBlackTree.insert(root, 20);
root = redBlackTree.insert(root, 30);
root = redBlackTree.insert(root, 40);
root = redBlackTree.insert(root, 50);
root = redBlackTree.insert(root, -15);
root = redBlackTree.insert(root, 25);
root = redBlackTree.insert(root, 17);
root = redBlackTree.insert(root, 21);
root = redBlackTree.insert(root, 24);
root = redBlackTree.insert(root, 28);
root = redBlackTree.insert(root, 34);
root = redBlackTree.insert(root, 32);
root = redBlackTree.insert(root, 26);
root = redBlackTree.insert(root, 35);
root = redBlackTree.insert(root, 19);
redBlackTree.printRedBlackTree(root);
root = redBlackTree.delete(root, 50);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, 40);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, -10);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, 15);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, 17);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, 24);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, 21);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, 32);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, 26);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, 19);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, 25);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, 17);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, -15);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, 20);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, 35);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, 34);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, 30);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, 28);
System.out.println(redBlackTree.validateRedBlackTree(root));
root = redBlackTree.delete(root, 10);
System.out.println(redBlackTree.validateRedBlackTree(root));
【讨论】:
【参考方案2】:这三个案例你真的做出了很好的结论。
在 RB-Tree 中插入后,如果有的话,还有一个主要问题需要解决。 有两个连续的红色节点!! 我们怎样才能让两个连续的红色节点消失而不违反该规则(每条路径都有相同的黑色节点) 所以我们看到两个节点,只存在3个circurm...
对不起,你可以看看算法教程教科书
没有人可以帮助您通过 rb-tree 进行思考。他们只能在某个关键点为您提供指导。
【讨论】:
【参考方案3】:任何 2-4 (2-3-4) 树都可以转换为红黑树。并且理解 2-4 棵树要容易得多。如果你只是在 2-4 棵树中进行插入和删除操作,你会觉得不需要记住任何规则来实现相同的操作。您将看到一个清晰简单的逻辑,使您能够提出解决方案来处理不同的插入和删除场景。
当你对 2-4 棵树有了清晰的认识之后,当你处理红黑树时,你可以在脑海中将这棵红黑树映射到 2-4 棵树,然后自己想出一个逻辑。
p>我发现以下几个视频对于理解 2-4 树、红黑树以及 2-4 树到红黑树的映射非常有用。我建议您浏览这些视频。
1) 对于 2-4 棵树:http://www.youtube.com/watch?v=JZhdUb5F7oY&list=PLBF3763AF2E1C572F&index=13
2) 对于红黑树:http://www.youtube.com/watch?v=JRsN4Oz36QU&list=PLBF3763AF2E1C572F&index=14
尽管每个视频都是一小时长的视频,但我觉得值得一读。
【讨论】:
【参考方案4】:为了其他阅读此主题但无法访问已接受答案中提到的书的人的利益,我希望这是一个可以接受的描述性答案。
旋转使树处于满足重新着色条件的状态(子节点有一个红色的叔叔)。有两个主要区别:
哪个节点是“孩子”,哪个节点是“叔叔”已经改变了; 不是将父母和叔叔重新着色为黑色,将祖父母重新着色为红色,而是将父母重新着色为红色,将祖父母重新着色为黑色。当子节点没有红色叔叔时,您必须旋转,因为如果叔叔节点已经是黑色,那么将父节点设为黑色只会在祖父节点的一侧将黑色高度增加 1。这将违反红黑树的高度不变性并使树不平衡。
现在让我们看看旋转如何变换树,以便我们有一个带有红色叔叔的子节点并且可以使用重新着色。我建议把它画出来以完全理解它。
设 x 为当前红色节点,其父节点为红色。 设 p 是 x 在旋转之前的红色父级(如果父级是黑色,我们已经完成了)。 让 y 成为 x 在旋转前的黑色叔叔(如果叔叔是红色,我们就不需要旋转。我们只需将父代和叔叔重新着色为黑色,将祖父母重新着色为红色)。 设 g 为 x 在旋转前的黑色祖父母(因为父母是红色的,祖父母必须是黑色的;否则这不是一棵红黑树。) 当您有左-左 (LL) 或右-右 (RR) 情况时(即 x 是 p 的左孩子,p 是 g 的左孩子或 x 是 p 和 p 的右孩子是 g 的右孩子),经过一次旋转(如果 LL 则为右,如果 RR 则为左),y 成为孩子,x 成为其叔叔。由于 x 是红色叔叔,因此您现在有一个可以重新着色的案例。因此,将孩子的父母(因为孩子现在是 y,它的父母是 g)重新着色为红色,将孩子的祖父母(现在是 p)重新着色为黑色。 当你有一个 LR(x 是左孩子或 p 并且 p 是 g 的右孩子)或 RL 案例(x 是 p 的右孩子,p 是 g 的左孩子)时,经过双重旋转(如果 LR 是右然后左,如果 RL 是左然后右),y 成为孩子,p 成为叔叔。由于 p 是红色叔叔,因此您又可以重新着色。因此,将父级(因为孩子现在是 y,其父级是 g)重新着色为红色,将孩子的祖父母(现在是 x)重新着色为黑色。在旋转和重新着色之前,您有一个黑色祖父母,A 面(左侧或右侧)有 2 个红色节点和 0 个黑色节点,B 面(A 面的对面)有 0 个红色节点和 1 个黑色节点。在旋转和重新着色之后,你有一个黑色的祖父母,A 侧有 1 个红色节点和 0 个黑色节点,B 侧有 1 个红色节点和 1 个黑色节点。所以你基本上将一个红色节点移动到另一个子树不增加任一子树的黑色高度的祖父母。
这就是旋转的魔力。它允许您将额外的红色节点移动到另一个分支而不改变黑色高度,并且仍然保留二叉搜索树的排序遍历属性。
【讨论】:
你没有提到删除。【参考方案5】:也许值得从left-leaning red black trees 开始。它们提供了一个有趣的简化实现。
【讨论】:
简化了维护树的代码。因此,您将插入和删除修正的代码减半。所以没有“如果左边做这个代码块,如果右边做这个镜像代码块”。但是没有说的是,你不可避免地会做更多的颜色变化和旋转来保持这个左倾不变性。正常插入最多 2 圈,删除最多 3 圈。这会增加。所以它并不简单。 @SJHowe 不要争论复杂性的形而上学,但通常“简单”代码与其性能无关,而是我们理解代码/算法的难易程度。一些相关概念:en.wikipedia.org/wiki/Kolmogorov_complexitysep.yimg.com/ty/cdn/paulgraham/…【参考方案6】:逻辑相当简单。假设 z 是红色并且 z 的父级是红色: 如果 z 的叔叔是红色的,则执行步骤 1 将有问题的节点向上推,直到 (1)父母成为根。然后简单地将根标记为黑色。完毕 或者 (2) z 的叔叔是黑人。
万一(2) (a) z 是其父级的左孩子,那么步骤 3 将是最后一步,因为 BST 的所有属性都已满足。完毕。 或 (b) z 是其父母的右孩子。步骤 2 将问题转换为情况 (a)。然后执行步骤 3。完成。
因此,逻辑是尝试到达情况 (1) 和 (2a),以先到者为准。这些是我们知道解决方案的情况。
【讨论】:
【参考方案7】:忽略我(现已删除)的评论 - 我认为冈崎的代码会帮助你。如果您有这本书(“纯功能数据结构”),请查看第 26 页的文本和图 3.5(面向,第 27 页)。很难比这更清楚了。
很遗憾the thesis available on-line 没有那个部分。
我不打算将它复制出来,因为图表很重要,但它表明所有不同的情况基本上是相同的,并且它提供了一些非常简单的 ML 代码,可以很好地证明这一点。
[更新] 看起来你可以在亚马逊上看到这个。转到the book's page,将鼠标悬停在图像上并在搜索框中输入“红黑”。这会为您提供包含第 25 页和第 26 页的结果,但您需要登录才能查看它们(显然 - 我还没有尝试登录查看)。
【讨论】:
以上是关于如何轻松记住红黑树的插入和删除?的主要内容,如果未能解决你的问题,请参考以下文章