链表一般是学习 Rust 的第一关,完成一个链表对于 Rust 的所有权、借用、可变/不可变、Box、Option 等重要的基础概念都有涉及。
能够写好链表,说明对 Rust 的基础概念和思维方式都有了一定的了解。
本文所指的链表特指用 Box 实现的单向链表,仅用来熟悉 Rust 的基础知识。其基本实现为:

  1. #[derive(Debug)]
  2. pubstructListNode {
  3. pub val: i32,
  4. pub next: Option<Box<ListNode>>,
  5. }

leetCode 上的链表相关题目,如 21、206、876 均只提供这种链表定义。
实际使用中,注重性能会考虑使用裸指针写链表,比如标准库里的LinkedList。
对性能要求不高可以使用 Rc/RefCell,写代码会容易很多,而且支持双向链表。

预备知识

Option

Option 是 Rust 中最常用最重要的类型,它的语义是这个类型的变量可能为 None。None 不是空指针,是一个类型,语义为空。
绝大部分变量都有可能为空的需求,比如链表的 next,最后一个节点的 next 肯定是空的。基本类型比如 i32 这种,空值可以用 0 表示。
复杂类型可能为空时,就应该使用 Option,这是 Rust 的一种最基本的设计模式。

Box

Box 相当于独占指针,只不过数据存在堆上,独占带来的后果就是,一个节点不可能被一个以上变量持有。在节点不能被克隆的情况下,就要考虑到底应该由谁来持有节点。
例如,翻转一个链表:1->2->3->4,就不能先把节点 4 的 next 指向节点 3,因为这样会导致节点 2 的 next 和节点 4 的 next 抢节点 3,这样的逻辑在 rust 中不可能通过编译,所以我们就不用浪费时间设计这种逻辑的代码了。

take/replace

操作链表,我们不可避免的要转移节点,比如合并两个链表,我们自然要把每个节点从原来的链表中拆出来,装到新的链表中。
那么问题来了,我们怎么把链表上的节点拿走?或者更具体点,我们怎么把节点的 next 的对象的所有权移走?
Rust 不允许我们留一个悬垂指针,因此 next 上一定要有些东西,最简单的方式,自然是找个成本低的东西,把我们需要的对象换出来。
take/replace 就是这种思维方式的实际应用。
take 用于 Option,可以用 None 把 Option 里的内容转移出来,取得其所有权。
take 改变了数据,因此被执行的 Option 对象必须可变,take 之后,此 Option 等于 None。
replace 使用的范围更广,很多数据结构都支持此方法,可以自己构造一个语义上的空对象来换取数据。

多个判断条件的模式解构

match/if let/while let 是 Rust 中常用的操作,可以从枚举中匹配出具体的类型,对 Option 类型用的尤其多。
但是如果要同时判断两个 Option 对象该怎么做?
可以把两个 Option 对象组成元组,再解构出来,比如:

  1. match (l1,l2) {
  2. (Some(_l1),None) => { },
  3. (None,Some(_l2)) => { },
  4. (Some(_l1),Some(_l2)) => { },
  5. (None,None) => { },
  6. }

模式解构中的 ref 和 mut

如果模式解构出的数据需要修改或者借用,可以使用如下形式:

  1. if let Some(ref mut _ret) = ret { }
  2. if let Some(ref _ret) = ret { }
  3. if let Some(mut _ret) = ret { }

链表实战

现在我们来实现一些链表的操作。

创建节点

使用 leetcode 提供的节点结构体定义,并实现 new 方法。
new 方法没什么好说的,next的初值设置为 None。

  1. #[derive(Debug)]
  2. pub struct ListNode {
  3. pub val:i32,
  4. pub next:Option<Box<ListNode>>
  5. }
  6. impl ListNode {
  7. pub fn new(val:i32) -> Self {
  8. return ListNode{val:val,next:None}
  9. }
  10. }

追加节点

