数据结构:红黑树

红黑树

红黑树是一种自平衡二叉查找树,可以在 O ( l o g ( n ) ) O(log(n)) O(log(n))时间内完成查找、插入和删除。相对于AVL树来说,牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能要优于AVL树。红黑树较AVL树等二叉树的不同在于其有个标识位(红色或者黑色),这个标识位的作用在于减少因不平衡造成的旋转操作,虽然此时树不是完全平衡二叉树,但依然有 O ( l o g ( n ) O(log(n) O(log(n)的时间效率。

红黑树的性质

红黑树要满足如下性质:

  1. Each node is either red or black. 节点是红色或者黑色
  2. The root is black. 根是黑色
  3. All leaves (NIL) are black. 所有叶子(NIL节点)都是黑色
  4. If a node is red, then both its children are black. 每个红色节点必须有两个黑色节点(从每个叶子节点到根的所有路径上不能有两个连续的红色节点)
  5. Every path from a given node to any of its descendant NIL nodes contains the same number of black nodes. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点

这些约束确保了红黑树的关键特性:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的,这与扩展AVL树(增大平衡因子以减少旋转,提高效率)的思想基本一致。红黑树相较于AVL树,对平衡性的要求有了一定的降低,以AVL树的角度看红黑树,有些时候即使平衡因子超过1也不会触发旋转操作,但不意味着红黑树的平衡性能不好(在保证整体平衡性的情况下牺牲一点点平衡性以减少旋转换取时间效率),正是这个特性使得红黑树的效率优于AVL树。

红黑树这个关键特性是为什么呢?解析如下:由性质4可推导出一个路径不能有两个毗连的红色节点,而性质5又要求从任一节点到每个叶子节点的所有简单路径都包含相同数目的黑色节点。我们知道,最短的可能路径是节点都是黑色,最长的可能路径是有交替的红色和黑色节点,因为性质5,黑色节点数目相同,所以可以得出没有路径能多于任何其他路径的两倍长。

红黑树的实现

现在我们开始实现红黑树,摆在我们面前的问题有:新插入一个节点,它应该是红色的呢还是黑色的呢?在插入或删除操作中,如果破坏了红黑树的性质,该怎么操作呢?这里最重要的插入和删除操作。

插入

我们首先以二叉查找树的方法增加节点并标记它为红色。如果设为黑色,由于性质5,会导致根到叶子的路径上有一条路上多了一个额外的黑节点,这个是很难调整的,但设为红色节点后,可能会出现两个连续红色节点的冲突,那么可通过颜色调换和树旋转来调整。这里树旋转与AVL树类似。

我们继续分析插入操作的情况:

  • 性质1和性质3总是保持着;
  • 性质4只在增加红色节点、重绘黑色节点为红色,或做旋转时受到威胁;
  • 性质5只在增加黑色节点、重绘红色节点为黑色,或做旋转时受到威胁;

在下面的示意图中,将要插入的节点标为N,N的父节点标为P,N的祖父节点标为G,N的叔父节点标为U。如同AVL插入的过程,插入分为如下几种情形:

叔父节点:父节点的兄弟节点。

情形1: 新节点N位于树的根上,没有父节点。这种情形下,我们把它重绘为黑色以满足性质2。

void insert_case1(Node* n) {
    if (nullptr == n->parent)
        n->color = ecolor::black;
    else
        insert_case2(n);
}

情形2: 新节点N的父节点P是黑色。在这种情形下,因为新节点N是红色,性质4与性质5都没有发生改变,此时树仍然有效。

void insert_case2(Node* n) {
    if (n->parent->color == ecolor::black)
        return;
    else
        insert_case3(n);
}

情形3: 如果父节点P和叔父节点U都是红色。则我们可将P和U重绘为黑色并重绘G为红色。但这样会带来如下问题:重绘为红色的祖父节点G有可能是根节点,这违反了性质2,也有可能祖父节点G的父节点是红色的,这违反了性质4,为了解决这个问题,我们在祖父节点G上递归地进行情形1的整个过程。

void insert_case3(Node* n) {
    if (n->uncle() != nullptr && n->uncle()->color == ecolor::red) {
        n->parent->color = ecolor::black;
        n->uncle()->color = ecolor::black;
        n->grandparent()->color = ecolor::red;
        insert_case1(n->grandparent());
    } else
        insert_case4(n);
}

情形4: 如果父节点P是红色而叔父节点U是黑色或者是缺少(NIL),并且新节点N是其父节点P的右子节点而父节点P又是其父节点的左子节点。(镜像对称的情况也是如此)。在这种情形下,我们进行一次坐旋转调换新节点和其父节点的角色;接着,我们按情形5处理以前的父节点P以解决仍然失效的性质4。

void insert_case4(Node* n) {
    if (n == n->parent->right && n->parent == n->grandparent()->left) {
        rotate_left(n);     //fixme
        n = n->left;
    } else if (n == n->parent->left && n->parent == n->grandparent()->right) {
        rotate_right(n);    // fixme
        n = n->right;
    }
    insert_case5(n);
}

情形5: 如果父节点P是红色而叔父节点U是黑色或者缺少(NIL),新节点N是其父节点的左子节点,而父节点P又是其父节点G的左子节点。(镜像对称的情况也是如此)。在这种情形下,我们进行一次针对祖父节点G的右旋转;在旋转产生的树中,以前的父节点P现在是新节点N和以前的祖父节点G的父节点。我们知道以前的祖父节点G一定是黑色(否则违反性质4),右旋后,我们切换以前的父节点P和祖父节点G的颜色,这样既满足了性质4又保证了性质5。

void insert_case5(Node* n) {
    n->parent->color = ecolor::black;
    n->grandparent()->color = ecolor::red;
    if (n == n->parent->left && n->parent == n->grandparent()->left)
        rotate_right(n->parent);    //fixme
    else
        rotate_left(n->parent);     //fixme
}
删除

删除操作要比插入复杂一些。如果要删除的节点有两个儿子,那么问题可以被转化成删除另一个只有一个儿子的节点的问题。这是因为删除一个节点可以用复制删除,寻找一个左子树的最大值或右子树的最小值节点复制到要被删除的节点,然后问题转化为删除那个最大值或最小值节点。

我们接着分析,如果我们删除一个红色节点,它的父节点和子节点一定是黑色,所以我们可以简单的用它的黑色儿子替换它,并不会破坏性质3和性质4,同时继续保证性质5。另一种简单情况是在被删除节点是黑色,而它的子节点是红色的时候,这时候用红色子节点替换被删除的节点后,性质5被破坏,只要将红色重绘为黑色,性质5即可继续保持。

比较麻烦的情况是要删除的节点和它的子节点都是黑色的时候,这时候,删除节点并将它的子节点替换后,性质5被破坏,对这种情况,类似插入操作,我们将分情况讨论。我们称被删除节点的子节点为N,称呼它的兄弟为S, S L S_L SL称呼S的左子节点, S R S_R SR称呼S的右子节点。
情形1: N是新的根节点。在这种情形下,新根节点是黑色的,所有性质都保持着。

void delete_case1(Node* n) {
    if (n->parent != nullptr)
        delete_case2(n);
}

在情形2、5和6下,我们假定N是它父亲的左子节点,如果它是右子节点,则在这些情形下的左和右对调即可。

情形2: S是红色。在这种情形下我们在N的父节点上做左旋转,把红色兄弟S转换成N的祖父,我们接着对调N的父节点和祖父节点的颜色。完成这两个操作后,尽管所有路径上黑色节点的数目没有改变,但现在N有了一个黑色的兄弟和一个红色的父亲,所以可以接下去按情形4、5或6来处理。(图中省略了被删除节点X,实际上是P->X->N删除X节点后变成P->N)

void delete_case2(Node* n) {
    Node* s = n->sibling();
    if (s->color == ecolor::red) {
        n->parent->color = ecolor::red;
        s->color = ecolor::black;
        if (n == n->parent->left)
            rotate_left(n->parent);
        else 
            rotate_right(n->parent);
    }
    delete_case3(n);
}

情形3: N的父节点、S和S的子节点都是黑色的。在这种情形下,我们简单的重绘S为红色。结果是通过S的所有路径,它们就是以前不通过N的那些路径,都少了一个黑色节点。因为删除N的初始的父节点(就是被删除掉的那个节点)使通过N的所有路径少了一个黑色节点,这时重新平衡了起来。但是通过P的所有路径现在比不通过P的路径少了一个黑色节点,所有性质5并没有充分满足,要修正这个问题,我们需要从情形1开始,在P上重新平衡处理。

void delete_case3(Node* n) {
    Node* s = n->sibling();
    if (n->parent->color == ecolor::black && s->color == ecolor::black && s->left->color == ecolor::black && s->right->color == ecolor::black) {
        s->color == ecolor::red;
        delete_case1(n->parent);
    } else
        delete_case2(n);
}

情形4: S和S的子节点都是黑色,但是N的父节点是红色。在这种情形下,我们简单的交换N的兄弟节点和父节点的颜色。这不影响不通过N的路径的黑色节点的数目,但是它在通过N的路径上对黑色节点数目增加了一,添补了在这些路径上删除的黑色节点,保持了性质5。

void delete_case4(Node* n) {
    Node* s = n->sibling();
    if (n->parent->color == ecolor::red && s->color == ecolor::red && s->left->color == ecolor::black && s->right->color == ecolor::black) {
        s->color = ecolor::red;
        n->parent->color = ecolor::black;
    } else
        delete_case5(n);
}

情形5: S是黑色,S的左子节点是红色,S的右子节点是黑色,而N是其父节点的左儿子。在这种情形下我们在S上做右旋转,这样S的左子节点成为S的父节点和N的兄弟节点。我们接着交换S和它的新父节点的颜色。现在N有了一个黑色兄弟节点,而它的右子节点是红色的,我们进入情形6。

void delete_case5(Node* n) {
    Node* s = n->sibling();
    if (s->color == ecolor::black) {
        if (n == n->parent->left && s->right->color == ecolor::black && s->left->color == ecolor::red) {
            s->color = ecolor::red;
            s->left->color = ecolor::black;
            rotate_right(s);
        } else if (n == n->parent->right && s->left->color == ecolor::black && s->right->color == ecolor::red) {
            s->color = ecolor::red;
            s->right->color == ecolor::black;
            rotate_left(s);
        }
    }
    delete_case6(n);
}

情形6: S是黑色,S的右子节点是红色,而N是它父节点的左子节点。在这种情形下我们在N的父节点上做左旋转,这样S成为N的父节点P和S的右子节点的父亲。我们接着交换N的父节点P和S的颜色,并使S的右子节点为黑色。子树在它的根上仍是同样的颜色。这样,保持了性质5。

void delete_case6(Node* n) {
    Node* s = n->sibling();
    s->color = n->parent->color;
    n->parent->color = ecolor::black;

    if (n == n->parent->left) {
        s->right->color = ecolor::black;
        rotate_left(n->parent);
    } else {
        s->left->color = ecolor::black;
        rotate_right(n->parent);
    }
}

综上,可以看到,删除一个节点,最多有3次旋转,比插入要稍微复杂一点。

红黑树与AVL树

红黑树与AVL树都是为了解决二叉查找树的不平衡问题,因为不平衡会降低树的各种操作(插入、查找、删除)的效率。AVL树的平衡因子为1,而红黑树则保证最长可能路径不超过最短可能路径的二倍长,这都是为了树的相对平衡,AVL树较红黑树平衡性更好,但相应的代价就是频繁的旋转操作,而红黑树则是牺牲了较小的平衡性换取了更少的维持平衡的额外代价。所以,相对而言,如果是会频繁插入、删除的场景,红黑树的性能更好一些(例如Linux中的epoll),而如果是查询多,而插入、删除非常少的场景,可能AVL树的性能更优,具体使用是用何种树应结合实际场景具体分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值