对链表进行迭代的不同方式

Different ways of iterating over linked list

本文关键字:方式 迭代 链表      更新时间:2023-10-16

当迭代链表时,我想到的第一件事是这样做:

Node* node = head;
while (node)
{
// do something to node
node = node->next;
}

但有时我看到人们做这种复杂的事情:

Node** node = &head;
while (*node)
{
// do something to node
node = &(*node)->next;
}

区别是什么?第二个用于什么?

您显然理解第一种方法。

第一个和第二个之间的根本区别在于用于枚举列表的指针所在的位置。在第一个中,指针通过局部变量使用,每次都将其更新为当前节点的next指针的值。在第二种情况下,使用指向指针的指针来保存"当前"指针的地址。它寻址的指针是"列表中"的实际指针,而不仅仅是它的值。最初它寻址head指针。对于每个步骤,它都会寻址当前节点的next指针。当算法完成时,它将保存链表中最后一个next成员的地址,其值最好为NULL。

第二种方法有明显的优势,尽管对于简单的枚举来说没有优势。这种方法更常用于维护场景,例如位置列表插入和删除。

示例:只给定一个指向具有以下节点形式的链表的头指针,编写一个函数,在列表末尾添加一个新节点:

struct Node
{
int data;
struct Node *next;
};

使用第一种方法进行枚举需要维护以前的指针,并特别注意检测初始NULL头指针。下面的函数可以做到这一点,并且总是返回链表的头:

struct Node * ll_insertTail(struct Node *head, int data)
{
struct Node *prev = NULL, *cur = head;
while (cur)
{
prev = cur;
cur = cur->next;
}
cur = malloc(sizeof(*head));
cur->data = data;
cur->next = NULL;
if (prev == NULL)
return cur;
prev->next = cur;
return head;
}

同样的操作,但使用指针对指针的方法来遍历实际的指针成员(而不仅仅是它们的值)将是这样的:

struct Node* ll_insertTail(struct Node *head, Type data)
{
struct Node **pp = &head;
while (*pp)
pp = &(*pp)->next;
*pp = malloc(sizeof(**pp));
(*pp)->data = data;
(*pp)->next = NULL;
return head;
}

这可以通过要求调用者首先传递头指针的地址来进一步增强。这增加了一个优点,即允许您将函数的返回值用于列表头指针之外的其他内容。例如:

int ll_insertTail(struct Node **pp, int data)
{
while (*pp)
pp = &(*pp)->next;
if ((*pp = malloc(sizeof(**pp))) == NULL)
{
perror("Failed to allocate linked list node");
return EXIT_FAILURE;
}
(*pp)->data = data;
(*pp)->next = NULL;
return EXIT_SUCCESS;
}

调用为:

int res = ll_insertTail(&head, data);

后两种情况都是可能的,因为通过地址而不是简单地通过值来使用指针。对于简单的枚举,使用指针对指针的方法没有什么意义。但是,如果您需要在列表中搜索特定节点或节点的位置保留使用指针的机制(可以是head,也可以是某个next成员),则指向指针的指针可以提供优雅的解决方案。

祝你好运。

在第一个代码示例中,变量node是指向Node结构的指针。它包含存储Node结构的内存位置的地址。

在第二个代码示例中,变量node是指向Node结构的指针的指针。它包含一个内存位置的地址,该内存位置包含存储Node结构的内存位置地址。

这在很大程度上听起来令人困惑,因为两个代码样本中的变量名称相同,而且几乎与Node相同。让我们重写代码示例,以便指针的含义更加清晰。

第一种情况:

Node* node_pointer = head;
while (node_pointer != NULL) {
// node_pointer points to a Node
// do something to that Node, then advance to the next element in the list
// ... something ...
node_pointer = node_pointer->next;  // advance
}

第二种情况:

Node** node_pointer_pointer = &head;
while (*node_pointer_pointer != NULL) {
// node_pointer_pointer points to a pointer which points to a Node
// do something to that Node, then advance to the next element in the list
// ... something ...
node_pointer_pointer = &((*node_pointer_pointer)->next);  // advance
}

在这两种情况下,变量head都是指向Node结构的指针。这就是为什么它的值在第一种情况下直接分配给node_pointer

node_pointer = head;

在第二种情况下,使用&运算符来获得head:的存储位置

node_pointer_pointer = &head;

什么是Node?它是一个结构,包含(可能还有其他内容)字段next,该字段是指向Node的指针。这就是为什么next的值可以在第一个代码样本中直接分配给node_pointer,但必须在第二个代码样本中将其用&运算符引用。

为什么第二种方法有用?在这个例子中,它不是。如果只想迭代链表的元素,那么只需要一个指向Node结构的指针。

但是,当您想要操作从属指针时,有一个指向指针的指针是有用的。例如,假设您已经完成了对列表的遍历,现在您想向尾部添加一个新节点。

在上面的第一种情况中,node_pointer没有帮助,因为它的值是NULL。你不能用它做更多的事情。

在第二种情况下,虽然*node_pointer_pointer的值是NULL,但node_pointer_pointer的值不是。它是列表中最后一个节点的next字段的地址。因此,我们可以将新的Node结构体的地址分配给next:

*node_pointer_pointer = make_new_node();  // make_new_node() returns a pointer

注意*node_pointer_pointer中的星号或取消引用运算符。通过延迟node_pointer_pointer,我们得到了next指针,我们可以为它分配一个新的Node结构的地址。

还要注意,如果node_pointer_pointer指向一个空列表的头,则此赋值有效。取消引用它会得到head,我们可以为它分配一个新的Node结构的地址。