链表肯定不能只有一个节点,所以先要实现追加节点的方法。
追加节点需要能够从头节点找到尾节点,然后修改尾节点的 next 属性。

  1. impl ListNode {
  2. // 想修改节点,必须返回可变借用
  3. pub fn getLastMut(&mut self) -> &mut Self {
  4. if let Some(ref mut boxNode) = self.next {
  5. return boxNode.getLastMut();
  6. }else{
  7. return self;
  8. }
  9. }
  10. // 追加节点
  11. pub fn append(&mut self, val:i32){
  12. let _node = ListNode::new(val);
  13. self.getLastMut().next = Some(Box::new(_node));
  14. }
  15. }

翻转链表
接下来我们实现 leetcode 206. 反转链表
示例:

  1. 输入: 1->2->3->4->5->NULL
  2. 输出: 5->4->3->2->1->NULL

翻转链表的关键,是保证不会有两个变量抢一个节点,同时所有的节点都能被访问到。
从头部开始循环翻转,循环时需要能拿到之前的一个节点和之后的一个节点,这样把当前节点的 next 指向前一个节点,然后把当前节点指向下一个节点,继续下一轮循环即可。
下一个节点可通过当前节点的 next 用 take() 取出,因此建立一个临时变量 prev 存储上一个节点,初始值自然为 None。

  1. impl ListNode {
  2. pub fn reverse_list(head:Option<Box<ListNode>>) -> Option<Box<ListNode>> {
  3. let mut prev = None; // 上一个节点
  4. let mut cur = head; // 当前节点
  5. while let Some(mut _node) = cur { // 用take置换next中的节点需要 mut
  6. cur = _node.next.take(); // 换出 next 作为下一次的 cur
  7. _node.next = prev; // 把next指向前一个节点
  8. prev = Some(_node); // 更新 prev
  9. }
  10. return prev; // 跳出循环时,prev就是翻转后的头节点
  11. }
  12. }

链表的中间结点
接下来我们实现 leetcode 876. 链表的中间结点
示例 1:

  1. 输入:[1,2,3,4,5]
  2. 输出:此列表中的结点 3 (序列化形式:[3,4,5])
  3. 返回的结点值为 3 (测评系统对该结点序列化表述是 [3,4,5])。
  4. 注意,我们返回了一个 ListNode 类型的对象 ans,这样:
  5. ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.

示例 2:

  1. 输入:[1,2,3,4,5,6]
  2. 输出:此列表中的结点 4 (序列化形式:[4,5,6])
  3. 由于该列表有两个中间结点,值分别为 3 4,我们返回第二个结点。

一般来说,获取中间节点可以使用快慢指针,快指针每次走两步,慢指针每次走一步,快指针走到终点,慢指针即可获取中间节点。
但是在 Rust 中,使用快慢指针意味着链表要给两个指针共享,共享不可变,我们没法修改链表,就没办法把中间节点的所有权拿走作为函数的返回。
因此,只能记录中间位置,然后再进行一次遍历,在遍历到中间位置时把节点返回。

  1. impl ListNode {
  2. pub fn middle_node(head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
  3. let mut head = head;
  4. let mut fast = &head;
  5. let mut total = 0; // 获取总长度
  6. while let Some(_fast) = fast {
  7. total += 1;
  8. fast = &_fast.next;
  9. }
  10. if total%2 == 0 {
  11. total = total/2
  12. }else{
  13. total = (total-1)/2
  14. }
  15. let mut step = 0; // 根据总长度算出中间位置
  16. while step < total {
  17. step += 1;
  18. if let Some(_head) = head {
  19. head = _head.next;
  20. }
  21. }
  22. return head;
  23. }
  24. }

// 快慢指针法,快指针走两步,慢指针走一步,慢指针停下来的地方就是中间结点。

// 然而在 Rust 中,这题的语义有所不同。函数接收一个单链表,返回一个单链表,实际上是把原链表从中间截断,释放前一半,返回后一半。其他语言可以不管释放,随便泄露,或者交给 GC 处理。

// node.next.as_ref() 取后继结点的只读指针,这个指针可为空,它的类型是 Option<&Box>.

// 当快慢指针在链表上移动时,整个链表处于只读锁定状态。Rust 编译器通过生命周期追踪到:快慢指针的有效性来源于 head 不可变。如果修改 head,指针很可能失效,违反了内存安全。

