在具体代码实现之前,先将上一章节我们接触到的一些Rust语法做一些简答的说明。
Option
Option
是Rust标准库中定义的一种枚举,它有两个变体:Some和None.
Some包装了一个具体的值,而None表示失败或缺少值。
在很多编程语言中会有空值(Null)这一个概念,但在Rust中并没有,因为容易因空指针引起一些不安全的问题。
当一个值可能有也可能没有的时候,在Rust中就需要你明确的处理这两种情况。
Box
在Rust中,Box是一种智能指针类型,用于在堆上分配内存并存储数据。
- Box类型本身是一个智能指针,它存储在栈上。
- Box里面指向的数据是存储在堆上。
let a = Box::new(1);
根据上述代码,可以看出来a
只是一个存储在栈上的智能指针,如果我们想用里面的值,我们需要进行解引用Deref
操作。
链表操作
单链表的使用都是默认带头结点的,你可以自己在练习的时候思考一下如果是不带头结点的链表如何实现这些基本操作,在不同的使用场景各有优劣,但对你来说更能加深对数据结构和Rust语法的理解。
1. 插入
1.1 头插法建立单链表
编写测试
- 新建链表
// linked_list/src/lib.rs
// Unit tests
#[cfg(test)]
mod test {
use super::*;
// ......................................
#[test]
fn test_list_insert_in_head() {
let mut list: LinkedList<i32> = LinkedList::new(0);
assert_eq!(list, LinkedList { head: Some(Box::new(LinkedNode { data: 0, next: None })) });
}
}
运行测试
编译错误 ❌
分析
- 我们的LinkedList结构体没有实现
PartialEq
这个trait,从而两个实例不能够进行相等性的比较。
解决方法
- 给
LinkedList
和LinkedNode
添加上PartialEq
这个trait。
// linked_list/src/lib.rs
// ......................................
#[derive(Debug,PartialEq)]
pub struct LinkedNode<T> {
data: T,
next: Option<Box<LinkedNode<T>>>,
}
#[derive(Debug,PartialEq)]
pub struct LinkedList<T> {
head: Option<Box<LinkedNode<T>>>,
}
运行测试 ✅
编写测试
- 链表插入
// linked_list/src/lib.rs
// Unit tests
#[cfg(test)]
mod test {
use super::*;
// ......................................
#[test]
fn test_list_insert_in_head() {
let mut list: LinkedList<i32> = LinkedList::new(0);
list.insert_head(1);
assert_eq!(list, LinkedList { head: Some(Box::new(LinkedNode { data: 0, next: Some(Box::new(LinkedNode { data: 1, next: None })) })) });
}
}
运行测试
编译错误 ❌
分析
- 我们现在还没有定义这个插入方法
解决方法
-
先创建该插入方法,使其能够编译通过。
我们不要想着一步到位,想着让其一下子就跑通,我们需要重复测试开发这几个环节,将我们的代码一点点完善起来。
// linked_list/src/lib.rs
impl<T> LinkedList<T> {
// ......................................
// insert node in this linked list head
pub fn insert_head(&mut self, value: T) {
}
}
运行测试
测试失败 ❌
分析
目前我们已经修复了编译错误,代码可以进行测试了,但是测试失败,左右不相等,不符合我们的预期。
- 我们定义的头插法,里面还没有写具体的操作代码,将代码进行完善,使其测试通过。
解决方法
- 完善
insert_head
方法,注意链表是带头结点。 - 目前先不管头结点里面存放数据问题,先让插入能够在头结点之后正常插入。
// linked_list/src/lib.rs
impl<T> LinkedList<T> {
// ------------------------------------------
// insert node in this linked list head
pub fn insert_head(&mut self, value: T) {
let new_node = LinkedNode {
data: value,
next: self.head.as_mut().unwrap().next.take(),
};
self.head.as_mut().unwrap().next = Some(Box::new(new_node));
}
}
代码说明
- 创建了一个新的链表结点,将头结点所指的next赋给新结点的next。
- 此时头结点的next指向None。
- 将头结点的next重新指向新结点。
-
as_mut
在Rust中,可变引用允许对数据进行可变访问和修改。可变引用是一种指向数据的引用,允许在引用的生命周期内对数据进行更改。这种机制使得Rust能够在编译时确保内存安全,避免数据竞争和未定义行为。
使用可变引用的主要作用包括:
- 修改数据:通过可变引用,可以在不转移所有权的情况下修改数据。这使得可以在程序中安全地进行数据的更改和更新。
- 避免数据竞争:Rust的借用规则确保了在同一时间只能有一个可变引用,从而避免了数据竞争的发生。这种限制使得在编译时就能够避免多线程并发访问数据时可能出现的问题。
- 内存安全:可变引用的使用受到Rust的借用规则的限制,确保了在编译时就能够预防空指针、野指针等内存安全问题。
Option
类型的as_mut
方法允许您将Option
转换为包含可变引用的Option
。这意味着您可以在Option
包含的值上进行可变操作,而不需要将值从Option
中取出。 -
take
Option
类型的take
方法允许您从Option
中取出值,并将Option
设置为None
。
运行测试 ✅
// linked_list/src/lib.rs
#[cfg(test)]
mod test {
// ---------------------------------------------
#[test]
fn test_list_insert_in_head() {
let mut list: LinkedList<i32> = LinkedList::new(0);
list.insert_head(1);
assert_eq!(
list,
LinkedList {
head: Some(Box::new(LinkedNode {
data: 0,
next: Some(Box::new(LinkedNode {
data: 1,
next: None
}))
}))
}
);
list.insert_head(2);
assert_eq!(
list,
LinkedList {
head: Some(Box::new(LinkedNode {
data: 0,
next: Some(Box::new(LinkedNode {
data: 2,
next: Some(Box::new(LinkedNode {
data: 1,
next: None
}))
}))
}))
}
);
list.insert_head(3);
assert_eq!(
list,
LinkedList {
head: Some(Box::new(LinkedNode {
data: 0,
next: Some(Box::new(LinkedNode {
data: 3,
next: Some(Box::new(LinkedNode {
data: 2,
next: Some(Box::new(LinkedNode {
data: 1,
next: None
}))
}))
}))
}))
}
);
}
}
我们可以多添加几个测试案例,尤其一些特殊情况下的处理。
我在代码中还实现了Display
方法,可以使用打印,来查看目前我们链表的结构,关于该方法后续再介绍。
这是带头结点的头插法,你快动手试试如何使用不带头结点的头插法。
版本管理
当我们测试通过后,我们使用git将我们的代码添加到本地仓库中。
1.2 尾插法建立单链表
完成了头插法,我们接下来进行尾插法的代码编写,但是步骤有所区别,因为我们这里并不知道表尾在哪里。所以我们需要遍历一下链表,找到表尾,然后在其后面插入数据。
先不考虑这么多,我们还是采用TDD的开发方式,来进行一步步开发。
编写测试
// linked_list/src/lib.rs
#[cfg(test)]
mod test {
// --------------------
#[test]
fn test_list_insert_in_tail() {
let mut list = LinkedList::new(0);
list.insert_tail(1);
list.insert_tail(2);
list.insert_tail(3);
assert_eq!(
list,
LinkedList {
head: Some(Box::new(LinkedNode {
data: 0,
next: Some(Box::new(LinkedNode {
data: 1,
next: Some(Box::new(LinkedNode {
data: 2,
next: Some(Box::new(LinkedNode {
data: 3,
next: None
}))
}))
}))
}))
}
);
}
}
运行测试
编译错误 ❌
分析
- 我们只编写了测试还没有编写该方法。
解决方法
- 编写
insert_tail
方法
这次我们将方法写完后再运行测试,你也可以根据头插法,先修复代码使其能够编译运行,测试不通过再通过修改添加代码功能来解决。
代码说明
- 从链表的第一个结点开始,遍历,直到找到最后一个结点**(指向None)**。
- 找到最后一个结点后,创建一个新的结点,并插入在已经找到的最后一个结点的后面。
// linked_list/src/lib.rs
impl<T> LinkedList<T> {
// ------------------------------------------
// insert node in this linked list tail
pub fn insert_tail(&mut self, value: T) {
let mut current_node = &mut self.head;
while let Some(node) = current_node {
if node.next.is_none() {
let new_node = LinkedNode {
data: value,
next: None,
};
node.next = Some(Box::new(new_node));
return;
}
current_node = &mut node.next;
}
}
}
运行测试 ✅
同时我们也可以看到,Cargo 默认会以多线程的方式运行多个测试。这意味着多个测试将会并行执行,以加快测试的运行速度并更快地提供反馈。
可以使用 --test-threads
标志并将线程数设置为 1。这样可以确保测试按顺序执行,避免测试之间相互干扰。
版本管理
当我们测试通过后,我们使用git将我们的代码更新提交到到本地仓库中。
下一章讲介绍链表的查找和在某个位置插入操作。