你想了解的所有树结构,都收集在这篇文章里了

前言:

《算法 第四版》官网

树结点类

public class TreeNode {

    Integer key; // 键
    Integer val; // 值
    TreeNode left; // 左子树
    TreeNode right; // 右子树

    TreeNode(Integer key, Integer val) {
        this.key = key;
        this.val = val;
    }

}

一、二叉查找树

1.1 定义

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

1.2 代码实现

public class BST {

    /**
     * 根据 key 查找 val(递归)
     */
    public static int searchRec(TreeNode root, int key) {
        if (root == null)
            return -1;
        if (key < root.key)
            return searchRec(root.left, key);
        if (key > root.key)
            return searchRec(root.right, key);
        return root.val;
    }

    /**
     * 根据 key 查找 val(迭代)
     */
    public static int searchIte(TreeNode root, int key) {
        while (root != null && root.key != key)
            root = key < root.key ? root.left : root.right;
        return root == null ? -1 : root.val;
    }

    /**
     * 根据 key 范围查找
     */
    public static void rangeSearch(TreeNode root, List<TreeNode> nodeList, Integer keyLeft, Integer keyRight) {
        if (root == null)
            return;
        if (keyLeft < root.key)
            rangeSearch(root.left, nodeList, keyLeft, keyRight);
        if (keyLeft <= root.key && keyRight >= root.key)
            nodeList.add(root);
        if (keyRight > root.key)
            rangeSearch(root.right, nodeList, keyLeft, keyRight);
    }

    /**
     * 查找最大 key 结点
     */
    public static TreeNode findMax(TreeNode root) {
        return root.right == null ? root : findMax(root.right);
    }

    /**
     * 查找最小 key 结点
     */
    public static TreeNode findMin(TreeNode root) {
        return root.left == null ? root : findMin(root.left);
    }

    /**
     * 插入结点(递归)
     */
    public static TreeNode insertRec(TreeNode root, Integer key, Integer val) {
        if (root == null)
            return new TreeNode(key, val);
        if (key < root.val)
            root.left = insertRec(root.left, key, val);
        else if (key > root.val)
            root.right = insertRec(root.right, key, val);
        else
            root.val = val;
        return root;
    }

    /**
     * 插入结点(迭代)
     */
    public static void insertIte(TreeNode root, TreeNode node) {
        if (root == null || node == null)
            return;
        TreeNode parent = root, current = root;
        while (true) {
            parent = current;
            if (node.key < current.key) {
                current = current.left;
                if (current == null) {
                    parent.left = node;
                    return;
                }
            } else if (node.key > current.key) {
                current = current.right;
                if (current == null) {
                    parent.right = node;
                    return;
                }
            } else {
                current.val = node.val;
                return;
            }
        }
    }

    /**
     * 删除最大 key 结点
     */
    public static TreeNode deleteMax(TreeNode root) {
        if (root.right == null)
            return root.left;
        root.right = deleteMax(root.right);
        return root;
    }

    /**
     * 删除最小 key 结点
     */
    public static TreeNode deleteMin(TreeNode root) {
        if (root.left == null)
            return root.right;
        root.left = deleteMin(root.left);
        return root;
    }

    /**
     * 根据 key 删除结点
     */
    public static TreeNode delete(TreeNode root, int key) {
        if (root == null)
            return null;
        if (key < root.key)
            root.left = delete(root.left, key);
        else if (key > root.key)
            root.right = delete(root.right, key);
        else {
            if (root.right == null)
                return root.left;
            if (root.left == null)
                return root.right;
            TreeNode node = root;
            root = findMin(node.right);
            root.right = deleteMin(node.right);
            root.left = node.left;
        }
        return root;
    }

}

二、平衡查找树

2.1 定义

平衡查找树,是一种二叉查找树,其中每一个结点的左子树和右子树的高度差至多为1。

2.2 代码实现

public class AVL {