// 所以我们只能先完成中间结点的定位,再释放前一半链表。在函数运行期间,中间结点的内存地址是不变的,可以用做标识。最后挨个检验结点地址,挨个释放,直到遇到中间结点,返回中间结点。

  1. pub fn middle_node(head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
  2. letmut head = head;
  3. letmut slow = head.as_ref();
  4. letmut fast = head.as_ref();
  5. loop {
  6. ifletSome(node) = fast {
  7. fast = node.next.as_ref();
  8. } else {
  9. break;
  10. }
  11. ifletSome(node) = fast {
  12. fast = node.next.as_ref();
  13. } else {
  14. break;
  15. }
  16. ifletSome(node) = slow {
  17. slow = node.next.as_ref();
  18. } else {
  19. break;
  20. }
  21. }
  22. let mid_addr = ifletSome(node) = slow {
  23. node.as_ref() as *const ListNode
  24. } else {
  25. returnNone;
  26. };
  27. whileletSome(node) = head.as_ref() {
  28. let addr = node.as_ref() as *const ListNode;
  29. if addr != mid_addr {
  30. head = head.unwrap().next;
  31. } else {
  32. break;
  33. }
  34. }
  35. head
  36. }

合并两个有序链表

接下来我们实现 leetcode 21. 合并两个有序链表
示例:

  1. 输入:1->2->4, 1->3->4
  2. 输出:1->1->2->3->4->4

两个链表都有序,合成一个链表,只要 take() 出来逐一比较,小的放入新链表,大的放回原链表,直到一个链表为空,把另外一个链表追加到尾部即可。
问题是最后要返回头节点,单向链表没法从尾部返回头节点,合并后也不可能还持有头结点。
因此,只能自己创建一个头结点,把合并的链表追加到此头结点的 next。
只要可变借用头结点即可修改 next,不需要持有头结点。
最后把头结点的 next 返回即可。

  1. impl ListNode {
  2. pub fn merge_two_lists(l1: Option<Box<ListNode>>, l2: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
  3. let mut retHead: Option<Box<ListNode>> = Some(Box::new(ListNode::new(-1))); // 创建一个头节点
  4. let mut cur= &mut retHead;
  5. let mut l1 = l1;
  6. let mut l2 = l2;
  7. let mut next = true;
  8. while next == true {
  9. match (l1.take(),l2.take()) { // 取出来比较
  10. (Some(_l1),None) => {
  11. // 只有l1了,后面不再需要遍历
  12. if let Some(ref mut _cur) = cur { // ref 禁止移动,mut 确保可以修改
  13. _cur.next = Some(_l1);
  14. }
  15. next = false;
  16. },
  17. (None,Some(_l2)) => {
  18. // 只有l2了,后面不再需要遍历
  19. if let Some(ref mut _cur) = cur { // ref 禁止移动,mut 确保可以修改
  20. _cur.next = Some(_l2);
  21. }
  22. next = false;
  23. },
  24. (Some(mut _l1),Some(mut _l2)) => { // mut 确保可以修改
  25. // 需要比大小了,先比个大小
  26. if &_l1.val < &_l2.val {
  27. let _next = _l1.next.take();
  28. if let Some(ref mut _cur) = cur { // ref 禁止移动,mut 确保可以修改
  29. _cur.next = Some(_l1);
  30. cur = &mut _cur.next; // 游标移动
  31. }
  32. l1 = _next;
  33. l2 = Some(_l2); // 大的放回
  34. }else{
  35. let _next = _l2.next.take();
  36. if let Some(ref mut _cur) = cur {
  37. _cur.next = Some(_l2);
  38. cur = &mut _cur.next; // 游标移动
  39. }
  40. l2 = _next;
  41. l1 = Some(_l1); // 大的放回
  42. }
  43. },
  44. (None,None) => {
  45. next = false;
  46. },
  47. }
  48. }
  49. return retHead.unwrap().next;
  50. }
  51. }

