Rust 练习册 105:从零开始实现链表数据结构

在计算机科学中,链表是最基础也是最重要的数据结构之一。它不仅是理解更复杂数据结构的基石,也是学习指针和内存管理概念的重要工具。今天我们要探讨的是如何在Rust中实现一个简单的链表,这将帮助我们深入理解Rust的所有权系统和内存安全特性。

链表的基本概念

链表是一种线性数据结构,其中的元素不是在内存中连续存储的,而是通过指针连接在一起。每个元素(节点)包含两部分:

  1. 数据部分:存储实际的数据
  2. 指针部分:指向下一个节点的引用

链表的主要优势:

  • 动态大小:可以在运行时增长或缩小
  • 高效插入/删除:在已知位置插入或删除元素的时间复杂度为O(1)
  • 内存利用率高:只分配需要的内存

主要劣势:

  • 随机访问效率低:访问特定位置的元素需要O(n)时间
  • 额外的内存开销:需要存储指针

问题描述

我们的任务是实现一个简单的单向链表,它需要支持以下操作:

rust 复制代码
use std::iter::FromIterator;

pub struct SimpleLinkedList<T> {
    // Delete this field
    // dummy is needed to avoid unused parameter error during compilation
    dummy: ::std::marker::PhantomData<T>,
}

impl<T> SimpleLinkedList<T> {
    pub fn new() -> Self {
        unimplemented!()
    }

    // You may be wondering why it's necessary to have is_empty()
    // when it can easily be determined from len().
    // It's good custom to have both because len() can be expensive for some types,
    // whereas is_empty() is almost always cheap.
    // (Also ask yourself whether len() is expensive for SimpleLinkedList)
    pub fn is_empty(&self) -> bool {
        unimplemented!()
    }

    pub fn len(&self) -> usize {
        unimplemented!()
    }

    pub fn push(&mut self, _element: T) {
        unimplemented!()
    }

    pub fn pop(&mut self) -> Option<T> {
        unimplemented!()
    }

    pub fn peek(&self) -> Option<&T> {
        unimplemented!()
    }

    pub fn rev(self) -> SimpleLinkedList<T> {
        unimplemented!()
    }
}

impl<T> FromIterator<T> for SimpleLinkedList<T> {
    fn from_iter<I: IntoIterator<Item = T>>(_iter: I) -> Self {
        unimplemented!()
    }
}

// In general, it would be preferable to implement IntoIterator for SimpleLinkedList<T>
// instead of implementing an explicit conversion to a vector. This is because, together,
// FromIterator and IntoIterator enable conversion between arbitrary collections.
// Given that implementation, converting to a vector is trivial:
//
// let vec: Vec<_> = simple_linked_list.into_iter().collect();
//
// The reason this exercise's API includes an explicit conversion to Vec<T> instead
// of IntoIterator is that implementing that interface is fairly complicated, and
// demands more of the student than we expect at this point in the track.

impl<T> From<SimpleLinkedList<T>> for Vec<T> {
    fn from(mut _linked_list: SimpleLinkedList<T>) -> Vec<T> {
        unimplemented!()
    }
}

设计思路

在实现链表之前,我们需要定义节点结构:

rust 复制代码
struct Node<T> {
    data: T,
    next: Option<Box<Node<T>>>,
}

pub struct SimpleLinkedList<T> {
    head: Option<Box<Node<T>>>,
    len: usize,
}

这里我们使用Box<T>来在堆上分配节点,使用Option<T>来处理可能为空的指针。

完整实现

让我们来实现这个链表:

rust 复制代码
use std::iter::FromIterator;

struct Node<T> {
    data: T,
    next: Option<Box<Node<T>>>,
}

pub struct SimpleLinkedList<T> {
    head: Option<Box<Node<T>>>,
    len: usize,
}

impl<T> SimpleLinkedList<T> {
    pub fn new() -> Self {
        SimpleLinkedList { head: None, len: 0 }
    }

    pub fn is_empty(&self) -> bool {
        self.head.is_none()
    }

    pub fn len(&self) -> usize {
        self.len
    }

    pub fn push(&mut self, element: T) {
        let new_node = Box::new(Node {
            data: element,
            next: self.head.take(),
        });
        
        self.head = Some(new_node);
        self.len += 1;
    }

    pub fn pop(&mut self) -> Option<T> {
        self.head.take().map(|node| {
            self.head = node.next;
            self.len -= 1;
            node.data
        })
    }