    /**
     * 左旋
     */
    public static void leftRotate(TreeNode root) {
        if (root == null)
            return;
        TreeNode node = new TreeNode(root.key, root.val);
        node.left = root.left;
        node.right = root.right.left;
        root.key = root.right.key;
        root.val = root.right.val;
        root.right = root.right.right;
        root.left = node;
    }

    /**
     * 右旋
     */
    public static void rightRotate(TreeNode root) {
        if (root == null)
            return;
        TreeNode node = new TreeNode(root.key, root.val);
        node.right = root.right;
        node.left = root.left.right;
        root.key = root.left.key;
        root.val = root.left.val;
        root.left = root.left.left;
        root.right = node;
    }

    /**
     * 树高度
     */
    public static int height(TreeNode root) {
        return Math.max(root.left == null ? 0 : height(root.left), root.right == null ? 0 : height(root.right)) + 1;
    }

    /**
     * 左子树高度
     */
    public static int leftHeight(TreeNode root) {
        return root.left == null ? 0 : height(root.left);
    }

    /**
     * 右子树高度
     */
    public static int rightHeight(TreeNode root) {
        return root.right == null ? 0 : height(root.right);
    }

    /**
     * 插入结点
     */
    public static TreeNode insertRec(TreeNode root, Integer key, Integer val) {
        if (root == null)
            return new TreeNode(key, val);
        if (key < root.val)
            root.left = insertRec(root.left, key, val);
        else if (key > root.val)
            root.right = insertRec(root.right, key, val);
        else
            root.val = val;
        // 右旋
        if (leftHeight(root) - rightHeight(root) > 1) {
            if (root.left != null && rightHeight(root.left) > leftHeight(root.left))
                leftRotate(root.left);
            rightRotate(root);
        }
        // 左旋
        if (rightHeight(root) - leftHeight(root) > 1) {
            if (root.right != null && leftHeight(root.right) > rightHeight(root.right))
                rightRotate(root.right);
            leftRotate(root);
        }
        return root;
    }

    /**
     * 查找最大 key 结点
     */
    public static TreeNode findMax(TreeNode root) {
        return root.right == null ? root : findMax(root.right);
    }

    /**
     * 查找最小 key 结点
     */
    public static TreeNode findMin(TreeNode root) {
        return root.left == null ? root : findMin(root.left);
    }

    /**
     * 删除最大 key 结点
     */
    public static TreeNode deleteMax(TreeNode root) {
        if (root.right == null)
            return root.left;
        root.right = deleteMax(root.right);
        return root;
    }

    /**
     * 删除最小 key 结点
     */
    public static TreeNode deleteMin(TreeNode root) {
        if (root.left == null)
            return root.right;
        root.left = deleteMin(root.left);
        return root;
    }

    /**
     * 根据 key 删除结点
     */
    public static TreeNode delete(TreeNode root, int key) {
        if (root == null)
            return null;
        if (key < root.key)
            root.left = delete(root.left, key);
        else if (key > root.key)
            root.right = delete(root.right, key);
        else {
            if (root.right == null)
                return root.left;
            if (root.left == null)
                return root.right;
            TreeNode node = root;
            root = findMin(node.right);
            root.right = deleteMin(node.right);
            root.left = node.left;
        }
        // 右旋
        if (leftHeight(root) - rightHeight(root) > 1) {
            if (root.left != null && rightHeight(root.left) > leftHeight(root.left))
                leftRotate(root.left);
            rightRotate(root);
        }
        // 左旋
        if (rightHeight(root) - leftHeight(root) > 1) {
            if (root.right != null && leftHeight(root.right) > rightHeight(root.right))
                rightRotate(root.right);
            leftRotate(root);
        }
        return root;
    }

}

三、2-3 查找树

3.1 定义

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

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

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

2-3 查找树

一棵完美平衡的 2-3 查找树中的所有空链接到根结点的距离都应该是相同。

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

4- 结点分解为一颗 2-3 树
这些局部变换不会影响树的全局有序性和平衡性。

2-3 树的构造轨迹:

2-3 树的构造轨迹

3.2 树的插入实现

