红黑树(五):删除最小键
1. 自顶向下的2-3-4树
虽然删除操作相对较难,但是删除的很多思路都和插入有联系。因为删除无外乎也是利用一系列的局部变换来在删除一个结点的同时保持树的完整平衡性。
借用教材上面的描述1:删除过程比插入一个结点更加复杂,因为我们不仅要在(为了删除一个结点而)构造临时4-结点时,沿着查找路径向下进行变换,还在分解遗留的4-结点时,沿着查找路径向上进行变换(同插入操作)。
对于这段描述,我们可以假象在原有2-3树的基础上,我们现在允许4-结点的存在,那么这种2-3-4树的插入操作要比2-3树要复杂一些,因为我们要自顶向下沿着查找树路径,需要保证当前遇到的结点不是4-结点,这样树底才会有空间进行插入操作,然后沿着路径向上进行变换是为了将之前创建的4-结点配平。
教材上有关于2-3-4树插入操作较为详细的介绍和图例,详见:3.3.4.1 自顶向下的2-3-4树1。但是根据笔者自己的经验,直接理解2-3-4树再去理解2-3树的删除操作反而现得有些麻烦了。所以下面笔者将根据自己的经验进行讲解,可是这种引入4-结点的思想是无论如何都是需要的,所以这里给大家简单介绍一下2-3-4树。
2. 删除最小键 - deleteMin()
2.1 整体思路
现在我们着眼于2-3树的deleteMin(),大家可以来思考这么一个问题:
对于红黑树,怎样deleteMin()才相对比较简单呢?
我们先来看看删除位置位于树底,因为直觉告诉我们,删除叶节点需要考虑的比较少,虽然可能需要自平衡,但是至少不用考虑子节点。
- 如果从树底部的3-结点中删除最小键是很简单的。这是因为我们删除的结点刚好是3-结点的红结点(如果min存在于3-结点之中,那它一定是红结点),那么整个树的完美平衡是不会受到影响的;
- 但是如果树底是2-结点,则会破坏树的完美平衡性。从2-结点中删除一个键会留下一个空结点,一般我们会将它替换为一个空链接,这样会使这条路径少一个黑链接;
那么,根据这样的思考再结合上面介绍的2-3-4树,我们是否可以让删除结点的位置一定是在一个3-结点或4-结点之中,也就是保证删除结点的位置一定不在2-结点之中,来简化删除的复杂性呢?
答案都不用说啦,肯定是可以的啦。所以deleteMin()的思路就是自顶向下保证经过的结点都不是2-结点,等删除完成后再自底向上将临时4-结点全部进行分解,而自底向上的这一步和之前的插入自平衡完全是一样的。
所以接下来,我们需要分析一下如何自顶向下进行变换。
2.2 分情况讨论
※ 这部分内容教材上基本省略了,直接给了代码,所以下面内容都是博主自己总结,如有纰漏,还请指出哒~
我们先来看看deleteMin()的所有情况,主要分为两类:
- root为2-结点;
- root为3-结点;
注意,我们只看了最多两层,也就是局部,但是整个树都可以拆分成这样的小块去解决,所以解决局部问题,也就解决了整体问题:
通过对图例的观察,我们可以得到下面的结论:
结论1:如果root.left为黑,那么按道理是需要进行转换,变成4-结点;但是不是所有 root.left == BLACK 的情况都需要转换。Case 4 和 Case 5就是 root.left 为黑,但是我们不需要变换,直接删除,这是因为 root.left.left 已经为红,已经存在了3-结点,而且我们知道最小值一定再 root.left.left 之后,所以只有 root.left == BLACK && root.left.left == BLACK 时才进行4-结点转换。
通过观察,我们得到了4-结点转换的条件,接着我们看看如何进行4-结点转换:
结论2:注意Case 6,如果我们不先R右旋,直接H左旋的话,也是可以完成删除的,只是会多一些步骤,所以先R右旋,再R左旋的效率最高,大家可以自己试试直接H左旋的变换。所以一般的转换操作就是:先反向染色,然后 root.right.left == RED 的时候,先左旋,再右旋。
最后就是删除结点之后进行自平衡操作,这个和之前的插入自平衡是一样的,不再赘述啦哒。
3. deleteMin()实现
接着我们结合图例来分析一下代码,整体的deleteMin():
/**
* delete the minimum key -> value in this R-B tree
* */
public void deleteMin() {
// the following commented out code is from the textbook,
// but from my point of view, they're redundant.
// if ( !isRed( ( RedBlackTreeNode ) root.left ) &&
// !isRed( ( RedBlackTreeNode ) root.right ) )
// ( ( RedBlackTreeNode ) root ).color = RED;
// the root is null, i.e the tree is empty,
// which is missed by the textbook
if ( isEmpty() ) return;
RedBlackTreeNode root = deleteMin( ( RedBlackTreeNode ) this.root );
this.root = root;
if ( !isEmpty() ) root.color = BLACK;
}
private RedBlackTreeNode deleteMin( RedBlackTreeNode root ) {
// base case, this node is the least one in the tree
// and it's also a leaf node in this R-B tree,
// so just return null, instead of return root.right,
// which is different from deleteMin() for BST.
if ( root.left == null ) return null;
// guarantee that every node
// we're traveling along left subtree
// is either 3-node or 4-node.
// !isRed( root.left.left ) is to
// differentiate case 4 and case 7
if ( !isRed( root.left ) &&
!isRed( root.left.left ) )
root = moveRedLeft( root );
// otherwise, look into the left subtree
root.left = deleteMin( ( RedBlackTreeNode ) root.left );
return balance( root );
}
private RedBlackTreeNode moveRedLeft( RedBlackTreeNode root ) {
flipColors( root, true );
if ( isRed( root.right.left ) ) {
// handle case 6
root.right = rotateRight( ( RedBlackTreeNode ) root.right );
root = rotateLeft( root );
flipColors( root, false );
}
return root;
}
我们先看看deleteMin():
private RedBlackTreeNode deleteMin( RedBlackTreeNode root ) {
// base case, this node is the least one in the tree
// and it's also a leaf node in this R-B tree,
// so just return null, instead of return root.right,
// which is different from deleteMin() for BST.
if ( root.left == null ) return null;
// guarantee that every node
// we're traveling along left subtree
// is either 3-node or 4-node.
// !isRed( root.left.left ) is to
// differentiate case 4 and case 7
if ( !isRed( root.left ) &&
!isRed( root.left.left ) )
root = moveRedLeft( root );
// otherwise, look into the left subtree
root.left = deleteMin( ( RedBlackTreeNode ) root.left );
return balance( root );
}
思路上面已经说过了,不断查找左子树,如果当前结点是2-结点,则进行4-结点转换。接着是4-结点转换方法:
private RedBlackTreeNode moveRedLeft( RedBlackTreeNode root ) {
flipColors( root, true );
if ( isRed( root.right.left ) ) {
// handle case 6
root.right = rotateRight( ( RedBlackTreeNode ) root.right );
root = rotateLeft( root );
flipColors( root, false );
}
return root;
}
转换方法先反向染色,然后检查 root.right.left 是否为红,如果是先 root.right 右旋,然后 root 再左旋,这个部分大家可以参考case 6。
最后大家注意一下deleteMin()主方法:
/**
* delete the minimum key -> value in this R-B tree
* */
public void deleteMin() {
// the following commented out code is from the textbook,
// but from my point of view, they're redundant.
// if ( !isRed( ( RedBlackTreeNode ) root.left ) &&
// !isRed( ( RedBlackTreeNode ) root.right ) )
// ( ( RedBlackTreeNode ) root ).color = RED;
// the root is null, i.e the tree is empty,
// which is missed by the textbook
if ( isEmpty() ) return;
RedBlackTreeNode root = deleteMin( ( RedBlackTreeNode ) this.root );
this.root = root;
if ( !isEmpty() ) root.color = BLACK;
}
下面这段注释掉的代码是教材上面提供的,但是根据刚才我们的分析,root是红还是黑都是无所谓的,所以笔者认为这一步是多余的。
// the following commented out code is from the textbook,
// but from my point of view, they're redundant.
// if ( !isRed( ( RedBlackTreeNode ) root.left ) &&
// !isRed( ( RedBlackTreeNode ) root.right ) )
// ( ( RedBlackTreeNode ) root ).color = RED;
另外,教材给的代码没有检查树为空的情况,需要添加下面的代码,否则空树调用deleteMin()时会报错:
// the root is null, i.e the tree is empty,
// which is missed by the textbook
if ( isEmpty() ) return;
到此,deleteMin()已经讲解完毕。接下来,我们将讲解deleteMax()。deleteMax()和deleteMin()是对称的,所以分析思路也是相似的,但因为deleteMax()不断检查右子树,具体情况会有一些不同。
到此,deleteMin()已经讲解完毕。接下来,我们将讲解deleteMax()。deleteMax()和deleteMin()是对称的,所以分析思路也是相似的,但因为deleteMax()不断检查右子树,具体情况会有一些不同。
上一节:红黑树(四):插入实现
下一节:红黑树(六):删除最大键
系列汇总:超详细!红黑树详解文章汇总(含代码)
3. 教材提供的deleteMin()代码
说实话,不建议大家看中文版上面的代码,各种魔改不说,好多边界案例都不考虑的,比如空树的情况。但是英文原版代码就没有这个问题哦。所以这里推荐英文原版完整红黑树代码:传送门
3.1 英文原版
/**
* Removes the smallest key and associated value from the symbol table.
* @throws NoSuchElementException if the symbol table is empty
*/
public void deleteMin() {
if (isEmpty()) throw new NoSuchElementException("BST underflow");
// if both children of root are black, set root to red
if (!isRed(root.left) && !isRed(root.right))
root.color = RED;
root = deleteMin(root);
if (!isEmpty()) root.color = BLACK;
// assert check();
}
// delete the key-value pair with the minimum key rooted at h
private Node deleteMin(Node h) {
if (h.left == null)
return null;
if (!isRed(h.left) && !isRed(h.left.left))
h = moveRedLeft(h);
h.left = deleteMin(h.left);
return balance(h);
}
// Assuming that h is red and both h.left and h.left.left
// are black, make h.left or one of its children red.
private Node moveRedLeft(Node h) {
// assert (h != null);
// assert isRed(h) && !isRed(h.left) && !isRed(h.left.left);
flipColors(h);
if (isRed(h.right.left)) {
h.right = rotateRight(h.right);
h = rotateLeft(h);
flipColors(h);
}
return h;
}
3.2 中文教课书
3. 特别感谢
- 感谢 @SENNICHEN 制作系列文章封面图
4. 免责声明
※ 本文之中如有错误和不准确的地方,欢迎大家指正哒~
※ 此项目仅用于学习交流,请不要用于任何形式的商用用途,谢谢呢;