【问题标题】:Red-Black Tree in Rust, getting 'expected struct Node, found mutable reference'Rust 中的红黑树,得到“预期的结构节点,找到可变引用”
【发布时间】:2020-12-07 06:23:19
【问题描述】:

我正在尝试在 Rust 中实现红黑树。经过2天与编译器的斗争,我准备放弃并在这里寻求帮助。

这个问题对我帮助很大:How do I handle/circumvent "Cannot assign to ... which is behind a & reference" in Rust?

我查看了 Rust 中 RB-Trees 的现有示例代码,但我看到的所有示例代码都使用了某种形式的不安全操作或 null,我们不应该在这里使用。

我有以下代码:

#[derive(Debug, Clone, PartialEq)]
pub enum Colour {
    Red,
    Black,
}

type T_Node<T> = Option<Box<Node<T>>>;

#[derive(Debug, Clone, PartialEq)]
pub struct Node<T: Copy + Clone + Ord> {
    value: T,
    colour: Colour,
    parent: T_Node<T>,
    left: T_Node<T>,
    right: T_Node<T>,
}

impl<T: Copy + Clone + Ord> Node<T>
{
    pub fn new(value: T) -> Node<T>
    {
        Node {
            value: value,
            colour: Colour::Red,  // add a new node as red, then fix violations
            parent: None,
            left: None,
            right: None,
            // height: 1,
        }
    }

    pub fn insert(&mut self, value: T)
    {
        if self.value == value
        {
            return;
        }

        let mut leaf = if value < self.value { &mut self.left } else { &mut self.right };

        match leaf
        {
            None =>
            {
                let mut new_node = Node::new(value);
                new_node.parent = Some(Box::new(self));
                new_node.colour = Colour::Red;

                (*leaf) = Some(Box::new(new_node));
            },
            Some(ref mut leaf) =>
            {
                leaf.insert(value);
            }
        };
    }
}

new_node.parent = Some(Box::new(self)); 行给了我错误。 我理解为什么会发生错误(self 被声明为可变引用)并且我不知道如何解决这个问题,但我需要 self 成为可变引用以便我可以修改我的树(除非你可以建议更好的东西)。

我尝试将 T_Node 声明为具有可变引用,而不仅仅是 Node,但这只会产生更多问题。

我也愿意接受关于更好地选择变量类型的建议。

感谢任何帮助。

【问题讨论】:

  • 您可以创建一个根结构,就像链接帖子中的 LinkedList 结构一样。这样你就不需要交换self,只需要交换self.start
  • 哦,我的错,我没有看到你在哪里引用父节点。这是一个与所有权规则不符的设计(因为现在一个节点由它的父节点和子节点共同拥有)。在这里您可以找到一些双链表的解决方法:stackoverflow.com/questions/22268861/… rust-unofficial.github.io/too-many-lists/index.html 也可能是在 rust 中构建新集合类型的有趣介绍
  • 我希望这最终归结为 this question 的副本,因为 left.parentright.parent 都循环回 self

标签: rust red-black-tree-insertion


【解决方案1】:

设计中存在一些缺陷,如果不进行一些更改,就无法继续前进。

首先,Box 不支持共享所有权,但您需要这样做,因为父节点 (rbtree.right/rbtree.left) 和子节点 (rbtree.parent) 引用了相同的节点。为此,您需要Rc

因此,您需要切换到Rc,而不是Box

type T_Node<T> = Option<Rc<Node<T>>>;

但这并不能解决问题。现在您的节点位于Rc 内,而Rc 不允许对其内容进行突变(您可以通过get_mut 进行突变,但这要求它是唯一的,这在您的情况下不是常数)。除非你可以改变一个节点,否则你将无法对你的树做很多事情。

所以你需要使用interior mutability pattern。为此,我们将添加一个额外的层RefCell

type T_Node<T> = Option<Rc<RefCell<Node<T>>>>;

现在,这将允许我们改变里面的内容。

但这并不能解决问题。因为您还需要持有从子级到父级的引用,所以您最终会创建一个引用循环。

幸运的是,rust book explains how to fix reference cycle for the exact same scenario

为了让子节点知道它的父节点,我们需要在我们的节点结构定义中添加一个父字段。问题在于决定父母的类型应该是什么。我们知道它不能包含 Rc,因为这会创建一个引用循环,其中 leaf.parent 指向分支,branch.children 指向叶子,这将导致它们的 strong_count 值永远不会为 0。换一种方式考虑关系,一个父节点应该拥有它的子节点:如果一个父节点被删除,它的子节点也应该被删除。但是,子节点不应拥有其父节点:如果我们删除子节点,则父节点应该仍然存在。这是弱引用的情况!

所以我们需要 child 持有对 parent 的弱引用。这可以这样做:

