迭代链表时我想到的第一件事是:
Node* node = head;
while (node)
{
// do something to node
node = node->next;
}
但有时候我看到人们做了这么复杂的事情:
Node** node = &head;
while (*node)
{
// do something to node
node = &(*node)->next;
}
有什么区别,第二个用于什么?
答案 0 :(得分:6)
你显然理解第一种方法。
第一个和第二个之间的根本区别在于用于枚举列表的指针所在的位置。在第一个中,指针值通过局部变量使用,每次将其更新为当前节点的next
指针的值。在第二个中,指向指针的指针用于保存"当前"的地址。指针。它所指向的指针是列表中的实际指针"而不仅仅是它的值。最初它解决了head
指针。每一步都会解决当前节点的next
指针。当算法结束时,它将保存链表中 last 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
,可能是某些{ {1}}成员),指针指向优雅的解决方案。
祝你好运。
答案 1 :(得分:1)
在第一个代码示例中,变量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
?它是struct包含(可能与其他内容一起)字段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
,我们可以为其分配新{{1}}结构的地址。