这次,总算把红黑树给搞清楚了

521 阅读21分钟

查找

顺序查找(基于无序链表)

public class SequentialSearchST<Key,Value> {
    private Node first;
    private class Node {
        Key key;
        Value val;
        Node next;
        
        public Node(Key key,Value val,Node next) {
            this.key = key;
            this.val = val;
            this.next = next;
        }
    }
    public Value get(Key key) {
        // 查找给定的键,返回相关联的值
		for (Node x = first; x != null; x = x.next)
			if (key.equals(x.key))
				return x.val; 			// 命中
		return null; 					// 未名中
    }
    public void put(Key key, Value val) { //查找给定的键,找到则更新其值,否则在表中新建结点 
        for (Node x = first; x != null; x = x.next)
            if (key.equals(x.key)) { 
                x.val = val; 
                return; 
            } // 命中,更新
        first = new Node(key,val,first); // 未命中,新建结点
    }
}

符号表的实现使用了一个私有内部Node类来在链表中保存键和值。get( ) 的实现会顺序地搜索链表查找给定的键(找到则返回相关联的值)。p u t()的实现也会顺序地搜索链表查找给定的键,如果找到则更新相关联的值,否则它会用给定的键值对创建一个新的结点并将其插入到链表的开头。

有序数组中的二分查找

二分查找(基于有序数组)

