【Rust日报】2023-07-05 让我们从 abandon 开始--用 rust 写链表

让我们从 abandon 开始--用 rust 写链表

虽然 Rust 的标准库中已经有了一个LinkedList数据结构,但创建自己的数据结构是了解更多 Rust 的一种有趣的方式。

节点

让我们首先定义我们的 Node 结构来保存模版。我们的 Node 将包含一个数据项,以及 Node 可能存在或可能不存在的上一个和下一个数据项。

/// A node containing a data item and links to previous and next nodes.
struct Node<T> {
    data: Rc<T>,
    prev: Link<T>,
    next: Link<T>,
}

通过将数据项存储在引用计数器指针 ( Rc) 中,在创建新节点时无需克隆或复制数据。 Rust 需要在编译时知道结构的大小,这会导致自引用数据类型出现一些问题。prev如果我们将和字段的类型设置next为Node,我们会收到如下所示的编译器错误消息:

error[E0072]: recursive type `Node` has infinite size
   --> src/data_structures/linkedlist.rs:127:1
    |
127 | struct Node<T> {
    | ^^^^^^^^^^^^^^
128 |     data: Rc<T>,
129 |     prev: Node<T>,
    |           ------- recursive without indirection

编译器错误消息为我们提供了如何继续操作的提示。我们正在被推动使用间接,这是谈论指针的另一种方式。 为了处理这个编译器错误,我们需要提供某种指针来代替Node. 我们可以定义一个自定义类型Link作为我们的间接Node包装器:

type Link<T> = Option<Rc<RefCell<Box<Node<T>>>>>;

请注意,因为我们的Link类型使用Rc(引用计数器指针)而不是Arc(原子引用计数指针),所以我们的实现Node不是线程安全的。

节点构造

我们的每个Node方法都将包含在一个块中:impl Node { ... } 这允许我们的方法获取通用类型的数据项T,使我们Node非常灵活!

现在我们知道我们的意志如何Node链接到其他人,我们可以实现该new()方法。每个都Node将开始作为通用类型的数据项保存T,并且不连接到上一个或下一个Node。

/// Creates a new Node containing the given data item. The previous and next
/// node links are set to None.
fn new(data: T) -> Node<T> {
    Node {
        data: Rc::new(data),
        prev: None,
        next: None,
    }
}

Node setters

我们更新前一个和下一个节点的方法将相当简单。使用我们的引用计数Link,我们只需要在使用我们的设置方法时克隆另一个链接。确保彼此相邻的两个节点正确指向彼此的逻辑将由我们的LinkedList.

/// Updates the previous node.
fn set_prev(&mut self, other: &Link<T>) {
    self.prev = other.clone();
}

/// Updates the next node.
fn set_next(&mut self, other: &Link<T>) {
    self.next = other.clone();
}

Node getters

与我们的 setter 方法类似Node,我们的 getter 方法只需要克隆下一个或上一个链接。

/// Gets the previous link from the Node via cloning.
fn get_prev(&self) -> Link<T> {
    self.prev.clone()
}

/// Gets the next link from the Node via cloning.
fn get_next(&self) -> Link<T> {
    self.next.clone()
}

为了获取我们存储的数据Node而不需要克隆或复制数据,我们可以返回引用计数指针的克隆。 为了方便我们的实现,我们将定义一个静态方法来从数据项LinkedList创建新的:LinkT

/// Creates a new Link containing the given data item.
fn new_link(data: T) -> Link<T> {
    Some(Rc::new(RefCell::new(Box::new(Node::new(data)))))
}

实现了我们的Node结构及其方法后,我们可以继续我们的 LinkedList 实现。

LinkedList

我们 LinkedList 需要跟踪它的第一个和最后一个节点(头节点和尾节点)及其长度。

/// An implementation of a doubly linked-list. Not thread-safe. Note that the
/// data items contained within nodes cannot be changed after they have been
/// added to the linked-list.
pub struct LinkedList<T> {
    head: Link<T>,
    tail: Link<T>,
    len: usize,
}

虽然我们可以动态计算 的长度LinkedList,但在推送和弹出数据项时跟踪其长度会更有效。

链表构造函数

我们的LinkedList方法将包含在一个块中:impl LinkedList { ... }。与我们的Node方法一样,此实现块将允许我们的LinkedList方法接受通用数据类型的数据项T,使我们的方法LinkedList像我们的方法一样灵活Node!

新的LinkedList开始是空的,没有头节点或尾节点,长度为 0。

/// Creates an empty LinkedList.
pub fn new() -> LinkedList<T> {
    LinkedList {
        head: None,
        tail: None,
        len: 0,
    }
}

push 方法

我们的 LinkedList push 方法是这个项目开始变得真正有趣的地方! 按照惯例,“push”本身意味着 push 到 LinkedList 的末尾,而“ push 到前面”正是这个意思。

要将新节点 push 到列表末尾,我们首先创建另一个包含新数据项的节点。 如果 LinkedList 为空,则新 Node 成为新的头和尾。 否则,新节点成为新尾部。 我们的 push() 方法的最后一部分确保旧尾部和新尾部彼此指向,并且 LinkedList 正在跟踪新尾部。

在 Link 实例上调用的 as_ref() 方法允许我们借用 Option 实例中持有的 Rc,而不是取得它的所有权。 借用 Rc 后,我们可以通过对嵌套在 Link 中的 RefCell 调用的borrow_mut() 方法动态借用 Link 中包含的 Node。

/// Pushes the data item to the end of the LinkedList.
pub fn push(&mut self, data: T) {
    let new_node: Link<T> = Node::new_link(data);
    // Increase the len counter
    self.len += 1;
    // Handle case for empty list
    if self.head.is_none() && self.tail.is_none() {
        self.head = new_node.clone();
        self.tail = new_node;
        return;
    }
    // Update the tail to point at the new node and connect to the old tail
    self.tail.as_ref().unwrap().borrow_mut().set_next(&new_node);
    new_node.as_ref().unwrap().borrow_mut().set_prev(&self.tail);
    self.tail = new_node;
}

我们对称的最重要的部分LinkedList是我们的push_front()方法最终类似于push(),其中头/尾和上一个/下一个的引用被交换。

/// Pushes the data item to the front of the LinkedList.
pub fn push_front(&mut self, data: T) {
    let new_node: Link<T> = Node::new_link(data);
    // Increase the len counter
    self.len += 1;
    // Handle case for empty list
    if self.head.is_none() && self.tail.is_none() {
        self.head = new_node.clone();
        self.tail = new_node;
        return;
    }
    // Update the head to point at the new node and connect to the old head
    self.head.as_ref().unwrap().borrow_mut().set_prev(&new_node);
    new_node.as_ref().unwrap().borrow_mut().set_next(&self.head);
    self.head = new_node;
}

pop 方法

我们从数据项中弹出的值LinkedList取决于它是否包含节点。与我们的推送方法一样,“pop”本身是指从末尾(尾部)删除一个值,LinkedList而“从前面弹出”就是它所说的意思。

如果我们LinkedList至少包含一个Node,我们的pop()方法将返回包含在已删除节点中的值,该值包装在 中的 aSome中Option。否则,它将返回一个None值,表明没有Node被删除。

/// Removes the last node from the LinkedList. Returns Some containing the value
/// from the removed node, otherwise None.
pub fn pop(&mut self) -> Option<Rc<T>> {
    // Handle case for empty list
    if self.head.is_none() && self.tail.is_none() {
        return None;
    }
    // Update the tail to be the second-last node and return value contained in removed node
    let old_tail = self.tail.clone();
    self.tail = old_tail.as_ref().unwrap().borrow().get_prev();
    self.tail.as_ref().unwrap().borrow_mut().set_next(&None);
    let old_data = old_tail.as_ref().unwrap().borrow().get_data();
    // Decrease the len counter
    self.len -= 1;
    Some(old_data)
}

与push 方法一样,pop 方法也是对称的。对于该pop_front()方法,与该方法相比,对 head/tail 和 prev/next 的引用被交换pop()。

/// Removes the first node from the LinkedList. Returns Some containing the
/// value from the removed node, otherwise None.
pub fn pop_front(&mut self) -> Option<Rc<T>> {
    // Handle case for empty list
    if self.head.is_none() && self.tail.is_none() {
        return None;
    }
    // Update head to be second node and return value contained in removed node
    let old_head = self.head.clone();
    self.head = old_head.as_ref().unwrap().borrow().get_next();
    self.head.as_ref().unwrap().borrow_mut().set_prev(&None);
    let old_data = old_head.as_ref().unwrap().borrow().get_data();
    // Decrease the len counter
    self.len -= 1;
    Some(old_data)
}

Checking length

现在检查我们的长度LinkedList非常简单,因为我们一直在推送和弹出节点时跟踪它们。

/// Returns the number of items contained in the LinkedList.
pub fn len(&self) -> usize {
    self.len
}

为了完整起见,我们应该实现一个is_empty()方法,因为我们有一个len()方法。如果该字段等于 0,我们的LinkedList将为空。len

/// Checks if the LinkedList is empty.
pub fn is_empty(&self) -> bool {
    self.len == 0
}

测试

为代码编写和使用测试用例是一个很好的做法。这样您就可以知道您的实现是否按预期运行,并在代码更改发生重大变化时保持警惕。

下面是一些示例测试方法,我们可以使用它们来检查LinkedList. 这些测试方法绝不是排他性的——您可以随意自己编写更多测试用例。您可以使用命令执行测试方法cargo test。

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_push_back_length() {
        let mut new_list = LinkedList::<i32>::new();
        let values = (0..10).collect::<Vec<i32>>();
        for i in values {
            new_list.push(i);
        }
        assert_eq!(new_list.len(), 10);
    }

    #[test]
    fn test_push_front_length() {
        let mut new_list = LinkedList::<i32>::new();
        let values = (0..10).collect::<Vec<i32>>();
        for i in values {
            new_list.push_front(i)
        }
        assert_eq!(new_list.len(), 10);
    }
}
  • 源码

  • https://cmooneycollett.github.io/2023/07/01/writing-a-linkedlist-in-rust


From 日报小组 Koalr

社区学习交流平台订阅:

  • Rustcc论坛: 支持rss

  • 微信公众号:Rust语言中文社区

猜你喜欢

转载自blog.csdn.net/u012067469/article/details/131606539