    pub fn peek(&self) -> Option<&T> {
        self.head.as_ref().map(|node| {
            &node.data
        })
    }

    pub fn rev(self) -> SimpleLinkedList<T> {
        let mut reversed_list = SimpleLinkedList::new();
        let mut current_list = self;
        
        while let Some(element) = current_list.pop() {
            reversed_list.push(element);
        }
        
        reversed_list
    }
}

impl<T> FromIterator<T> for SimpleLinkedList<T> {
    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
        let mut list = SimpleLinkedList::new();
        
        for item in iter {
            list.push(item);
        }
        
        list
    }
}

impl<T> From<SimpleLinkedList<T>> for Vec<T> {
    fn from(mut linked_list: SimpleLinkedList<T>) -> Vec<T> {
        let mut vec = Vec::with_capacity(linked_list.len());
        
        while let Some(data) = linked_list.pop() {
            vec.push(data);
        }
        
        // 由于链表是后进先出的结构,我们需要反转向量
        vec.reverse();
        vec
    }
}

测试案例详解

通过查看测试案例,我们可以更好地理解链表的行为:

rust 复制代码
#[test]
fn test_new_list_is_empty() {
    let list: SimpleLinkedList<u32> = SimpleLinkedList::new();
    assert_eq!(list.len(), 0, "list's length must be 0");
}

新创建的链表应该是空的。

rust 复制代码
#[test]
fn test_push_increments_length() {
    let mut list: SimpleLinkedList<u32> = SimpleLinkedList::new();
    list.push(1);
    assert_eq!(list.len(), 1, "list's length must be 1");
    list.push(2);
    assert_eq!(list.len(), 2, "list's length must be 2");
}

每次push操作应该增加链表长度。

rust 复制代码
#[test]
fn test_pop_decrements_length() {
    let mut list: SimpleLinkedList<u32> = SimpleLinkedList::new();
    list.push(1);
    list.push(2);
    list.pop();
    assert_eq!(list.len(), 1, "list's length must be 1");
    list.pop();
    assert_eq!(list.len(), 0, "list's length must be 0");
}

每次pop操作应该减少链表长度。

rust 复制代码
#[test]
fn test_pop_returns_head_element_and_removes_it() {
    let mut list: SimpleLinkedList<u32> = SimpleLinkedList::new();
    list.push(1);
    list.push(2);
    assert_eq!(list.pop(), Some(2), "Element must be 2");
    assert_eq!(list.pop(), Some(1), "Element must be 1");
    assert_eq!(list.pop(), None, "No element should be contained in list");
}

pop操作应该返回最后插入的元素(后进先出),当链表为空时返回None。

rust 复制代码
#[test]
fn test_peek_returns_reference_to_head_element_but_does_not_remove_it() {
    let mut list: SimpleLinkedList<u32> = SimpleLinkedList::new();
    assert_eq!(list.peek(), None, "No element should be contained in list");
    list.push(2);
    assert_eq!(list.peek(), Some(&2), "Element must be 2");
    assert_eq!(list.peek(), Some(&2), "Element must be still 2");
    list.push(3);
    assert_eq!(list.peek(), Some(&3), "Head element is now 3");
    assert_eq!(list.pop(), Some(3), "Element must be 3");
    assert_eq!(list.peek(), Some(&2), "Head element is now 2");
    assert_eq!(list.pop(), Some(2), "Element must be 2");
    assert_eq!(list.peek(), None, "No element should be contained in list");
}

peek操作应该返回对头部元素的引用但不移除它。

rust 复制代码
#[test]
fn test_reverse() {
    let mut list: SimpleLinkedList<u32> = SimpleLinkedList::new();
    list.push(1);
    list.push(2);
    list.push(3);
    let mut rev_list = list.rev();
    assert_eq!(rev_list.pop(), Some(1));
    assert_eq!(rev_list.pop(), Some(2));
    assert_eq!(rev_list.pop(), Some(3));
    assert_eq!(rev_list.pop(), None);
}

rev操作应该返回一个反转的链表。

rust 复制代码
#[test]
fn test_into_vector() {
    let mut v = Vec::new();
    let mut s = SimpleLinkedList::new();
    for i in 1..4 {
        v.push(i);
        s.push(i);
    }
    let s_as_vec: Vec<i32> = s.into();
    assert_eq!(v, s_as_vec);
}