type Child<T> = Option<Rc<RefCell<Node<T>>>>;
type Parent<T> = Option<Weak<RefCell<Node<T>>>>;

现在我们已经修复了大部分设计。

我们应该做的另一件事是,与其直接暴露Node,不如将​​其封装在一个结构RBTree中,该结构将保存树的rootinsertsearch等操作,delete等可以在RBtree上调用。这将使事情变得简单,实现将变得更加合乎逻辑。

pub struct RBTree<T: Ord> {
    root: Child<T>,
}

现在,让我们编写一个类似于您的 insert 实现:

impl<T: Ord> RBTree<T> {
    pub fn insert(&mut self, value: T) {
        fn insert<T: Ord>(child: &mut Child<T>, mut new_node: Node<T>) {
            let child = child.as_ref().unwrap();
            let mut child_mut_borrow = child.borrow_mut();

            if child_mut_borrow.value == new_node.value {
                return;
            }

            let leaf = if child_mut_borrow.value > new_node.value {
                &mut child_mut_borrow.left
            } else {
                &mut child_mut_borrow.right
            };

            match leaf {
                Some(_) => {
                    insert(leaf, new_node);
                }
                None => {
                    new_node.parent = Some(Rc::downgrade(&child));
                    *leaf = Some(Rc::new(RefCell::new(new_node)));
                }
            };
        }

        let mut new_node = Node::new(value);

        if self.root.is_none() {
            new_node.parent = None;
            self.root = Some(Rc::new(RefCell::new(new_node)));
        } else {
            // We ensure that a `None` is never sent to insert()
            insert(&mut self.root, new_node);
        }
    }
}

我在RBTree::insert 中定义了一个insert 函数,只是为了简化递归调用。在嵌套的insert 函数中执行对根和进一步插入的外部函数测试。

基本上,我们从:

let mut new_node = Node::new(value);

这会创建一个新节点。

那么,

if self.root.is_none() {
    new_node.parent = None;
    self.root = Some(Rc::new(RefCell::new(new_node)));
} else {
    // We ensure that a `None` is never sent to insert()
    insert(&mut self.root, new_node);
}

如果根是None,则插入root,否则调用insert 本身带有root。所以嵌套的insert函数基本上接收到检查左右孩子并进行插入的父级。

然后,控件移动到嵌套的insert 函数。

为了方便访问内部数据,我们定义了以下两行:

let child = child.as_ref().unwrap();
let mut child_mut_borrow = child.borrow_mut();

就像在您的实现中一样,如果值已经存在,我们将返回:

if child_mut_borrow.value == new_node.value {
    return;
}

现在我们存储一个对左或右孩子的可变引用:

let leaf = if child_mut_borrow.value > new_node.value {
    &mut child_mut_borrow.left
} else {
    &mut child_mut_borrow.right
};

现在,检查孩子是None 还是Some。对于None,我们进行插入。否则,我们递归调用insert

match leaf {
    Some(_) => {
        insert(leaf, new_node);
    }
    None => {
        new_node.parent = Some(Rc::downgrade(&child));
        *leaf = Some(Rc::new(RefCell::new(new_node)));
    }
};

Rc::downgrade(&amp;child) 用于生成弱引用。

这是一个工作示例:Playground

【讨论】:

  • 谢谢@Mihir。我非常感谢所有的帮助和 cmets。我正在逐行浏览代码,在插入函数中,我曾经收到错误消息,称value 是私有成员。我将它重命名为n_value,现在在if child_mut_borrow.n_value == new_node.n_value 之类的行中有n_value,以及leftright,我得到no field 'n_value' on type '&amp;mut &amp;Rc&lt;RefCell&lt;rbtree::Node&lt;T&gt;&gt;&gt;'
  • @nurchi, valueRefCell 的私人成员。很难用一行来判断你的情况发生了什么。您能否改为更新问题中的代码,或者如果您可以提供playground link 相同的代码,效果会更好。
  • 另外,在我回答的最后,我添加了一个指向游乐场的链接。这包含相同的功能代码。这可能会有所帮助。
  • 它与包含有关。当我输入.borrow_mut() 时,Visual Studio Code 决定添加对std::{borrow::BorrowMut 的引用,这肯定是别的东西。很抱歉造成混乱。
  • 是的,自动导入问题很常见。另外,请注意,上述实现并没有真正结合红黑树修复来平衡树。所以,它只是一个普通的 bst 插入。您必须为案例编写 rb-tree-fixup 代码(在 2 月进行我的算法考试,当您需要记住算法时,rb 树很糟糕,仍然无法正确:p)。
猜你喜欢
  • 2011-03-21
  • 2018-05-15
  • 1970-01-01
  • 2015-11-17
  • 2012-06-03
  • 1970-01-01
  • 2017-03-02
  • 1970-01-01
  • 2011-09-18
相关资源
最近更新 更多