2-3 树插入可分为三种情况。

  1. 对于空树,插入一个2结点即可,这很容易理解。

  2. 插入结点到一个2结点的叶子上。应该说,由于其本身就只有一个元素,所以只需要将其升级为3结点即可。如图8-8-3所示。我们希望从左图的2-3树中插入元素3,根据遍历可知,3比8小、比4小,于是就只能考虑插入到叶子结点1所在的位置,因此很自然的想法就是将此结点变成一个3结点,即右图这样完成插入操作。当然,要视插入的元素与当前叶子结点的元素比较大小后,决定谁在左谁在右。例如,若插入的是0,则此结点就是“0”在左“1”在右了。
    8-8-3

  3. 要往3结点中插入一个新元素。因为3结点本身已经是2-3树的结点最大容量(已经有两个元素),因此就需要将其拆分,且将树中两元素或插入元素的三者中选择其一向上移动一层。复杂的情况也正在于此。

    第一种情况,见图8-8-4,需要向左图中插入元素5。经过遍历可得到元素5比8小比4大,因此它应该是需要插入在拥有6、7元素的3结点位置。问题就在于,6和7结点已经是3结点,不能再加。此时发现它的双亲结点4是个2结点,因此考虑让它升级为3结点,这样它就得有三个孩子,于是就想到,将6、7结点拆分,让6与4结成3结点,将5成为它的中间孩子,将7成为它的右孩子,如图8-8-4的右图所示。
    8-8-4

    另一种情况,如图8-8-5所示,需要向左图中插入元素11。经过遍历可得到元素11比12、14小比9、10大,因此它应该是需要插入在拥有9、10元素的3结点位置。同样道理,9和10结点不能再增加结点。此时发现它的双亲结点12、14也是一个3结点,也不能再插入元素了。再往上看,12、14结点的双亲,结点8是个2结点。于是就想到,将9、10拆分,12、14也拆分,让根结点8升级为3结点,最终形成如图8-8-5的右图样子。
    8-8-5
    再来看个例子,如图8-8-6所示,需要在左图中插入元素2。经过遍历可得到元素2比4小、6比1大,因此它应该是需要插入在拥有1、3元素的3结点位置。与上例一样,你会发现,1、3结点,4、6结点都是3结点,都不能再插入元素了,再往上看,8、12结点还是一个3结点,那就意味着,当前我们的树结构是三层已经不能满足当前结点增加的需要了。于是将1、3拆分,4、6拆分,连根结点8、12也拆分,最终形成如图8-8-6的右图样子。
    8-8-6

3.3 树的删除实现

  1. 所删除元素位于一个3结点的叶子结点上,这非常简单,只需要在该结点处删除该元素即可,不会影响到整棵树的其他结点结构。如图8-8-7所示,删除元素9,只需要将此结点改成只有元素10的2结点即可。
    8-8-7

  2. 所删除的元素位于一个2结点上,即要删除的是一个只有一个元素的结点。如果按照以前树的理解,删除即可,可现在的2-3树的定义告诉我们这样做是不可以的。比如图8-8-8所示,如果我们删除了结点1,那么结点4本来是一个2结点(它拥有两个孩子),此时它就不满足定义了。
    8-8-8
    因此,对于删除叶子是2结点的情况,我们需要分四种情形来处理。
    情形一,此结点的双亲也是2结点,且拥有一个3结点的右孩子。如图8-8-9所示,删除结点1,那么只需要左旋,即6成为双亲,4成为6的左孩子,7是6的右孩子。
    8-8-9
    情形二,此结点的双亲是2结点,它的右孩子也是2结点。如图8-8-10所示,此时删除结点4,如果直接左旋会造成没有右孩子,因此需要对整棵树变形,办法就是,我们目标是让结点7变成3结点,那就得让比7稍大的元素8下来,随即就得让比元素8稍大的元素9补充结点8的位置,于是就有了图8-8-10的中间图,于是再用左旋的方式,变成右图结果。
    8-8-10
    情形三,此结点的双亲是一个3结点。如图8-8-11所示,此时删除结点10,意味着双亲12、14这个结点不能成为3结点了,于是将此结点拆分,并将12与13合并成为左孩子。
    8-8-11
    情形四,如果当前树是一个满二叉树的情况,此时删除任何一个叶子都会使得整棵树不能满足2-3树的定义。如图8-8-12所示,删除叶子结点8时(其实删除任何一个结点都一样),就不得不考虑要将2-3的层数减少,办法是将8的双亲和其左子树6合并为一3个结点,再将14与9合并为3结点,最后成为右图的样子。
    8-8-12

  3. 所删除的元素位于非叶子的分支结点。此时我们通常是将树按中序遍历后得到此元素的前驱或后继元素,考虑让它们来补位即可。

    如果我们要删除的分支结点是2结点。如图8-8-13所示我们要删除4结点,分析后得到它的前驱是1后继是6,显然,由于6、7是3结点,只需要用6来补位即可,如图8-8-13右图所示。
    8-8-13
    如果我们要删除的分支结点是3结点的某一元素,如图8-8-14所示我们要删除12、14结点的12,此时,经过分析,显然应该是将是3结点的左孩子的10上升到删除位置合适。
    8-8-14

