链表一般是学习 Rust 的第一关,完成一个链表对于 Rust 的所有权、借用、可变/不可变、Box、Option 等重要的基础概念都有涉及。
能够写好链表,说明对 Rust 的基础概念和思维方式都有了一定的了解。
本文所指的链表特指用 Box 实现的单向链表,仅用来熟悉 Rust 的基础知识。其基本实现为:
#[derive(Debug)]
pub struct ListNode {
pub val:i32,
pub next:Option>
}
leetCode 上的链表相关题目,如 21、206、876 均只提供这种链表定义。
实际使用中,注重性能会考虑使用裸指针写链表,比如标准库里的LinkedList。
对性能要求不高可以使用 Rc/RefCell,写代码会容易很多,而且支持双向链表。
Option 是 Rust 中最常用最重要的类型,它的语义是这个类型的变量可能为 None。None 不是空指针,是一个类型,语义为空。
绝大部分变量都有可能为空的需求,比如链表的 next,最后一个节点的 next 肯定是空的。基本类型比如 i32 这种,空值可以用 0 表示。
复杂类型可能为空时,就应该使用 Option,这是 Rust 的一种最基本的设计模式。
Box 相当于独占指针,只不过数据存在堆上,独占带来的后果就是,一个节点不可能被一个以上变量持有。在节点不能被克隆的情况下,就要考虑到底应该由谁来持有节点。
例如,翻转一个链表:1->2->3->4,就不能先把节点 4 的 next 指向节点 3,因为这样会导致节点 2 的 next 和节点 4 的 next 抢节点 3,这样的逻辑在 rust 中不可能通过编译,所以我们就不用浪费时间设计这种逻辑的代码了。
操作链表,我们不可避免的要转移节点,比如合并两个链表,我们自然要把每个节点从原来的链表中拆出来,装到新的链表中。
那么问题来了,我们怎么把链表上的节点拿走?或者更具体点,我们怎么把节点的 next 的对象的所有权移走?
Rust 不允许我们留一个悬垂指针,因此 next 上一定要有些东西,最简单的方式,自然是找个成本低的东西,把我们需要的对象换出来。
take/replace 就是这种思维方式的实际应用。
take 用于 Option,可以用 None 把 Option 里的内容转移出来,取得其所有权。take 改变了数据,因此被执行的 Option 对象必须可变,take 之后,此 Option 等于 None。
replace 使用的范围更广,很多数据结构都支持此方法,可以自己构造一个语义上的空对象来换取数据。
match/if let/while let 是 Rust 中常用的操作,可以从枚举中匹配出具体的类型,对 Option 类型用的尤其多。
但是如果要同时判断两个 Option 对象该怎么做?
可以把两个 Option 对象组成元组,再解构出来,比如:
match (l1,l2) {
(Some(_l1),None) => { },
(None,Some(_l2)) => { },
(Some(_l1),Some(_l2)) => { },
(None,None) => { },
}
如果模式解构出的数据需要修改或者借用,可以使用如下形式:
if let Some(ref mut _ret) = ret { }
if let Some(ref _ret) = ret { }
if let Some(mut _ret) = ret { }
现在我们来实现一些链表的操作。
使用 leetcode 提供的节点结构体定义,并实现 new 方法。
new 方法没什么好说的,next的初值设置为 None。
#[derive(Debug)]
pub struct ListNode {
pub val:i32,
pub next:Option>
}
impl ListNode {
pub fn new(val:i32) -> Self {
return ListNode{val:val,next:None}
}
}
链表肯定不能只有一个节点,所以先要实现追加节点的方法。
追加节点需要能够从头节点找到尾节点,然后修改尾节点的 next 属性。
impl ListNode {
// 想修改节点,必须返回可变借用
pub fn getLastMut(&mut self) -> &mut Self {
if let Some(ref mut boxNode) = self.next {
return boxNode.getLastMut();
}else{
return self;
}
}
// 追加节点
pub fn append(&mut self, val:i32){
let _node = ListNode::new(val);
self.getLastMut().next = Some(Box::new(_node));
}
}
接下来我们实现 leetcode 206. 反转链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
翻转链表的关键,是保证不会有两个变量抢一个节点,同时所有的节点都能被访问到。
从头部开始循环翻转,循环时需要能拿到之前的一个节点和之后的一个节点,这样把当前节点的 next 指向前一个节点,然后把当前节点指向下一个节点,继续下一轮循环即可。
下一个节点可通过当前节点的 next 用 take() 取出,因此建立一个临时变量 prev 存储上一个节点,初始值自然为 None。
impl ListNode {
pub fn reverse_list(head:Option>) -> Option> {
let mut prev = None; // 上一个节点
let mut cur = head; // 当前节点
while let Some(mut _node) = cur { // 用take置换next中的节点需要 mut
cur = _node.next.take(); // 换出 next 作为下一次的 cur
_node.next = prev; // 把next指向前一个节点
prev = Some(_node); // 更新 prev
}
return prev; // 跳出循环时,prev就是翻转后的头节点
}
}
接下来我们实现 leetcode 876. 链表的中间结点
示例 1:
输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
示例 2:
输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
一般来说,获取中间节点可以使用快慢指针,快指针每次走两步,慢指针每次走一步,快指针走到终点,慢指针即可获取中间节点。
但是在 Rust 中,使用快慢指针意味着链表要给两个指针共享,共享不可变,我们没法修改链表,就没办法把中间节点的所有权拿走作为函数的返回。
因此,只能记录中间位置,然后再进行一次遍历,在遍历到中间位置时把节点返回。
impl ListNode {
pub fn middle_node(head: Option>) -> Option> {
let mut head = head;
let mut fast = &head;
let mut total = 0; // 获取总长度
while let Some(_fast) = fast {
total += 1;
fast = &_fast.next;
}
if total%2 == 0 {
total = total/2
}else{
total = (total-1)/2
}
let mut step = 0; // 根据总长度算出中间位置
while step < total {
step += 1;
if let Some(_head) = head {
head = _head.next;
}
}
return head;
}
}
接下来我们实现 leetcode 21. 合并两个有序链表
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
两个链表都有序,合成一个链表,只要 take() 出来逐一比较,小的放入新链表,大的放回原链表,直到一个链表为空,把另外一个链表追加到尾部即可。
问题是最后要返回头节点,单向链表没法从尾部返回头节点,合并后也不可能还持有头结点。
因此,只能自己创建一个头结点,把合并的链表追加到此头结点的 next。
只要可变借用头结点即可修改 next,不需要持有头结点。
最后把头结点的 next 返回即可。
impl ListNode {
pub fn merge_two_lists(l1: Option>, l2: Option>) -> Option> {
let mut retHead: Option> = Some(Box::new(ListNode::new(-1))); // 创建一个头节点
let mut cur= &mut retHead;
let mut l1 = l1;
let mut l2 = l2;
let mut next = true;
while next == true {
match (l1.take(),l2.take()) { // 取出来比较
(Some(_l1),None) => {
// 只有l1了,后面不再需要遍历
if let Some(ref mut _cur) = cur { // ref 禁止移动,mut 确保可以修改
_cur.next = Some(_l1);
}
next = false;
},
(None,Some(_l2)) => {
// 只有l2了,后面不再需要遍历
if let Some(ref mut _cur) = cur { // ref 禁止移动,mut 确保可以修改
_cur.next = Some(_l2);
}
next = false;
},
(Some(mut _l1),Some(mut _l2)) => { // mut 确保可以修改
// 需要比大小了,先比个大小
if &_l1.val < &_l2.val {
let _next = _l1.next.take();
if let Some(ref mut _cur) = cur { // ref 禁止移动,mut 确保可以修改
_cur.next = Some(_l1);
cur = &mut _cur.next; // 游标移动
}
l1 = _next;
l2 = Some(_l2); // 大的放回
}else{
let _next = _l2.next.take();
if let Some(ref mut _cur) = cur {
_cur.next = Some(_l2);
cur = &mut _cur.next; // 游标移动
}
l2 = _next;
l1 = Some(_l1); // 大的放回
}
},
(None,None) => {
next = false;
},
}
}
return retHead.unwrap().next;
}
}
把上述代码保存成文件 xbox.rs,在 main.rs 写个测试调用,如下:
mod xbox;
fn main () {
let mut listNode = xbox::ListNode::new(1);
listNode.append(2);
listNode.append(3);
listNode.append(4);
listNode.append(5);
listNode.append(6);
println!("list=>{:?}",listNode);
let listNode2 = xbox::ListNode::reverse_list(Some(Box::new(listNode)));
println!("list2=>{:?}",listNode2);
let listNode3 = xbox::ListNode::middle_node(listNode2);
println!("list3=>{:?}",listNode3);
let mut listNode4 = xbox::ListNode::new(1);
listNode4.append(3);
listNode4.append(7);
let mut listNode5 = xbox::ListNode::new(3);
listNode5.append(4);
listNode5.append(5);
let listNode6 = xbox::ListNode::merge_two_lists(Some(Box::new(listNode4)), Some(Box::new(listNode5)));
println!("list6=>{:?}",listNode6);
}