Java实现红黑树中红黑二叉查找树的详细过程

121 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情

Java实现红黑树中红黑二叉查找树的详细过程

红黑树接近平衡的二叉树,插入,删除函数跟平衡二叉树一样,只是平衡函数不同,

前言

在实现红黑树之前,我们先来了解一下符号表。

符号表有时候被称为字典,就如同英语字典中,一个单词对应一个解释,符号表有时候又被称之为索引,即书本最后将术语按照字母顺序列出以方便查找的那部分。总的来说,符号表就是将一个键和一个值联系起来,就如Python中的字典,JAVA中的HashMap和HashTable,Redis中以键值对的存储方式。

在如今的大数据时代,符号表的使用是非常频繁的,但在一个拥有着海量数据的符号表中,如何去实现快速的查找以及插入数据就是高效的算法去完成的事情,可以说没有这些算法的发明产生,信息时代无从谈起。

既然是数据结构去实现符号表,这就要求我们对符号表的API,也就是符号表的功能去定义,前面我们说到既然符号表的使用是如何在海量数据中去查找,插入数据,那么我们便定义符号表的API有增删改查这四个基本功能。

/**
 * <p>
 *     符号表的基本API
 * </p>
 */
public interface RedBlackBST<Key extends Comparable<Key>,Value> {

    /**
     * 根据Key在符号表中找到Value
     * @param key the key
     * @return the value of key
     */
    Value get(Key key);

    /**
     * 插入key-value,如果符号表中有Key,且Key不为空则将该Key的Value转为传入的Value
     * @param key the-key
     * @param value the-value
     */
    void put(Key key,Value value);

    /**
     * 根据Key去符号表中删除Key
     * @param key the key
     */
    void delete(Key key);

}


这里由于红黑树是平衡二叉树,即意味着其有平衡性和有序性,因为其有序性的特点,因此我们可以范围或根据位置去需找键,也可以查找到树中的最小键和最大键。

因此我们可以额外的定义:

    /**
     * 根据位置返回键,如果没有返回null
     * @param k the index of key
     * @return the key
     */
    Key select(int k);

    /**
     * 返回红黑树中最小的键
     * @return the min key in this tree
     */
    Key min();

    /**
     * 返回红黑树中最大的键
     * @return the max key in this tree
     */
    Key max();

    /**
     * 返回小于该键的数量
     * @param key the key
     * @return amount of key small than the key
     */
    int rank(Key key);

接下来我们说说红黑树。

红黑二叉查找树

红黑二叉查找树实际上基于二叉查找树上实现了2-3树,也就是说红黑二叉查找树是一个2-3树。所以在认识红黑二叉查找树之前,我们得了解2-3树的原理,以及组成结构。

我们把含有一个键,两个链接的结点称为2-结点,标准的二叉查找树其每个结点都是2-结点,在考虑好的情况下,我们构造标准二叉查找树,一般能够得到树高为总键树的对数的一个查找树,其查找和插入操作都是对数级别的,但标准二叉查找树的基本实现的良好性能取决于键值对分布的足够乱以致于打消长路径带来的问题,但我们不能保证插入情况是随机的,如果键值对的插入时顺序插入的,就会带来下面的问题:

从图中我们可以看到,我们将A,B,C,D,E按顺序插入的话,会得到一个键值与树高成正比的二叉查找树,其插入和查找的会从对数级别提到O(N)级别。

当然我们希望的肯定是无论键值对的情况是怎样的,我们都能构造一个树高与总键数成对数,插入查找等操作均能够在对数时间内完成的数据结构。也就是说,在顺序插入的情况下,我们希望树高依然为lgN,这样我们就能保证所有的查找都能在lgN次比较结束。

为了保证查找树的平衡性,我们需要一些灵活性,因此在这里我们允许树中的一个结点保存多个键,我们引入3-结点,所谓的3-结点就是一个结点中有2个键,3个链接。

因此一颗查找树或为一颗空树,或由2-结点和3-结点组成。在介绍2-3树的操作前,我们将A,B,C,D,E,F,G,H顺序插入得到的树如下图所示:

从图中我们可以看出2-3树的平衡性,灵活性,它保证了任意的插入得到的树高依旧是总键的对数。

树的插入操作

理解2-3树的插入操作,有利于去构造红黑树,在这里分三种情况:

  1. 插入新键,底层结点是2-结点
  2. 插入新键,底层结点是3-结点,父结点是2-结点
  3. 插入新键,底层结点是3-结点,父结点是3-结点

第一种情况

若插入新键,底层结点是2-结点的话,该底层结点变为3-结点,将插入的键保存其中即可。

第二种情况

若插入新键,底层结点是3-结点,底层结点先变成临时的4-结点(3个键,4条链接),后4-结点中的中键吐出,使得父节点由2-结点变为3-结点,原4-结点中键两边的键变成两个2-结点,原本由父结点指向子结点的一个链接,替换为原4-结点中键左右两边的链接,分别指向两个新的2-结点。

第三种情况

若插入新键,底层结点是3-结点,其父结点也是3-结点的话,使得底层结点变为临时的4-结点,后4-结点中的中键吐出,使得父节点由3-结点变为临时的4-结点,原4-结点中键两边的键变成两个2-结点,原本由父结点指向子结点的一个链接,替换为原4-结点中键左右两边的链接,分别指向两个新的2-结点,随后父节点也要吐出中键,重复上述的步骤,如果父节点的父节点也是3-结点,则继续持续上述步骤,若根结点也是3-结点,根节点吐出中键,生成两个2-结点后,整个树高+1,但各个底层结点到根结点的路径始终相等。

以上的三种变化是2-3树的动态变化的核心,非常关键,我们可以在推演的过程种看到这种变化是自下向上的,而且是局部的变化,这种局部的变化并没有影响2-3树的有序性和平衡性。

同时我们也可以看出,如果要以代码来实现2-3树的话相当的麻烦,因为需要处理的情况实在太多。我们需要维护两种不同类型的结点,将被查找的键和结中的每个键进行比较,将链接和其他信息从一个结点复制到另一个结点。实现这些需要大量的代码,实现的这些代码所带来开销或许还会比标准二叉查找树要多。因此后面人们想出了结合标准二叉树来实现2-3树的数据结构,这便是红黑树。