四、2-3-4 查找树

有了2-3树的讲解,2-3-4树就很好理解了,它其实就是2-3树的概念扩展,包括了4结点的使用。一个4结点包含小中大三个元素和四个孩子(或没有孩子),一个4结点要么没有孩子,要么具有4个孩子。如果某个4结点有孩子的话,左子树包含小于最小元素的元素;第二子树包含大于最小元素,小于第二元素的元素;第三子树包含大于第二元素,小于最大元素的元素;右子树包含大于最大元素的元素。

由于2-3-4树和2-3树是类似的,我们这里就简单介绍一下,如果我们构建一个数组为{7,1,2,5,6,9,8,4,3}的2-3-4树的过程,如图8-8-15所示。图1是在分别插入7、1、2时的结果图,因为3个元素满足2-3-4树的单个4结点定义,因此此时不需要拆分,接着插入元素5,因为已经超过了4结点的定义,因此拆分为图2的形状。之后的图其实就是在元素不断插入时最后形成了图7的2-3-4树。
8-8-15
图8-8-16是对一个2-3-4树的删除结点的演变过程,删除顺序是1、6、3、4、5、2、9。
8-8-16

五、B+ 树

尽管前面我们已经讲了B树的诸多好处,但其实它还是有缺陷的。对于树结构来说,我们都可以通过中序遍历来顺序查找树中的元素,这一切都是在内存中进行。

可是在B树结构中,我们往返于每个结点之间也就意味着,我们必须得在硬盘的页面之间进行多次访问,如图8-8-18所示,我们希望遍历这棵B树,假设每个结点都属于硬盘的不同页面,我们为了中序遍历所有的元素,页面2→页面1→页面3→页面1→页面4→页面1→页面5。而且我们每次经过结点遍历时,都会对结点中的元素进行一次遍历,这就非常糟糕。有没有可能让遍历时每个元素只访问一次呢?
8-8-18
B+树是应文件系统所需而出的一种B树的变形树,注意严格意义上讲,它其实已经不是前面定义的树了。在B树中,每一个元素在该树中只出现一次,有可能在叶子结点上,也有可能在分支结点上。而在B+树中,出现在分支结点中的元素会被当作它们在该分支结点位置的中序后继者(叶子结点)中再次列出。另外,每一个叶子结点都会保存一个指向后一叶子结点的指针。

例如图8-8-19所示,就是一棵B+树的示意,灰色关键字即是根结点中的关键字在叶子结点再次列出,并且所有叶子结点都链接在一起。
8-8-19
一棵m阶的B+树和m阶的B树的差异在于:

  • 有n棵子树的结点中包含有n个关键字;
  • 所有的叶子结点包含全部关键字的信息,及指向含这些关键字记录的指针,叶子结点本身依关键字的大小自小而大顺序链接;
  • 所有分支结点可以看成是索引,结点中仅含有其子树中的最大(或最小)关键字。