旋转链表

  1. 旋转链表: 给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置 ```bash 输入:head = [1,2,3,4,5], k = 2
    输出:[4,5,1,2,3]

输入:head = [0,1,2], k = 4
输出:[2,0,1]

  1. 链表只有一条,当将头节点指针&mut Box<ListNode>推入队列时,这根指针就已经有了修改整条链表的权利了。显然,第二个指针无法拥有mut权限了,因为不能同时存在同一内存的两个可变引用。
  2. 在上面的代码中,我第一遍老实地使用不可变引用遍历一遍链表统计链表长,什么也没有发生。
  3. 第二次遍历要用可变引用,但是并没有存储,所以是可以的。到达要切开链表的地方,我用了take()方法将这之后的结点(原先被head所拥有)转移到了由new_head所拥有。
  4. 最后将剩余的链表遍历完,将最后的None修改成head。现在head失去所有权,整条链表由new_head所拥有。
  5. ```rust
  6. pub fn rotate_right(mut head: Option<Box<ListNode>>, k: i32) -> Option<Box<ListNode>> {
  7. if head.is_none() || k <= 0 {
  8. return head;
  9. }
  10. // Step 1 - loop the linked-list and count total length(Don't need mut)
  11. let mut ptr: Option<&Box<ListNode>> = head.as_ref();
  12. let mut list_len = 0;
  13. while let Some(node) = ptr {
  14. ptr = node.next.as_ref();
  15. list_len += 1;
  16. }
  17. // Step 2 - calculate the curoff place and reach that node using &mut Box
  18. // because this time we want to mutate the list
  19. let cutoff_cnt = list_len - k % list_len;
  20. if cutoff_cnt == list_len {
  21. return head;
  22. }
  23. let mut ptr: &mut Box<ListNode> = head.as_mut().unwrap();
  24. let mut i = 1;
  25. while i < cutoff_cnt {
  26. ptr = ptr.next.as_mut().unwrap();
  27. i += 1;
  28. }
  29. // Step 3 - Split into two list and then concatenate
  30. // head owns one list and new_head owns the other
  31. let mut new_head: Option<Box<ListNode>> = ptr.next.take(); // split
  32. let mut ptr: Option<&mut Box<ListNode>> = new_head.as_mut();
  33. while let Some(node) = ptr {
  34. if node.next.is_none() {
  35. ptr = Some(node);
  36. break;
  37. }
  38. ptr = node.next.as_mut();
  39. }
  40. ptr.unwrap().next = head; // concatenate
  41. new_head
  42. }

总结:在Rust中定义链表,不要再认为节点是孤立的,一个结点代表的就是之后整条链。要对链进行操作,想清楚分裂后的两条链分别归谁拥有。Rust有效地防止了环链表的出现,比如说,一个链表只有一个next字段指向自己的节点。

调用

把上述代码保存成文件 xbox.rs,在 main.rs 写个测试调用,如下:

  1. fn main() {
  2. letmut list_node = ListNode::new(1);
  3. list_node.append(2);
  4. list_node.append(3);
  5. list_node.append(4);
  6. list_node.append(5);
  7. list_node.append(6);
  8. println!("list=>{:?}", list_node);
  9. let list_node2 = ListNode::reverse_list(Some(Box::new(list_node)));
  10. println!("list2=>{:?}", list_node2);
  11. let list_node3 = ListNode::middle_node(list_node2);
  12. println!("list3=>{:?}", list_node3);
  13. let list_node4 = ListNode::middle_node_fast_show(list_node3);
  14. println!("list4=>{:?}", list_node4);
  15. letmut list_node5 = ListNode::new(1);
  16. list_node5.append(2);
  17. list_node5.append(7);
  18. letmut list_node6 = ListNode::new(1);
  19. list_node6.append(3);
  20. list_node6.append(5);
  21. let list_node7 =
  22. ListNode::merge_two_lists(Some(Box::new(list_node5)), Some(Box::new(list_node6)));
  23. println!("list7=>{:?}", list_node7);
  24. let mut list_node8 = ListNode::new(1);
  25. list_node8.append(2);
  26. list_node8.append(3);
  27. list_node8.append(4);
  28. list_node8.append(5);
  29. let list_node9 = ListNode::rotate_right(Some(Box::new(list_node8)), 2);
  30. println!("list9=>{:?}", list_node9);
  31. }