public class BinarySearchST<Key extends Comparable<Key>, Value> {
	private Key[] keys;
	private Value[] vals;
	private int N;
	public BinarySearchST(int capacity) { // 调整数组大小的标准代码请见算法1.1
		keys = (Key[]) new Comparable[capacity] ;
		vals = (Value[]) new Object[capacity
	public int size() { 
    	return N; 
	} 
	public Value get(Key key)
		if (isEmpty()) 
        	return null;
		int i = rank(key);
		if ( i < N && keys[i].compareTo(key) == 0) 
        	return vals[i] ;
		else
      		return null;
	public int rank(Key key) {
        if (hi < lo) 
            return lo;
        int mid = lo + (hi - lo) / 2;
        int cmp = key.compareTo(keys[mid]); 
        if (cmp < 0)
            return rank(key, lo, mid-1);
        else if (cmp > 0)
            return rank(key, mid + 1, hi) ;
        else 
            return mid;
    }
	public void put(Key key, Value val)	{ // 查找键,找到则更新值,否则创建新的元素
		int i = rank(key);
		if (i < N && keys[i].compareTo(key) == 0) { 
        	vals[i] = val; 
        	return; 
    	} 
    	for (int j = N; j > i; j--) { 
        	keys[j] = keys[j-1]; 
        	vals[j] = vals[j-1]; 
    	}
        keys[i] = key; 
        vals[i] = val; 
    	N++; 
	}
    public void delete(Key key)
	
}

基于有序数组的二分查找(迭代)

public in t rank(Key key) {
    int lo = 0, hi = N - 1;
	while (lo <= hi) {
        int mid = lo + (hi - To) / 2;
        int cmp = key.compareTo(keys[mid]) ;
        if (cmp < 0) 
            hi = mid - 1;
        else if(cmp > 0) 
            To = mid + 1;
        else 
            return mid:
    }
    return lo;
}

二分查找的分析

在N个键的有序数组中进行二分查找最多需要(IgN + 1) 次比较( 无论是否成功) 。

二分查找减少了比较的次数但无法减少运行所需时间,因为它无法改变以下事实:在键是随机排列的情况下,构造一个基于有序数组的符号表所需 要访问数组的次数是数组长度的平方级別。

比较

一般情况下二分查找都比顺序查找快得多,它也是众多实际应用程序的最佳选择。

image-20210615185551529.png

要支持高效的插人操作,我们似乎需要一种链式结构。但单链接的链表是无法使用二分查找法的,因为二分查找的高效来自于能够快速通过索引取得任何子数组的中间元素(但得到一条链表的中间元素的唯一方法只能是沿链表遍历)。为了将二分查找的效率和链表的灵活性结合起来,我们需要更加复杂的数据结构。能够同时拥有两者的就是二叉查找树

image-20210615185944273.png

二叉查找树

一棵二叉查找树( BST) 是一棵二叉树,其中每个舞点都含有一个Comparable的键(以及相关联的值) 且每个结点的键都大于其左子树中的任意结点的键而小于右子树的任意结点的键。

image-20210616122332942.png

基于二叉树的符号表

public class BSTcKey extends Comparable<Key>, Value> {
    private Node root; / / 二叉查找树的根结点
    private class Node {
        private Key key;  // 键
        private Value val;  // 值
        private Node left, right; // 指向子树的链接
        private int N; // 以该结点为根的子树中的结点总数
        
        public Node(Key key, Value val, int N) { 
            this.key = key; 
            this.val = val; 
            this.N = N; 
        }
        
	public int size() { 
        return size(root); 
    }
        
    private int size(Node x) {
        if (x == null)
            return 0;
        else
            return x.N;
    }
    public Value get(Key key) {
        return get(root, key);
    }
    private Value get(Node x, Key key) { 
    //在以x为根结点的子树中查找并返回key所对应的值;
	// 如果找不到则返回null
		if (x == null)
            return null;
    	int cmp = key.compareTo(x.key);
        if (cmp < 0) 
            return get(x.left,key);
        else if(cmp > 0)
            return get(x.right,key);
        else 
            return x.val;
    }
    public void put(Key key, Value val) {
        root = put(root,key,val);
    }
    private Node put(Node x,Key key,Value val) {
        //如果key存在于以x为根结点的子树中则更新它的值;
		//否则将以key和val为键值对的新结点插入到该子树中
        if (x == nul1) 
            return new Node(key,val,1);
        int cmp = key.compareTo(x.key);
        if (cmp < 0) 
            x.left = put(x.left, key,val);
        else if (cmp > 0) 
            x.right = put(x.right, key,val);
        else 
            x.val = val;
		x.N = size(x.left) + size(x.right) + 1;
		return x:
    }
   // max()、min()、floor()、ceiling() 
   //select()N rank()
   //delete()、deleteMin()、deleteMax()
   //keys()
 }

分析

使用二叉查找树的算法的运行时间取决于树的形状,而树的形状又取决于键被插入的先后顺序。在最好的情况下, 一 棵含有w 个结点的树是完全平衡的,每条空链接和根结点的距离都为〜lgN。在最坏的情况下,搜索路径上可能有N个结点。如图3.2.7所示。但在一般情况下树的形状和最好情况更接近。

二叉查找树和快速排序几乎就是“ 双胞胎” 。树的根结点就是快速排序中的第一 个切分元素(左侧的键都比它小,右侧的键都比它大),而这对于所有的子树同样适用,这和快速排序中对子数组的递归排序完全对应。

image-20210616141550273.png

二叉查找树中max()、min()、floor( )、ceiling() 方法的实现。

public Key min() {
	return min (root).key; 
}

private Node min(Node x) {
	if (x.left == null) 
        return x;
	return min(x.left); 
}

public Key floor(Key key) {
    Node x = floor(root,key);
    if (x == null) 
        return null;
    return x.key; 
}

private Node floor(Node x, Key key) {
    if (x == nul1) 
        return null ;
    int cmp = key.compareTo(x.key);
    if (cmp == 0) 
        return x;
    if (cmp < 0) 
        return floor(x.left, key);
    Node t = floor(x .right,key);
    if (t != nul1) 
        return t ;
    else return x;
}

max()和 ceiling() 的实现分别与min()和 floo r()方法基本相同,只是将代码中的left 和 right (以及>=和<=) 调换而已。

二叉查找树中selectO()和 rank()方法的实现

public Key select(int k) {
    return select(root,k).key;
}

private Node select(Node x,int k) {
    if (x == null)
        return null;
    int t = size(x.left);
    if (t > k)
        return select(x.left,k);
    else if (t < k) 
        return select(x.right,k - t - 1);
    else 
        return x;
}

public int rank(Key key) {
    return rank(key,root);
}

private int rank(Key key,Node x) {
    if (x == null) 
        return 0;
    int cmp = key.compareTo(x.key);
    if (cmp < 0)
        return rank(key,x.left);
    else if (cmp > 0)
        return 1 + size(x.left) + rank(key,x.right);
    else 
        return size(x.left);
}

删除最大键和删除最小键

**

public void deleteMin() {
    root = deleteMin(root);
}

private Node deleteMin(Node x) {
    if (x.left == null) 
        return x.right;
    x.left = deleteMin(x.left);
    x.N = size(x.left) + size(x.right) + 1;
    return x;
}

public void delete(Key key) {
    root = delete(root,key);
}

private Node delete(Node x,Key key) {
    if (x == null)
        return null;
    int cmp = key.compareTo(x.key);
    if (cmp < 0) 
        x.left = delete(x.left,key);
    else if (cmp > 0)
        x.right = delete(x.right,key);
    else {
        if (x.right == null) 
            return x.left;
        if (x.left == null)
            return x.right;
        Node t = x;
        x = min(t.right);
        x.left = t.left;
    }
    x.N = size(x.left) + size(x.right) + 1;
    return x;
  
}

二叉查找树的范围查找操作

**

public Iterable <Key> keys() { 
    return keys(min(), max()); 
}
public Iterable <Key> keys(Key lo , Key hi) {
	Queue<Key> queue = new Queue<Key>();
	keys(root, queue, lo, h i) ;
	return queue;
}
private void keys(Node x, Queue<Key> queue, Key lo, Key hi) {
    if (x == null) 
        return;
    int cmplo = lo.compareTo(x.key);
    int cmphi = hi.compareTo(x.key);
    if (cmplo < 0) 
        keys(x.left,queue,lo,hi) ;
	if (cmplo <= 0 && cmphi >= 0) 
        queue.enqueue(x.key);
    if (cmphi > 0) keys (x.right,queue,lo,hi);
}

为了确保以给定结点为根的子树中所有在指定范围之内的键加入队列,我们会(递归地)查找根结点的左子树,然后查找根结点,然 后(递归地)查找根结点的右子树。

image-20210616172016760.png

分析

在一裸二叉查我树中,所有操作在最坏情况下所需的时间都和树的高度成正比。

平衡查找树

理想情况下我们希望能够保持二分查找树的平衡性。在一棵含有N个结点的树中,我们希望树高为〜lgN,这样我们就能保证所有查找都能在〜lgN次比较内结束,就和二分查找一样。

2-3查找树

一棵2 - 3 查找树或为一棵空树,或由以下结点组成:

  • 2 - 结点,含有一个键(及其对应的值)和两条链接,左链接指向的2 - 3 树中的键都小于该结点,右链接指向的2 - 3 树中的键都大于该结点 。
  • 3 -结点,含有两个键( 及其对应的值) 和三条链接,左链接指向的2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该结点的两个键之间,右链接指向的2-3树中的键都大于该结点。

image-20210617090631651.png

查找

将二叉查找树的查找算法一般化我们就能够直接得到2-3树的查找算法。

image-20210617091506866.png

向2- 结点中插入新键

要在2-3树中插入一个新结点,我们可以和二叉查找树一样先进行一次未命中的查找,然后把新结点挂在树的底部。 插入K 但这样的话树无法保持完美平衡性。 2-3树的主要原因就在于它能够在插入后继续保持平衡。如果未命中的查 找结束于一个2-结点,事情就好办了 :我们只要把这个2-结点替换为一个3-结点,将要插入的键保存在其中即可。

向一棵只含有一个3- 结点的树中插入新键

这棵树中有两个键,所以在它唯一的结点中已经没有可插人新键的空间了。为了将新键插人,我们先临时将新键存入该结 点中,使之成为一个4 -结点。它很自然地扩展了以前的结点并含有3 个键和4 条链接。创建一个4-结点很方便,因为很容易将它转换为一棵由3个 2-结点组成的2-3树•,其中一个结点(根 )含有中键, 一个结点含有3 个键中的最小者(和根结点的左链接相连) ,一个结点含有3 个键中的最大者(和根结点的右链接相连)。这棵树既是一棵含有3个结点的二叉查找树,同时也是一棵完美平衡的2-3树 ,因为其中所有的空链接到根结点的距离都相等。

向一个父结点为2- 结点的3- 结点中插入新键

在这 种情况下我们需要在维持树的完美平衡的前提下为新键腾出空间。我们先像刚才一样构造一个临时的4-结点并将其分解,但此时我们不会为中键创建一个新结点,而是将其移动至原来的父结点中。

image-20210617094317689.png

向一个父结点为3 -结点的3 -结点中插入新键

我们再次和刚才一样构造一个 临时的4 -结点并分解它,然后将它的中键插入它的父结点中。但父结点也是一个3 -结点,因此我们再用这个中键构造一个新的临时4 -结点,然后在这个结点上进行相同的变换,即分解这个父结点并将它的中键插入到它的父结点中去。推广到一般情况,我们就这样一直向上不断分解临 时的4 -结点并将中键插入更高层的父结点,直至遇到一个2- 结点并将它替换为一个不需要继续分解的3 -结点,或者是到达3 -结点的根。

分解根结点

如果从插入结点到根结点的路径上全都是3-结点,我们的根结点最终变成一个临时的4-结点。 此时我们可以按照向一棵只有一个3-结点的树中插入新键的方法处理这个问题。我们将临时的4-结点分解为3 个 2-结点,使得树高加1

image-20210617094743297.png

将一个4-结点分解为一棵2-3树可能有6 种情况

image-20210617095933144.png

image-20210617100010386.png

和标准的二叉查找树由上向下生长不同,2-3树的生长是由下向上的。

在一棵大小为N的2 - 3 树中,查找和插入操作访问的结点必然不超过lgN个。因此我们可以确定2-3树在最坏情况下仍有较好的性能。每个操作中处理每个结点的时间都不会 超过一个很小的常数,且这两个操作都只会访问一条路径上的结点,所以任何查找或者插入的成本都 肯定不会超过对数级别。

尽管我们可以用不同的数据类型表示2-结点和3- 结点并写出变换所需的代码,但用这种直白的表示方法实现大多数的操作并不方便,因为需要处理的情况实在太多。我们需要维护两种不同类型的结点,将被查找的键和结点中的每个键进行比较,将链接和其他信息从一种结点复制到另一种结点,将结点从一种数据类型转换到另一种数据类型,等等。实现这些不仅需要大量的代码,而且它们所产生的额外开销可能会使算法比标准的二叉查找树更慢。

所以有了红黑二叉查找树。

红黑二叉查找树

红黑二叉查找树背后的基本思想是用标准的二叉查找树(完全由2-结点构成) 和一些额外的信息(替换3 - 结点)来表示2-3树。我们将树中的链接分为两种类型:红链接将两个2- 结点连接起来构成一个3-结点黑链接则是2-3树中的普通链接。确切地说,我们将3-结点表示为由一条左斜的红色链接(两个2-结点其中之一是另一个的左子结点)相连的两个2-结点

红黑树的另一种定义是含有红黑链接并满足下列条件的二叉查找树:

  • 红链接均为左链接;
  • 没有任何一个结点同时和两条红链接相连;
  • 该树是完美黑色平衡的,即任意空链接到根结点的路径上的黑链接数量相同。

满足这样定义的红黑树和相应的2-3树是一一对应的。

如果我们将一棵红黑树中的红链接画平,那么所有的空链接到根结点的距离都将是相同的。如果我们将由红链接相连的结点合并,得到的就是一棵2-3树。相反,如果将一棵 2 - 3 树中的3 -结点画作由红色左链接相连的两个2 -结点,那么不会存在能够和两条红链接 相连的结点,且树必然是完美黑色平衡的,因为黑链接即2-3树中的普通链接,根据定义这些链接必然是完美平衡的。无论我们选择用何种方式去定义它们,红黑树都既是二叉查找树

image-20210617104529351.png

方便起见,因为每个结点都只会有一条指向自己的链接(从它的父结点指向它),我们将链接的颜色保存在表示结点的Node数据类型的布尔变量color中。如果指向它的链接是红色的,那么该变量为true,黑色则为false 。我们约定空链接为黑色。当我们提到一个结点的颜色时,我们指的是指向该结点的链接的颜色,反之亦然。

private static final boolean RED = true;
private static final boolean BLACK = false;

private class Node {
    Key key;
    Value val;
    Node left,right;
    int N;
    boolean color;
    
    Node (Key key,Value,int N,boolean color) {
        this.key = key;
        this.val = val;
        this.N = N;
        this.color = color;
    }
}

private boolean isRed(Node x) {
    if (x == null)
        return false;
    return x.color == RED;
}

旋转

左旋转

image-20210617111413531.png

image-20210617111434899.png

Node rotateLeft(Node h) {
    Node x = h.right;
    h.right = x.left;
    x.left = h;
    x.color = h.color;
    h.color = RED;
    x.N = h.N;
    h.N = 1 + size(h.left) + size(h.right);
    return x;
}

右旋

image-20210617112457294.png

image-20210617112522105.png

Node rotateRight(Node h) {
    Node x = h.left;
    h.left = x.right;
    x.right = h;
    x.color = h.color;
    h.color = RED;
    x.N = h.N;
    h.N = 1 + size(h.left) + size(h.right);
    return x;
}

在插人新的键时我们可以使用旋转操作帮助我们保证2-3树和红黑树之间的一 一 对应关系, 因为旋转操作可以保持红黑树的两个重要性质:有序性和完美平衡性。

向 2 -结点中插入新键

一棵只含有一个键的红黑树只含有一个2-结点。插入另一个键之后,我们马上就需要将它们旋转。如果新键小于老键,我们只需要新增一个红色的结点即可,新的红黑树和单个3- 结点完全等价。如果新键大于老键,那么新增的红色结点将会产生一条红色的右链接。我们需要使用root = rotateLeft(root);来将其旋转为红色左链接并修正根结点的链接,插入操作才算完成。两种情况的结果均为一棵和单个3-结点等价的红黑树,其中含有两个键,一条红链接,树的黑链接高度为1。

向树底部的2-结点插入新键

用和二叉查找树相同的方式向一棵红黑树中插入一个新键会在树的底部新增一个结点(为了保证有序性),但总是用红链接将新结点和它的父结点相连。如果它的父结点是一个2-结点,那么刚才讨论的两种处理方法仍然适用。如果指向新结点的是父结点的左链接,那么父结点就直接成为 了一个3-结点;如果指佝新结点的是父结点的右链接,这就是一个错误的3-结点,但一次左旋转就能够修正它。

image-20210617114511488.png

向一棵双键树(即一个3 -结点)中插入新键

这种情况又可分为三种子情况:新键小于树中的两个键,在两者之间,或是大于树中的两个键。 每种情况中都会产生一个同时连接到两条红链接的结点,而我们的目标就是修正这一点。

  • 三者中最简单的情况是新键大于原树中的两个键,因此它被连接到3-结点的右链接。此时 树是平衡的,根结点为中间大小的键,它有两条红链接分别和较小和较大的结点相连。如果我们将两条链接的颜色都由红变黑,那么我们就得到了一棵由三个结点组成、高为2 的平衡 树。它正好能够对应一棵2-3树,如图3.3.20 ( 左 )。其他两种情况最终也会转化为这种情况。
  • 如果新键小于原树中的两个键,它会被连接到最左边的空链接,这样就产生了两条连续的红 链接,如图3.3.20 ( 中 )。此时我们只需要将上层的红链接右旋转即可得到第一种情况(中值键为根结点并和其他两个结点用红链接连)。
  • 如果新键介于原树中的两个键之间,这又会产生两条连续的红链接,一 条红色左链接接一条红色右链接,如图3.3.20 ( 右 )。此时我们只需要将下层的红链接左旋转即可得到第二种情 况 (两条连续的红色左链接)。

image-20210617115000753.png

颜色转换

专门用一个方法flipColors ( ) 来转换一个结点的两个红色子结点的颜色。除 了将子结点的颜色由红变黑之外,我们同时还要将父 结点的颜色由黑变红。这项操作最重要的性质在于它 和旋转操作一样是局部变换,不会影响整棵树的黑色平衡性。

image-20210617115543698.png

image-20210617115557904.png

void flipColors(Node h) {
    h.color = RED;
    h.left.color = BLACK;
    h.right.color = BLACK;
}

根结点总是黑色

颜色转换会使根结点变为红色。这也可能出现在很大的红黑树中。严格地说,红色的根结点说明根结点是一个3-结点的一部分,但实际情况并不是这样。因此我们在每次插人后都会将根结点设为黑色。注意,每当根结点由红变黑时树的黑链接高度就会加1。

向树底部的3- 结点插入新键

现在假设我们需要在树的底部的一个3- 结点下加人一个新结点。前面讨论过的三种情况都会出现,如 图 3.3.22所示。指向新结点的链接可能是3- 结点的右链接(此时我们只需要转换颜色即可),或是左链接(此时我们需要进行右旋转然后再转换颜色),或是中链接(此时我们需要先左旋转下层链接然后右旋转上层链接,最后再转换颜色)。颜色转换会使到中结点的链接变红,相当于将它送人了父结点。

image-20210617135859672.png

将红链接在树中向上传递

2-3树中的插人算法需要我们分解3 -结点,将中间键插入父结点,如此这般直到遇到一个2-结点或是根结点。我们所考虑过的所有情况都正是为了达成这个目标:每次必要的旋转之后我们都会进行颜色转换,这使得中结点变红。在父结 点看来,处理这样一个红色结点的方式和处理一个新插入的红色结点完全相同,即继续把红链接 转移到中结点上去。图 3.3.23中总结的三种情况 显示了在红黑树中实现2-3树的插入算法的关键 操作所需的步骤:要在一个3-结点下插人新键, 先创建一个临时的4 -结点,将其分解并将红链接由中间键传递给它的父结点。重复这个过程,我们就能将红链接在树中向上传递,直至遇到一个2-插入H 出现两条连续的左 链接,需要右旋转 拥有两个红色子链接,需要进行颜色转换出现红色右链接,需要左旋转 结点或者根结点。

如果右子结点是红色的而左子结点是黑色的,进行左旋转;

如果左子结点是红色的且它的左子结点也是红色的,进行右旋转

如果左右子结点均为红色,进行颜色转换

image-20210617141555749.png

红黑树的插入算法

public class RedBlackBST<Key extends Comparable<Key>,Value> {
    private Node root;
    private class Node {
        Key key;
        Value val;
        Node left,right;
        int N;
        boolean color;
        Node (Key key,Value,int N,boolean color) {
            this.key = key;
            this.val = val;
            this.N = N;
            this.color = color;
        }
	}
    private boolean isRed(Node h);
    private Node rotateLeft(Node h);
    private Node rotateRight(Node h);
    private void flipColors(Node h);
    
    private int size();
    
    public void put(Key key,Value val) {
        root = put(root,key,val);
        root.color = BLACK;
    }
    
    private Node put(Node h,Key key,Value val) {
        if (h == null)
            return new Node(key,val,1,RED);
        int cmp = key.compareTo(h.key);
        if (cmp < 0)
            h.left = put(h.left,key,val);
        else if (cmp > 0)
            h.right = put(h.right,key,val);
        else
            h.val = val;
        if (isRed(h.right) && !isRed(h.left))
            h = rotateLeft(h);
        if (isRed(h.left) && isRed(h.left.left))
            h = rotateRight(h);
        if (isRed(h.left) && isRed(h.right))
            flipColors(h);
        h.N = size(h.left) + size(h.right) + 1;
        return h;
    }
 	
}

红黑树的删除算法

删除最小键

从 2 -结点中删除一个键会留下一个空结点,一般我们会将它替 换为一个空链接,但这样会破坏树的完美平衡性。所以我们需要这样做:为了保证我们不会删除一个 2-结点,我们沿着左链接向下进行变换,确保当前结点不是2-结 点 (可能是3-结点,也可能是临时的4-结点)。首先,根结点可能有两种情况。如果根是2-结点且它的两个子结点都是2-结点,我们可以直接将这三个结点变成一个4 -结点;否则我们需要保证根结点的左子结点不是2-结点,如有必要可以从它右侧的兄弟结点“借” 一个键来。

在沿着左链接向下的过程中,保证以下情况之一成立:

  • 如果当前结点的左子结点不是2-结点,完成
  • 如果当前结点的左子结点是2 -结点而它的亲兄弟结点不是2 -结点,将左子结点的兄弟结点中的一个键移动到左子结点中
  • 如果当前结点的左子结点和它的亲兄弟结点都 是 2-结点,将左子结点、父结点中的最小键和左子结点最近的兄弟结点合并为一个4- 结点,使父结点由3-结点变为2-结点或者由4-结点变为3-结点

在遍历的过程中执行这个过程,最后能够得到一个含有最小键的3-结点或者4 -结点,然后我们就可 以直接从中将其删除,将 3 -结点变为2-结点,或者将 4-结点变为3-结点。

private Node moveRedLeft(Node h) {
  	//假设结点h为红色,h.left和h.left.left都是黑色,
	// 将h.left或者h.left的子结点之一变红
    flipColors(h);
    if (isRed(h.right.left)) {
        h.right = rotateRight(h.right);
        h = rotateLeft(h);
    }
    return h;
}

public void deleteMin() {
    if (!isRed(root.left) && !isRed(root.right))
        root.color = RED;
    root = deleteMIn(root);
    if (!isEmpty())
        root.color = BLACK;
}

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);
}

private Node balance(Node h) {
    if (isRed(h.right)) h = rotateLeft(h);
    if (isRed(h.right) && !isRed(h.left))
        h = rotateLeft(h);
    if (isRed(h.left) && isRed(h.left.left))
        h = rotateRight(h);
    if (isRed(h.left) && isRed(h.right))
        flipColors(h);
    h.N = size(h.left) + size(h.right) + 1;
    return h;
}

这里的flipColors()方法将会补全三条链接的颜色,而不是正文中实现插入操作时实现的flipColors()方法。对于删除,我们会将父结点设为BLACK( 黑 )而将两个子结点设为RED(红 )。

刪除最大键

private Node moveRedRight(Node) {
    flipColors(h);
    if (!isRed(h.left.left))
        h = rotateRight(h);
    return h;
}
public void deleteMax() {
    if (!isRed(root.left) && !isRed(root.right)) 
        root.color = RED;
    root = deleteMax(root);
    if (!isEmpty())
        root.color = BLACK;
} 

private Node deleteMax(Node h) {
    if (isRed(h.left))
        h = rotateRight(h);
    if (h.right == null)
        return null;
    if (!isRed(h.right) && !isRed(h.right.left))
        h = moveRedRight(h);
    h.right = deleteMax(h.right);
    return balance(h);
}

删除操作

在查找路径上进行和删除最小键相同的变换同样可以保证在查找过程中任意当前结点均不是2-结点。如果被查找的键在树的底部,我们可以直接删除它。如果不在,我们需要将它和它的后继结点交换,就和 二叉查找树一样。因为当前结点必然不是2-结点,问题已经转化为在一棵根结点不是2- 结点的子树中删除最小的键,我们可以在这棵子树中使用前文所述的算法。

public void delete(Key key) {
    if (!isRed(root.left) && !isRed(root.right))
        root.color = RED;
    root = delete(root,key);
    if (!isEmpty())
        root.color = BLACK;
}

private Node delete(Node h,Key key) {
    if (key.compareTo(h.key) < 0) {
        if (!isRed(h.left) && !isRed(h.left.left))
            h = moveRedLeft(h);
        h.left = delete(h.left,key);
    } else {
        if (isRed(h.left))
            h = rotateRight(h);
        if (key.compareTo(h.key) == 0 && (h.right == null))
            return null;
        if (key.compareTo(h.key) == 0) {
            h.val = get(h.right,min(h.right).key);
            h.key = min(h.right).key;
            h.right = deleteMin(h.right);
        } else 
            h.right = delete(h.right);
    }
    return balance(h);
}