这样的数据结构最大的好处就在于,如果是要随机查找,我们就从根结点出发,与B树的查找方式相同,只不过即使在分支结点找到了待查找的关键字,它也只是用来索引的,不能提供实际记录的访问,还是需要到达包含此关键字的终端结点。

如果我们是需要从最小关键字进行从小到大的顺序查找,我们就可以从最左侧的叶子结点出发,不经过分支结点,而是延着指向下一叶子的指针就可遍历所有的关键字。

B+树的结构特别适合带有范围的查找。比如查找我们学校18~22岁的学生人数,我们可以通过从根结点出发找到第一个18岁的学生,然后再在叶子结点按顺序查找到符合范围的所有记录。

B+树的插入、删除过程也都与B树类似,只不过插入和删除的元素都是在叶子结点上进行而已。

六、B* 树

B* 树是 B+ 树的变体,在 B+ 树的非根和非叶子结点再增加指向兄弟的指针;B* 树定义了非叶子结点关键字个数至少为 (2/3)*M,即块的最低使用率为 2/3(代替B+树的1/2)。

B* 树

七、红黑二叉查找树

7.1 替换 3- 结点

红黑二叉查找树背后的基本思想是用标准的二叉查找树(完全由 2- 结点构成)和一些额外的信息(替换 3- 结点)来表示 2-3 树。

我们将树中的链接分为两种类型:

  • 红链接:将两个 2- 结点连接起来构成一个 3- 结点。
  • 黑链接:2-3 树中的普通链接。

确切地说,我们将 3- 结点表示为由一条左斜的红色链接(两个 2- 结点其中之一是另一个的左子结点)相连的两个 2- 结点,如图所示。

这种表示法的一个优点是,我们无需修改就可以直接使用标准二叉查找树的 get() 方法。对于任意的 2-3 树,只要对结点进行转换,我们都可以立即派生出一棵对应的二叉查找树。

红黑二叉查找树
由一条红色左链接相连的两个 2- 结点表示一个 3- 结点。

7.2 一种等价的定义

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

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

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

7.3 一一对应

如果我们将一棵红黑树中的红链接画平,那么所有的空链接到根结点的距离都将是相同的(如图所示)。如果我们将由红链接相连的结点合并,得到的就是一棵 2-3 树。

相反,如果将一棵 2-3 树中的 3- 结点画作由红色左链接相连的两个 2- 结点,那么不会存在能够和两条红链接相连的结点,且树必然是完美黑色平衡的。

红黑树既是二叉查找树,也是 2-3 树。因此,如果我们能够在保持一一对应关系的基础上实现 2-3 树的插入算法,那么我们就能够将两个算法的优点结合起来:二叉查找树中简洁高效的查找方法和 2-3 树中高效的平衡插入算法。

红黑树和 2-3 树的一一对应关系

7.4 颜色表示

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

我们约定空链接为黑色。为了代码的清晰我们定义了两个常量 RED 和 BLACK 来设置和测试这个变量。我们使用私有方法 isRed() 来测试一个结点和它的父结点之间的链接的颜色。当我们提到一个结点的颜色时,我们指的是指向该结点的链接的颜色,反之亦然。颜色表示的代码实现如图所示。

红黑树的结点表示

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 val, 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;
}

7.5 旋转

在我们实现的某些操作中可能会出现红色右链接或者两条连续的红链接,但在操作完成前这些情况都会被小心地旋转并修复。旋转操作会改变红链接的指向。

首先,假设我们有一条红色的右链接需要被转化为左链接。这个操作叫做左旋转,它对应的方法接受一条指向红黑树中的某个结点的链接作为参数。假设被指向的结点的右链接是红色的,这个方法会对树进行必要的调整并返回一个指向包含同一组键的子树且其左链接为红色的根结点的链接。

这个操作很容易理解:我们只是将用两个键中的较小者作为根结点变为将较大者作为根结点。实现将一个红色左链接转换为一个红色右链接的一个右旋转的代码完全相同,只需要将 left 换成 right 即可。

