[Rust]Rust学习笔记-通过链表操作学习rust

链表一般是学习 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

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 对象组成元组,再解构出来,比如:

 match (l1,l2) {

    (Some(_l1),None) => { },

    (None,Some(_l2)) => { },

    (Some(_l1),Some(_l2)) => { },

    (None,None) => { },
}

模式解构中的 ref 和 mut

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


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);
}

你可能感兴趣的:(rust,rust,链表)