双向链接的红黑树(一):基础概念和插入

双向链接的红黑树(一):基础概念和插入

1.基础概念

首先我们还是先来看看双向链接红黑树的概念,其实和双向链接BST相似,我们只是在此前的基础上引入红黑节点,其他的概念保持不变。下图展示了一个完整的双向链接红黑树:

在这里插入图片描述

基本和之前的双向链接BST相似,所以这里不再详细介绍双向链接红黑树的概念。同样,经过这样处理的红黑树将不是一颗树,严格意义上来说,它是个连接图(Connected Graph)。

2.直接插入

2.1 思路分析

在讲解直接插入操作之前,我们先来简单回忆一下原版红黑树的插入操作,主要涉及两个步骤:

  1. 自顶向下查找需要插入的位置;
  2. 在相应位置插入红节点,之后自底向上进行自平衡;

同样,如果你对上述步骤不是很熟悉,请参考:红黑树(二):旋转和插入。那么,大家可以想想,现在我们需要在某个位置直接插入节点,上述两个步骤哪些是必须的,哪些又是多余的呢?其实答案应该很简单,对于直接插入,第一步查找插入位置是多余,第二步是必须的,所以接下来我们可以思考这么一个问题,既然现在我们还是需要利用之前的部分思路,那么是否可以重复利用之前的代码呢?因为这样可以大幅简化代码。

大家应该还记得put()的代码逻辑,递归自顶向下查找,然后利用递归栈自底向上自平衡,大体的代码逻辑如下:

put( root, key, val ) {
    // base case, attach the new node to this position
    if ( root == null ) return new node ( key, val, RED );

    int res = compareKeys( root, key );
    // the node should be attached in the left subtree
    if ( res > 0 ) root.left = put( root.left, key, val );
    // the node should be attached in the right subtree
    else if ( res < 0 ) root.right = put( root.right, key, val );
    // added before, update value
    else root.val = val;

    // update size and restore this R-B tree
    return balance( root );
}

那我们是否可以复用上面的代码逻辑呢?答案是不太合适,因为在直接插入中,我们没有了查找插入位置的过程,所以原先的递归栈也自然不存在。但是递归栈中存储的自底向上的路径,我们还是需要的,因为需要进行自平衡。那么,我们应该怎们去解决这些问题呢?首先,大家需要注意到下面两点:

  1. 现在的红黑树引入了双向节点结构;
  2. 理论上,递归和迭代是可以相互转换的;

对于第一点,我们以上面的红黑树为例,假如最后我们插入了A节点,那么大家知道如何找到之后自底向上自平衡的路径呢?答案也很简单,我们可以利用双向指向的节点,一级一级向上寻找需要自平衡的路径,直到抵达root,也就是父节点指针充当了我们的“指路人”,它的存在功不可没。现在我们解决了自平衡路径问题,那么实际代码的逻辑是什么样子的呢?

既然我们有了路径,我们就可以循环一级一级向上查找并自平衡,并且循环的终止条件即为父节点指针为null,即抵达root(root没有父节点)。分析到这里,我们就可以来看看代码应该如何书写了。

2.2 代码分析

首先,我们来看看整体的代码:

protected void put( RBTreeNode<K, V> root, RBTreeNode<K, V>  node,
                    boolean isLeft ) {

    // doubly-connected node, root <-> child.
    // note that we have a connected graph, instead of a tree
    if ( isLeft ) {
        assert root.left == null;
        root.left = node;
        linkedList.addBefore( root.node, node.node );
    }
    else {
        assert root.right == null;
        root.right = node;
        linkedList.addAfter( root.node, node.node );
    }
    node.parent = root;

    // self-balancing process
    // update size and restore this R-B tree
    // all the way to the root node
    do {
        // root may change to another node after balancing,
        // we need to keep a copy of it
        RBTreeNode<K, V>  origin = root;
        root = balance( root );
        // only RedBlackTreeNode used here, so ignore the warning
        @SuppressWarnings( "unchecked" )
        RBTreeNode<K, V>  parent = ( RBTreeNode<K, V>  ) root.parent;
        // restore root after balancing
        if ( parent == null ) this.root = root;
        // restore parent's child after balancing
        else {
            if ( parent.left == origin ) parent.left = root;
            else {
                assert parent.right == origin;
                parent.right = root;
            }
        }

        root = parent;
    } while( root != null );

    ( ( RBTreeNode<K, V> ) this.root ).color = BLACK;

    if ( !check() )
        throw new IllegalArgumentException( "Inserted node is not at the right position" );
}

因为我们已经知道插入到哪个节点的左还是右孩子,所以一开始我们就根据传入的参数进行插入:

// doubly-connected node, root <-> child.
// note that we have a connected graph, instead of a tree
if ( isLeft ) {
    assert root.left == null;
    root.left = node;
    linkedList.addBefore( root.node, node.node );
}
else {
    assert root.right == null;
    root.right = node;
    linkedList.addAfter( root.node, node.node );
}
node.parent = root;

注意这里我们还需要更新一下双向链表的结构。同样地,插在左孩子,链表中插在节点的前面,反之插入到节点的后面。在完成插入操作之后,我们就需要根据父节点指针,向上进行自平衡操作:

// self-balancing process
// update size and restore this R-B tree
// all the way to the root node
do {
    // root may change to another node after balancing,
    // we need to keep a copy of it
    RBTreeNode<K, V>  origin = root;
    root = balance( root );
    // only RedBlackTreeNode used here, so ignore the warning
    @SuppressWarnings( "unchecked" )
    RBTreeNode<K, V>  parent = ( RBTreeNode<K, V>  ) root.parent;
    // restore root after balancing
    if ( parent == null ) this.root = root;
    // restore parent's child after balancing
    else {
        if ( parent.left == origin ) parent.left = root;
        else {
            assert parent.right == origin;
            parent.right = root;
        }
    }

    root = parent;
} while( root != null );

自平衡过程的逻辑也很简单,达到一级的节点,我们对其进行自平衡,然后向上一级进行处理。只是这里需要注意一下,抵达root的情况:如果抵达root,需要将树指向root的指针进行重定向,如果没有抵达root,需要修复双向指向关系,这点不要忘记了,否则之后再进行自平衡,则不能得到正确的向上路径。

所以说,直接插入还是相对比较简单的,大家可以记住:

我们利用双向指向关系,确定向上自平衡的路径,然后利用迭代一级一级向上自平衡即可,其余和原版红黑树没有任何区别,相关代码都可以复用。

另外,关于双向链接红黑树的直接前后继问题,它和之前BST的是一模一样的,因为两者投影到数轴上的结果都是有序数组,所以和之前的思路完全一致。

接下来,我们将进入到双向链接红黑树最难的操作——删除操作。大家可以看到因为自平衡的问题(主要是旋转改变树的结构),我们的自平衡路径寻找将会遇到不小的问题。


上一节:双向链接的二叉查找树(二):直接插入和删除
下一节:双向链接的红黑树(二):直接删除
系列汇总:塞尔达和计算几何 | Voronoi图详解文章汇总(含代码)

3. 免责声明

※ 本文之中如有错误和不准确的地方,欢迎大家指正哒~
※ 此项目仅用于学习交流,请不要用于任何形式的商用用途,谢谢呢;


在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值