左旋转 h 的右链接:
左旋转 h 的右链接

右旋转 h 的左链接:

右旋转 h 的左链接

7.6 在旋转后重置父结点的链接

无论左旋转还是右旋转,旋转操作都会返回一条链接。我们总是会用 rotateRight() 或 rotateLeft() 的返回值重置父结点(或是根结点)中相应的链接。返回的链接可能是左链接也可能是右链接,但是我们总会将它赋予父结点中的链接。这个链接可能是红色也可能是黑色 —— rotateLeft() 和 rotateRight() 都通过将 x.color 设为 h.color 保留它原来的颜色。这可能会产生两条连续的红链接,但我们的算法会继续用旋转操作修正这种情况。

7.7 向 2- 结点中插入新键

一棵只含有一个键的红黑树只含有一个 2- 结点。插入另一个键之后,我们马上就需要将它们旋转。

如果新键小于老键,我们只需要新增一个红色的结点即可,新的红黑树和单个 3- 结点完全等价。如果新键大于老键,那么新增的红色结点将会产生一条红色的右链接。我们需要使用 root = rotateLeft(root); 来将其旋转为红色左链接并修正根结点的链接,插人操作才算完成。

两种情况的结果均为一颗和单个 3- 结点等价的红黑树,其中含有两个键,一条红链接,树的黑链接高度为 1。

向 2- 结点中插入新键

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

用和二叉查找树相同的方式向一棵红黑树中插人一个新键会在树的底部新增一个结点 (为了保证有序性),但总是用红链接将新结点和它的父结点相连。

如果它的父结点是一个 2- 结点,那么刚才讨论的两种处理方法仍然适用。如果指向新结点的是父结点的左链接,那么父结点就直接成为了一个 3- 结点;如果指向新结点的是父结点的右链接,这就是一个错误的 3- 结点,但一次左旋转就能够修正它,如图所示。

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

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

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

  • 三者中最简单的情况是新键大于原树中的两个键,因此它被连接到 3- 结点的右链接。此时树是平衡的,根结点为中间大小的键,它有两条红链接分别和较小和较大的结点相连。如果我们将两条链接的颜色都由红变黑,那么我们就得到了一棵由三个结点组成、高为 2 的平衡树。它正好能够对应一棵 2-3 树,如图(左)。其他两种情况最终也会转化为这种情况。

  • 如果新键小于原树中的两个键,它会被连接到最左边的空链接,这样就产生了两条连续的红链接,如图(中)。此时我们只需要将上层的红链接右旋转即可得到第一种情况 (中值键为根结点并和其他两个结点用红链接相连)。

  • 如果新键介于原树中的两个键之间,这又会产生两条连续的红链接,一条红色左链接接一条红色右链接,如图(右)。此时我们只需要将下层的红链接左旋转即可得到第二种情况(两条连续的红色左链接)。

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

7.10 颜色转换

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

颜色转换

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

7.11 根结点总是黑色

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

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

现在假设我们需要在树的底部的一个 3- 结点下加入一个新结点。前面讨论过的三种情况都会出现,如图所示。颜色转换会使到中结点的链接变红,相当于将它送入了父结点。这意味着在父结点中继续插入一个新键,我们也会继续用相同的办法解决这个问题。

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

7.13 将红链接在树中向上传递

图中总结的三种情况显示了在红黑树中实现 2-3 树的插人算法的关键操作所需的步骤:要在一个 3- 结点下插入新键,先创建一个临时的 4- 结点,将其分解并将红链接由中间键传递给它的父结点。重复这个过程,我们就能将红链接在树中向上传递,直至遇到一个 2- 结点或者根结点。

在沿着插入点到根结点中顺序完成以下操作,我们就能完成插入操作:

  • 如果右子结点是红色的而左子结点是黑色的,进行左旋转;
  • 如果左子结点是红色的且它的左子结点也是红色的,进行右旋转;
  • 如果左右子结点均为红色,进行颜色转换。