将链表转换为向量应该保持元素顺序。

Rust语言特性运用

在这个实现中,我们运用了多种Rust语言特性:

  1. Option类型: 用于安全地处理可能为空的值
  2. Box智能指针: 用于在堆上分配节点
  3. 所有权和借用: 正确管理数据的所有权,避免内存泄漏
  4. 模式匹配: 使用[match]和[map]等方法处理Option值
  5. 泛型: 使链表可以存储任何类型的数据
  6. 特质实现: 实现[FromIterator]和[From]特质以支持集合转换
  7. 生命周期: 隐式使用生命周期确保引用安全

内存安全保证

Rust的所有权系统确保了我们的链表实现是内存安全的:

  1. 无悬垂指针: 所有节点都通过[Box]进行管理
  2. 无内存泄漏: 当链表被销毁时,所有节点都会被自动释放
  3. 无数据竞争: 通过借用检查器确保线程安全

性能分析

我们的链表实现具有以下性能特征:

  1. push操作: O(1)时间复杂度
  2. pop操作: O(1)时间复杂度
  3. len操作: O(1)时间复杂度(因为我们维护了长度字段)
  4. peek操作: O(1)时间复杂度
  5. rev操作: O(n)时间复杂度
  6. 空间复杂度: O(n)

实际应用场景

链表在以下场景中非常有用:

  1. 实现其他数据结构: 栈、队列、图等
  2. 函数调用栈: 程序执行时的函数调用管理
  3. 浏览器历史记录: 前进后退功能
  4. 音乐播放列表: 上一首/下一首功能
  5. 撤销操作: 文本编辑器的撤销功能
  6. 内存管理: 操作系统的内存分配

优化版本

我们可以对实现进行一些优化:

rust 复制代码
impl<T> SimpleLinkedList<T> {
    // 使用迭代而不是递归来反转链表,避免栈溢出
    pub fn rev_iterative(self) -> SimpleLinkedList<T> {
        let mut prev = None;
        let mut current = self.head;
        let len = self.len;
        
        while let Some(mut node) = current {
            current = node.next.take();
            node.next = prev;
            prev = Some(node);
        }
        
        SimpleLinkedList { head: prev, len }
    }
    
    // 在常量时间内清空链表
    pub fn clear(&mut self) {
        self.head = None;
        self.len = 0;
    }
}

与标准库对比

Rust标准库提供了[LinkedList]类型,它是一个双向链表,功能更丰富:

rust 复制代码
use std::collections::LinkedList;

let mut list = LinkedList::new();
list.push_back(1);  // 在尾部插入
list.push_front(2); // 在头部插入
list.pop_back();    // 从尾部弹出
list.pop_front();   // 从头部弹出

我们的实现是一个简化的单向链表,专注于教学目的。

总结

通过这个练习,我们学习到了:

  1. 链表数据结构的基本原理和实现方法
  2. Rust中内存管理的概念和实践
  3. 所有权系统如何保证内存安全
  4. 泛型编程的基本用法
  5. 特质系统和集合转换
  6. 如何编写测试来验证数据结构的正确性

链表实现是理解Rust语言特性的绝佳练习,它涉及了所有权、借用、泛型、特质等多个核心概念。通过亲手实现链表,我们不仅加深了对数据结构的理解,也更好地掌握了Rust语言的精髓。

在实际项目中,除非有特殊需求,我们通常会使用标准库提供的集合类型,因为它们经过了充分的测试和优化。但对于学习目的,自己实现数据结构是无价的经验。

相关推荐
良木生香41 分钟前
【数据结构-初阶】详解算法复杂度:时间与空间复杂度
数据结构
会员果汁43 分钟前
优先级队列-C语言
c语言·数据结构·算法
却话巴山夜雨时i1 小时前
347. 前 K 个高频元素【中等】
数据结构·算法·leetcode
蘑菇小白1 小时前
数据结构--栈
数据结构·算法·
Bear on Toilet1 小时前
12 . 二叉树的直径
数据结构·算法·二叉树
惜.己1 小时前
数据结构与算法-数组异或操作
数据结构·算法
Dylan的码园1 小时前
ArrayList与顺序表
java·数据结构·链表
2301_807997381 小时前
代码随想录-day55
数据结构·c++·算法
别动哪条鱼3 小时前
AAC ADTS 帧结构信息
网络·数据结构·ffmpeg·音视频·aac