前言
受到 你所不知道的 C 语言: linked list 和非连续记忆体 的启发,记录对于 Indirect Pointer 的学习历程,并尝试使用 Rust 实现此概念。
测试程式码: https://github.com/hank20010209/good-taste-linked-list
直观的 Linked List 实作
首先定义 Linked List 资料结构,定义节点本身以及 Linked List 本身
typedef struct Node { int value; struct Node *next;}Node;
typedef struct List { Node *head;}List;
init_list
To init the Linked List
List *init_list(){ List *l = (List *)malloc(sizeof(List)); l->head = NULL; return l;}
初始化 Linked List,并将初始化完成的 Linked List 回传
find_node
Find Node in Linked List
Return the pointer point to Node if in Linked List
Return NULL pointer if not in Linked List
Node *find_node(List *list, int target){ Node *temp = list->head; while(temp && temp->value != target) { temp = temp->next; } return temp;}
insert_tail
insert Node to the tail of Linked List
void insert_tail(List *list, int value){ Node *curr = list->head; Node *prev = NULL; Node* new = (Node *)malloc(sizeof(Node)); new->next = NULL; new->value = value; while(curr) { prev = curr; curr = curr->next; } if(!prev) list->head = new; else prev->next = new;}
在 Linked List 的尾端插入节点,使用 prev
和 curr
去走访节点,prev
为 NULL
的情况,表示 Linked List 没有任何节点或是为 NULL
,否则就向 prev->next
插入节点,图像化如下
在 prev
不为 NULL
,也就是 Linked List 不为空的情况下,存在节点的情况下
在 prev
为 NULL
,也就是 Linked List 为空,没有节点的情况下
remove_node
Remove the Node in Linked List
If target not in Linked List, return
void remove_node(List *list, Node *target){ if(!target) return; Node *prev = NULL; Node *curr = list->head; while(curr != target) { prev = curr; curr = curr->next; } if(!prev) list->head = target->next; else prev->next = target->next;}
使用 prev
以及 curr
走访 Linked List 找到目标节点,接下来分成两种情况进行讨论,分别为删除的为头节点或是其他节点
删除其他节点的情况
删除头节点的情况
观察 remove_node 以及 insert_tail
可以发现到在 remove_node
以及 insert_tail
都会出现特定情况需要进行处理,通过 if-else
判断是否为头节点,而以下将使用间接指标 (或称为指标的指标) 对程式码进行优化,除去对于特例的判断
Indirect Pointer 概念
前面 remove_node
的想法为将节点 Node
的记忆体地址储存到 curr
和 prev
中,通过 curr
和 prev
走访 Linked List,找到 target
时便停止。
而 Indirect Pointer 的想法为运用指向到节点记忆体地址的指标进行操作,原先的 curr
和 prev
中储存的是节点本身的记忆体地址,反参考 curr
和 prev
我们能够得到节点本身,而 Indirect Pointer 中储存的是指向到节点记忆体地址的指标,如果我们对其进行反参考,我们会得到的是该节点的记忆体地址,概念说明如下
#include <stdio.h>#include <stdlib.h>void foo(int *a_ptr){ a_ptr = NULL;}int main(void){ int a = 3; int *a_ptr = &a; printf("%d\n", *a_ptr); foo(a_ptr); printf("%d\n", *a_ptr);}
上面这一段程式码,我们有一个指向到整数的指标变数 a_ptr
,接着我们将指标变数传入 foo()
函式,让 a_ptr
由原先的指向变数 a
变为指向到 NULL
。
我们可以通过第 15 行查看对 a_ptr
的修改是否成功,如果修改成功,则第 15 行会出现对空指标进行反参考的错误,造成记忆体区段错误。
以下为执行结果
33
可以发现我们并没有成功修改 a_ptr
所指向的物件,原因为 C 语言的函式传递是以传值进行的,传入函式的参数会产生一份複本到函式中,而我们在函式中修改的参数,实际上修改的是刚刚产生的複本。举一个简单的例子
#include <stdio.h>#include <stdlib.h>void foo(int a){ a = 10;}int main(void){ int a = 3; printf("%d\n", a); foo(a); printf("%d\n", a);}
上面这个例子十分容易理解,我们在 foo()
中修改的 a
,实际上是在函式中产生的複本,不会影响到 main
中的 a
。
如果要针对 a
进行修改,我们需要传入指向到 a
的指标变数,修改 a
记忆体地址中储存的资料,範例如下
#include <stdio.h>#include <stdlib.h>void foo(int *a){ *a = 10;}int main(void){ int a = 3; printf("%d\n", a); foo(&a); printf("%d\n", a);}
图解如下
这里可以注意到,对于 call by value,如果我们修改传入函式中的值,实际上我们是修改到他的複本,但是我们可以修改他背后的资料,以 *a
而言,指标变数中储存的是记忆体地址,而我们对其反参考可以得到记忆体地址储存的资料,对于 call by value,我们无法修改变数中储存的值,也就是我们无法修改指标中储存的记忆体地址,但是我们可以通过反参考指标修改背后的资料。
对应到上面的範例,我们是传入指标变数到 foo()
中,在 foo()
中我们得到 *a
的複本,称为 copy *a
,之后我们反参考 copy *a
,修改记忆体地址指向的资料。
接着回到我们在一开始看到的例子
#include <stdio.h>#include <stdlib.h>void foo(int *a_ptr){ a_ptr = NULL;}int main(void){ int a = 3; int *a_ptr = &a; printf("%d\n", *a_ptr); foo(a_ptr); printf("%d\n", *a_ptr);}
这里我们尝试修改变数的值,也就是修改指标变数储存的记忆体地址,将其修改为 NULL
,我们修改到的是变数複製出来的複本,而不是在 main()
中 a_ptr
本身,因此在第 15 行不会发生反参考空指标所导致的记忆体区段错误。
这里整理一下,如果我们要对 a
进行修改,我们需要传入 *a
。而如果我们要修改 *a
,我们会想到我们可能要传入 **a
,*a
为指标,而 **a
称为指标的指标,又称为间接指标。我们可以通过间接指标去修改指标变数中储存的值,只需要反参考间接指标,修改其值即可,以下範例
#include <stdio.h>#include <stdlib.h>void foo(int **a_ptr){ *a_ptr = NULL;}int main(void){ int a = 3; int *a_ptr = &a; printf("%d\n", *a_ptr); foo(&a_ptr); printf("%d\n", *a_ptr);}
output:
3Segmentation fault (core dumped)
从以上範例可以看到,我们通过间接指标,去修改指标内储存的值。
而接下来我们将使用间接指标的手法,对 Linked List 的实作进行优化。
应用 Indirect Pointer 的 Linked List
remove_node
前面 remove_node
的想法为我们使用一个指标变数,储存该节点的记忆体地址,反参考后就可以得到节点本身。
而这边使用间接指标的想法,我们储存的指标,是指向到该节点的记忆体地址,如果我们反参考指标,得到的是节点的记忆体地址。
以下为实作部分
void remove_list_node(List *list, Node *target){ Node **indirect = &list->head; while (*indirect != target) indirect = &(*indirect)->next; *indirect = target->next;}
我们使用指向到 Node
指标的指标变数 indirect
,里面储存指向到 list->head
的指标,也就是说 indirect
是一个指标的指标,也称为间接指标,indirect is a pointer to pointer, which mean a pointer to your pointer value,your pointer value point to Node
, so the type of pointer value is Node *
, and the type of the pointer to your pointer value is Node **
.
之后我们开始通过 indirect
去走访 Linked List,*indirect
这边为对 indirect
进行反参考,得到的会是节点的记忆体地址,型别为 Node *
,通过节点的记忆体地址和 target
进行比较,target
同样也是记忆体地址,意义为节点的记忆体地址。
第 6 行,首先我们对 indirect
进行反参考,得到 Node *
,也就是 Node
的记忆体地址,通过箭头运算子取得成员 next
,成员 next
指向到下一个节点,型别为 Node *
,意义为指向到节点的记忆体地址,接着我们对他进行取址,得到 Node **
,意义为指向到指向到节点的记忆体地址,这边听起来有些绕口,概念上就是储存的是指向到节点的记忆体地址。
举一个简单的例子
#include <stdio.h>#include <stdlib.h>typedef struct Node { int a; int b;}Node;int main(void){ Node a = {.a = 1, .b = 2}; Node *a_ptr = &a; Node **a_ptr_ptr = &a_ptr; printf("%d", (*a_ptr_ptr)->a);}
图解为以下
指标变数本身也是一个变数,有自己的记忆体地址,只是该变数里面储存的值为记忆体地址
#include <stdio.h>#include <stdlib.h>typedef struct Node { int a; int b;}Node;int main(void){ Node a = {.a = 1, .b = 2}; Node *a_ptr = &a; Node **a_ptr_ptr = &a_ptr; printf("Address of Node a: %-10x\n\n", &(a)); printf("Context of a_ptr: %-10x\n", a_ptr); printf("Dereference of a_ptr: %-10x\n", *a_ptr); printf("Address of a_ptr: %-10x\n\n", &(a_ptr)); printf("Context of a_ptr_ptr: %-10x\n", a_ptr_ptr); printf("Dereference of a_ptr_ptr: %-10x\n", *a_ptr_ptr); printf("Address of a_ptr_ptr: %-10x\n", &(a_ptr_ptr));}
output
Address of Node a: 61ff18 Context of a_ptr: 61ff18Dereference of a_ptr: 1Address of a_ptr: 61ff14Context of a_ptr_ptr: 61ff14 Dereference of a_ptr_ptr: 61ff18 Address of a_ptr_ptr: 61ff10
Node a
的记忆体地址为 0x61ff18
。
Node *a_ptr
为指标变数,记忆体地址为 0x61ff14
,内容为 0x61ff18
,为一个指向到 Node
的指标,对 a_ptr
进行反参考,意味着存取 a_ptr
指标变数内容所指向的资料,也就是 0x61ff18
所指向的资料,存取到 Node a
的第一个成员,得到 1。
Node **a_ptr_ptr
为指标变数,记忆体地址为 0x61ff10
,内容为 0x61ff14
,为一个指向到 Node *
的指标,对 a_ptr_ptr
进行反参考,意味着存取 a_ptr_ptr
指标变数内容所指向的资料,也就是 0x61ff14
所指向的资料,0x61ff14
中的资料为 0x61ff18
。
将这个概念应用在 remove_list_node
中,图解如下
这是初始情况,indirect
为一个指标的指标,也就是 indirect
为一个指标变数,储存指向到节点的指标。(加上记忆体地址方便理解)
不同于原先的实作,指向到节点本身,这边是储存指向到节点的指标,概念上为储存要更新的位置,而不是要更新的节点本身。反参考 indirect
得到的是指向到节点的记忆体地址。
接着看到走访,假设目前情况如下
首先先对 indirect
进行反参考,会得到节点的下一个节点的指标,概念上为接下来我们要更新的地址
接着我们通过箭头运算子存取成员 next
,也就是 (*indirect)->next
接着我们取出指向到下一个节点的指标,也就是取出指向 Node *
的指标&(*indirect)->next
,型别为 Node **
:::info
由走访节点的过程中我们可以观察到一件事情,indirect
所维护的,是我们要更新的位置,我们接下来要更新的位置是储存在 next
中的,型别为 Node *
,而我们要存取要更新的位置,因此需要使用 Node **
进行操作。
在上面的实作中,我们可以通过 *indirect
来得到 Node *
,进而去存取下一个节点的资讯。
:::
如果 *indirect
就是我们要删除的节点,也就是 *indirect == target
,则图解为以下
我们可以通过 *indirect
得到要删除的节点本身,也就是 target
,型别为 Node *
,目前 *indirect
的内容为前一个节点指向的下一个节点,于是,我们可以直接通过修改 *indirect
去改变前一个节点所指向的下一个节点,概念上可以理解为修改前一个节点中,next
的内容。
*indirect = target->next
的意义为将前一个节点的下一个节点改变为 target
的下一个节点。
这时候的 *indirect
为以下
可以注意到,实际上 target
并没有被我们移除。
remove_node 头节点情况
在最一开始的 while
迴圈判断时,会先判断 *indirect != target
,而我们最一开始条件便不成立了,因此不会进入到 while
迴圈,目前图解如下
接着我们通过 *indirect
获得第一个节点本身,型别为 Node *
,第一个节点本身也就是 list_head
,接着我们将 *indirect
设为头节点的下一个节点,也就是将 *indirect
设为 target->next
,图解如下
indirect pointer vs direct pointer
我们在最一开始直觉上的实作,是使用 direct pointer 的方式,直接针对节点本身进行操作,但是会有一些例外状况需要处理,如头节点为特殊情况。
而 indirect pointer 的想法为我们针对接下来要更新的位置进行判断,好处是不会有例外情况进行处理,因为我们并没有直接针对头节点进行操作。
Leetcode 21. Merge Two Sorted Lists
给定两个已经完成排序的单向链结串列,请将这两个链结串列进行合併,并回传合併完成的链结串列头节点。
直觉上的做法,是建立一个头节点,接着我们开始走访 list1
和 list2
,将 val
较小的节点串到我们建立的节点上,完成后回传我们建立头节点的下一个节点 (我们建立的头节点概念上是一个 dummy node)
以下为该实作程式码
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){ struct ListNode *head = (struct ListNode *)malloc(sizeof(struct ListNode)); struct ListNode *ptr = head; while(list1 && list2) { if(list1->val < list2->val) { ptr->next = list1; list1 = list1->next; } else { ptr->next = list2; list2 = list2->next; } ptr = ptr->next; } ptr->next = list1 ? list1 : list2; return head->next;}
这里我们的 head
使用 malloc
配置了一块记忆体空间,而我们可以使用间接指标的方法优化这一段空间,让我们不用使用 malloc
去对 head
进行记忆体空间的分配
前面的想法为 head
为一个实际存在的节点,而我们通过 ptr
去维护 head
后面串接的节点,但实际上 head
为一个 dummy node,内容没有意义,只是为了让我们串接后面的节点。
这边使用间接指标的想法,概念上是改变 head
所指向的节点,直接修改 head
的内容,我们直接宣告一个 head
pointer,指向到节点,并初始化为 NULL,之后我们使用 **ptr = &head
,ptr
里面储存的,是 head
这个指标变数本身的指标,反参考 ptr
会得到 head
指标变数所在的记忆体地址。
我们可以直接通过 *ptr = some address
直接改变 head
的内容,也就是 head
所储存的指向到节点的记忆体地址。
接着我们可以通过 &(*ptr)->next
得到指向 head
下一个节点的指标的指标。
以下为初始情况
加入第一个节点 (直接修改 head
的内容,也就是让 head
直接指向某一个节点),(*ptr) = list?
通过 (*ptr)
存取到节点本身的指标,通过箭头运算子存取成员
这里的概念如同前面提及的 remove_node
一样,我们是维护记忆体地址本身,而非节点本身。
实作如下
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){ struct ListNode *head; struct ListNode **ptr = &head; while(list1 && list2) { if(list1->val < list2->val) { *ptr = list1; list1 = list1->next; } else { *ptr = list2; list2 = list2->next; } ptr = &(*ptr)->next; } (*ptr) = list1 ? list1 : list2; return head;}
接着观察到 if-else
的部分,发现要做的操作都是反参考 ptr
,指向到下一个要接上的节点,接着更新到下一个节点,我们可以使用一个指标变数去指向 L1
或是 L2
。
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){ struct ListNode *head; struct ListNode **ptr = &head; struct ListNode **node; while(list1 && list2) { node = (list1->val < list2->val) ? (&list1) : (&list2); *ptr = *node; ptr = &(*ptr)->next; *node = (*node)->next; } (*ptr) = list1 ? list1 : list2; return head;}
使用 for
迴圈可以减少一些行数
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){ struct ListNode *head = NULL, **ptr = &head, **node = NULL; for(; list1 && list2; *node = (*node)->next, ptr = &(*ptr)->next) { node = (list1->val < list2->val) ? (&list1) : (&list2); *ptr = *node; } (*ptr) = list1 ? list1 : list2; return head;}
虽然可以将操作写入 for 迴圈中,但程式码将不易阅读
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){ struct ListNode *head = NULL, **ptr = &head, **node = NULL; for(; list1 && list2; node = (list1->val < list2->val) ? (&list1) : (&list2), *ptr = *node, *node = (*node)->next, ptr = &(*ptr)->next); return (*ptr) = list1 ? list1 : list2, head;}
Leetcode 2095. Delete the Middle Node of a Linked List
给定一单向链结串列,删除中间的节点后回传
直觉做法
struct ListNode* deleteMiddle(struct ListNode* head){ struct ListNode* slow_prev; struct ListNode* slow = head; struct ListNode* fast = head; if(!head->next) return NULL; while(fast && fast->next){ slow_prev = slow; slow = slow->next; fast = fast->next->next; } slow_prev->next = slow->next; return head;}
我们可以尝试使用间接指标,上面的作法次我们使用一个指标去指向到节点,如果对该指标进行反参考,我们会得到节点本身,而使用间接指标,我们要操作的对象是针对节点中 next
这个成员,要修改 next
,我们需要使用 Node **
(指向到 next
) 来对 next
进行操作,也就是间接指标,以下範例
struct ListNode* deleteMiddle(struct ListNode* head){ struct ListNode **indirect = &head; for(struct ListNode *fast = head; fast && fast->next; fast = fast->next->next) indirect = &(*indirect)->next; *indirect = (*indirect)->next; return head;}
这边走访时概念和上方删除节点的概念相同,都是针对接下来要操作的位置进行操作。
这边同样可以发现,实际上我们没有将节点移除,这边我们尝试将节点占用的记忆进行释放。
我们需要维护一个指标,指向到 *indirect
的前一个节点
struct ListNode* deleteMiddle(struct ListNode* head){ struct ListNode **indirect = &head; struct ListNode *prev = NULL; for(struct ListNode *fast = head; fast && fast->next; fast = fast->next->next) { prev = *indirect; indirect = &(*indirect)->next; } prev->next = (*indirect)->next; free(*indirect); return head;}
反参考 indirect
得到的是下一个节点,而在 indirect
更新之前,我们先使用一个指标储存 indirect
,达到储存前一个节点的目标。
迴圈结束后,*indirect
为中间节点,因此只要我们将 prev->next = (*indirect)->next
便完成移除中间节点的操作了,之后我们再将 *indirect
使用 free()
进行释放便完成操作,以给定单向链结串列 [1,3,4,7,1,2,6]
为例,在迴圈走访结束后,prev
指向节点的值为 4,*indirect
指向节点的值为 7。
接着我们把 prev
的下一个节点设为 *indirect
的下一个节点,这边其实 prev->next = (*indirect)->next
的意义等同于 (*indirect) = (*indirect)->next
,相当于把指向到 7 的节点设为指向到 1 的节点,也就是,这边 *indirect
的内容为 1
接着我们会将 *indirect
进行释放,可以发现到我们将指向到 1 的节点进行释放了,这会导致我们在后续走访的时候存取到空指标。
我们试着执行后,会发现 leetcode 中的 AddressSanitizer 会发出 heap-use-after-free 的错误。
要解决这个问题,我们可以使用一个指标去存放 (*indirect)->next
,实作如下
struct ListNode* deleteMiddle(struct ListNode* head){ struct ListNode **indirect = &head; struct ListNode *prev = NULL; if(!head->next) return NULL; for(struct ListNode *fast = head; fast && fast->next; fast = fast->next->next) { prev = *indirect; indirect = &(*indirect)->next; } struct ListNode *next = (*indirect)->next; free(*indirect); prev->next = next; return head;}
图解如下,首先是 *next = (*indirect)->next
的概念如下
这边没有直接让 next
画在 *indirect
节点的 next
下方,是为了不和 Node **
混淆。
接着 prev->next = next
如此我们便完成了释放中间节点的操作。
也可以使用以下作法
struct ListNode* deleteMiddle(struct ListNode* head){ struct ListNode **indirect = &head; struct ListNode *prev = NULL; if(!head->next) return NULL; for(struct ListNode *fast = head; fast && fast->next; fast = fast->next->next) { prev = *indirect; indirect = &(*indirect)->next; } struct ListNode *del = (*indirect); prev->next = (*indirect)->next; free(del); return head;}
Rust 版本
在 Rust 版本中如果我们想要使用快慢指标,则会发现到一些问题,最一开始 slow
和 fast
都需要指向到 head
,接着 slow
和 fast
都需要走访 Linked List,因此 slow
和 fast
都需要是可变的。
impl Solution { pub fn delete_middle(mut head: Option<Box<ListNode>>) -> Option<Box<ListNode>> { }}
delete_middle
函式传入了一个 Option<Box<ListNode>>
,Option
如同其他的 enum
型别,我们可以使用 match
去匹配其所有的结果,在 Linked List 的实作中,会有空节点的情况发生,也就是 nullptr
,但是在 rust 中并没有 nullptr
存在,而是通过 Option
,在 Option
中有两个成员,一个为 None
,另外一个为 Some(val)
,我们可以使用 match
对 Option
进行解构,或是使用更加简单的方式,使用 ?
进行解构,以下举例
struct Person { job: Option<Job>,}#[derive(Clone, Copy)]struct Job { phone_number: Option<PhoneNumber>,}#[derive(Clone, Copy)]struct PhoneNumber { area_code: Option<u8>, number: u32,}impl Person { fn work_phone_area_code(&self) -> Option<u8> { match self.job { Some(job) => { match job.phonstruct Person { job: Option<Job>,}#[derive(Clone, Copy)]struct Job { phone_number: Option<PhoneNumber>,}#[derive(Clone, Copy)]struct PhoneNumber { area_code: Option<u8>, number: u32,}impl Person { fn work_phone_area_code(&self) -> Option<u8> { match self.job { Some(job) => { match job.phone_number { Some(phone_number) => match phone_number.area_code { Some(area_code) => Some(area_code), number => number, None => None, }, None => None, } } None => None, } }}fn main() { let p = Person { job: Some(Job { phone_number: Some(PhoneNumber { area_code: Some(61), number: 439222222, }), }), }; println!("{}", p.work_phone_area_code().unwrap());}
可以看到 work_phone_area_code
的部分,如果使用 match
进行解构,将会看起来十分的杂乱,这边可以使用 ?
对 Option
进行解构
impl Person { fn work_phone_area_code(&self) -> Option<u8> { self.job?.phone_number?.area_code }}
x?
为表达式,能够对他进行取值,self.job?phone_number
意义为对 Option<job>
取值,当 job
为 Some(value)
时,取出 phone_number
。
回到题目本身,我们需要 slow
和 fast
对 Linked List 进行走访,slow
需要为 head
的可变引用,因为我们需要修改他所指向的下一个节点,而 fast
只是用来走访 Linked List 的,因此只需要不可变引用即可,实作如下
impl Solution { pub fn delete_middle(mut head: Option<Box<ListNode>>) -> Option<Box<ListNode>> { let mut slow = &mut head; let mut fast = &head; }}
但上面这个写法有一个问题,我们无法同时持有对于某一个物件的可变引用以及不可变引用,为此,我们需要再创造一个新的物件 head
,他的内容需要和 head
相同,但是是两个独立的物件
impl Solution { pub fn delete_middle(mut head: Option<Box<ListNode>>) -> Option<Box<ListNode>> { let mut slow = &mut head; let mut fast = &(head.clone()); }}
概念如下
#[derive(Clone)]struct Obj { a:i32}fn main() { let mut a: Obj = Obj { a: 1, }; let mut b = &a; let mut c = &(a.clone()); println!("{:p}", &a); println!("{:p}", b); println!("{:p}", c);}
output
0x7ffe4eb9ed580x7ffe4eb9ed580x7ffe4eb9ed5c
可以看到 a
和 c
是不同的物件,如此我们便可以利用 fast
以及 slow
对 Linked List 完成走访了
impl Solution { pub fn delete_middle(mut head: Option<Box<ListNode>>) -> Option<Box<ListNode>> { let mut fast = &(head.clone()); let mut slow = &mut head; while fast.is_some() && fast.as_ref()?.next.is_some() { slow = &mut (slow.as_mut()?.next); fast = &(fast.as_ref()?.next.as_ref()?.next); } *slow = (*slow).as_mut()?.next.take(); head }}
source
同样的,我们也可以使用 indirect pointer 的概念进行操作
impl Solution { pub fn delete_middle(mut head: Option<Box<ListNode>>) -> Option<Box<ListNode>> { let mut fast = &(head.clone()); let mut indirect = &mut head; while fast.is_some() && fast.as_ref()?.next.is_some() { fast = &(fast.as_ref()?.next.as_ref()?.next); indirect = &mut indirect.as_mut().unwrap().next; } *indirect = indirect.as_mut().unwrap().next.take(); head }}
参考资料
你所不知道的 C 语言: linked list 和非连续记忆体
Rust-lang
Rust. Fast&Slow pointers Solution