将红链接在树中向上传递

7.14 实现

因为保持树的平衡性所需的操作是由下向上在每个所经过的结点中进行的,将它们植人我们已有的实现中十分简单:只需要在递归调用之后完成这些操作即可。

public class RedBlackBST<Key extends Comparable<Key>, Value> {

	private Node root;
	private class Node // 含有color变量的Node对象
	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) {
		// 查找key,找到则更新值,否则为它新建一个结点
		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;
	}
	
}

红黑树的构造轨迹:

红黑树的构造轨迹

7.15 删除操作

要描述删除算法,首先我们要回到 2-3 树。和插人操作一样,我们也可以定义一系列局部变换来在删除一个结点的同时保持树的完美平衡性。这个过程比插入一个结点更加复杂,因为我们不仅要在(为了删除一个结点而)构造临时 4- 结点时沿着查找路径向下进行变换,还要在分解遗留的 4- 结点时沿着查找路径向上进行变换(同插入操作)。

7.15.1 自顶向下的 2-3-4 树

作为第一轮热身,我们先学习一个沿查找路径既能向上也能向下进行变换的稍简单的算法:2-3-4 树的插入算法。

2-3-4 树中允许存在我们以前见过的 4- 结点。它的插入算法沿查找路径向下进行变换是为了保证当前结点不是 4- 结点(这样树底才有空间来插人新的键),沿查找路径向上进行变换是为了将之前创建的 4- 结点配平,如图所示。

向下的变换和我们在 2-3 树中分解 4- 结点所进行的变换完全相同。如果根结点是 4- 结点,我们就将它分解成三个2-结点,使得树高加 1。

在向下查找的过程中,如果遇到一个父结点为 2- 结点的 4- 结点,我们将 4- 结点分解为两个 2- 结点并将中间键传递给它的父结点,使得父结点变为一一个 3-结点;如果遇到一个父结点为 3- 结点的 4- 结点,我们将 4- 结点分解为两个 2- 结点并将中间键传递给它的父结点,使得父结点变为一个 4- 结点。到达树的底部之后,我们也只会遇到 2- 结点或者 3- 结点,所以我们可以插入新的键。

要用红黑树实现这个算法,我们需要:

  • 将 4- 结点表示为由三个 2- 结点组成的一棵平衡的子树,根结点和两个子结点都用红链接相连;
  • 在向下的过程中分解所有 4- 结点并进行颜色转换;
  • 和插人操作一样,在向上的过程中用旋转将 4- 结点配平。

自顶向下的 2-3-4 树的插入算法中的变换
令人惊讶的是,你只需要移动算法 7.14 的 put() 方法中的一行代码就能实现 2-3-4 树中的插入操作:将 colorFlip() 语句(及其 if 语句)移动到递归调用之前(null 测试和比较操作之间)。

7.15.2 删除最小键

在第二轮热身中我们要学习 2-3 树中删除最小键的操作。

我们注意到从树底部的 3- 结点中删除键是很简单的,但 2- 结点则不然。从 2- 结点中删除一个键会留下一个空结点,一般我们会将它替换为一个空链接,但这样会破坏树的完美平衡性。所以我们需要这样做:为了保证我们不会删除一个 2- 结点,我们沿着左链接向下进行变换,确保当前结点不是 2- 结点(可能是 3- 结点,也可能是临时的 4- 结点)。

首先,根结点可能有两种情况。如果根是 2- 结点且它的两个子结点都是 2- 结点,我们可以直接将这三个结点变成一个 4- 结点;否则我们需要保证根结点的左子结点不是 2- 结点,如有必要可以从它右侧的兄弟结点 “借” 一个键来。

以上情况如图所示。在沿着左链接向下的过程中,保证以下情况之一成立:

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

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

删除最小键操作中的变换

7.15.3 删除操作

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

八、总结

各种符号表实现的性能总结:

各种符号表实现的性能总结

关注微信公众号:CodeFish 回复 “算法” 即可获取《算法 第四版》中英文版全网最高清PDF电子书资源!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值