在链表中添加节点时使用双指针的原因是啥?
Posted
技术标签:
【中文标题】在链表中添加节点时使用双指针的原因是啥?【英文标题】:What is the reason for using a double pointer when adding a node in a linked list?在链表中添加节点时使用双指针的原因是什么? 【发布时间】:2011-11-08 10:56:48 【问题描述】:下面的两个代码示例都在链表的顶部添加了一个节点。 但是第一个代码示例使用双指针,而第二个代码示例使用单指针
代码示例 1:
struct node* push(struct node **head, int data)
struct node* newnode = malloc(sizeof(struct node));
newnode->data = data;
newnode->next = *head;
return newnode;
push(&head,1);
代码示例 2:
struct node* push(struct node *head, int data)
struct node* newnode = malloc(sizeof(struct node));
newnode->data = data;
newnode->next = head;
return newnode;
push(head,1)
这两种策略都有效。但是,许多使用链表的程序使用双指针来添加新节点。我知道双指针是什么。但是,如果单个指针就足以添加一个新节点,为什么很多实现都依赖于双指针呢?
是否存在单指针不工作需要双指针的情况?
【问题讨论】:
删除了 C++ 标签。这绝对是C 在 C 中你不会转换malloc()
的结果。去掉演员表,它会更容易阅读和更惯用。
@EAGER_STUDENT - Explaination。基本上在c 中,除了意外隐藏错误之外,它永远不会做任何事情。在c++ 中是必需的。
嗯...如果我编写一个双向链表,我喜欢让它循环并且总是有一个初始的、空的哨兵节点,头指向它。这使得许多例程变得更加简单。例如。根本不需要传递或修改头部。它永远不会改变。
@EAGER_STUDENT:没有称为“C/C++”的语言。转换malloc()
的结果是C和C++的区别之一。
【参考方案1】:
一些实现传递一个指向指针参数的指针,以允许直接更改头指针而不是返回新指针。因此你可以写:
// note that there's no return value: it's not needed
void push(struct node** head, int data)
struct node* newnode = malloc(sizeof(struct node));
newnode->data=data;
newnode->next=*head;
*head = newnode; // *head stores the newnode in the head
// and call like this:
push(&head,1);
不带头指针的实现必须返回新头,调用者自己负责更新:
struct node* push(struct node* head, int data)
struct node* newnode = malloc(sizeof(struct node));
newnode->data=data;
newnode->next=head;
return newnode;
// note the assignment of the result to the head pointer
head = push(head,1);
如果你在调用这个函数的时候不做这个赋值,你会泄露你用malloc分配的节点,头指针总是指向同一个节点。
现在好处应该很明显了:用第二种,如果调用者忘记将返回的节点分配给头指针,就会发生不好的事情。
【讨论】:
谢谢@Yogi。我手动应用了您的修改,即使它被拒绝了。struct node* push(struct node* head, int data) struct node* newnode = malloc(sizeof(struct node)); newnode->data=data; newnode->next=head; head = newnode;
为什么不是这个?
@Amit 因为这不会改变任何事情。此答案中的解释可能会有所帮助:***.com/questions/8403447/…【参考方案2】:
虽然前面的答案已经足够好了,但我认为从“按值复制”的角度来思考要容易得多。
当您传入指向函数的指针时,地址值将被复制到函数参数中。由于函数的作用域,该副本一旦返回就会消失。
通过使用双指针,您将能够更新原始指针的值。双指针仍将按值复制,但这没关系。您真正关心的是修改原始指针,从而绕过函数的作用域或堆栈。
希望这不仅能回答您的问题,还能回答其他与指针相关的问题。
【讨论】:
【参考方案3】:正如@R. Martinho Fernandes 在his answer 中指出的那样,使用pointer to pointer 作为void push(struct node** head, int data)
中的参数允许您直接从push
函数中更改head
指针,而不是返回新指针。
还有另一个很好的例子说明了为什么使用pointer to pointer 代替单个指针可以缩短、简化和加速您的代码。您询问了 添加 一个新节点到列表中,与从单链表中 删除 节点相比,该节点可能通常不需要指针到指针。您可以在没有指针的情况下实现从列表中删除节点,但它不是最理想的。我描述了细节here。我建议您也观看解决问题的this YouTube video。
顺便说一句:如果你算上Linus Torvaldsopinion,你最好学习如何使用指针到指针。 ;-)
Linus Torvalds: (...) 在光谱的另一端,我实际上希望更多的人了解真正核心的低级编码。不是大而复杂的东西,比如无锁名称查找,但只是很好地使用了指针到指针等。例如,我见过太多人通过跟踪“上一个”条目来删除单链表条目,然后删除条目,执行类似的操作
if (prev) prev->next = entry->next; else list_head = entry->next;
每当我看到这样的代码时,我都会说“这个人不懂指针”。遗憾的是,这很常见。
了解指针的人只是使用“指向入口指针的指针”,并使用 list_head 的地址对其进行初始化。然后当他们遍历列表时,他们可以在不使用任何条件的情况下删除条目,只需执行“*pp = entry->next”。 (...)
其他可能有用的资源:
C double pointers Pointers to Pointers Why use double pointer? or Why use pointers to pointers?【讨论】:
【参考方案4】:在您的特定示例中,不需要双指针。但是,如果您要执行以下操作,则可能需要它:
struct node* push(struct node** head, int data)
struct node* newnode = malloc(sizeof(struct node));
newnode->data=data;
newnode->next=*head;
//vvvvvvvvvvvvvvvv
*head = newnode; //you say that now the new node is the head.
//^^^^^^^^^^^^^^^^
return newnode;
【讨论】:
@a6h:不客气...................................... ..................................................... ..................................................... .....................................【参考方案5】:观察和发现,为什么...
我决定做一些实验并得出一些结论,
观察 1- 如果链表不为空,那么我们可以仅使用单个指针将节点添加到其中(显然是在末尾)。
int insert(struct LinkedList *root, int item)
struct LinkedList *temp = (struct LinkedList*)malloc(sizeof(struct LinkedList));
temp->data=item;
temp->next=NULL;
struct LinkedList *p = root;
while(p->next!=NULL)
p=p->next;
p->next=temp;
return 0;
int main()
int m;
struct LinkedList *A=(struct LinkedList*)malloc(sizeof(struct LinkedList));
//now we want to add one element to the list so that the list becomes non-empty
A->data=5;
A->next=NULL;
cout<<"enter the element to be inserted\n"; cin>>m;
insert(A,m);
return 0;
解释起来很简单(基本)。我们的 main 函数中有一个指针,它指向列表的第一个节点(根)。在insert()
函数中,我们传递根节点的地址,并使用该地址到达列表的末尾并向其添加一个节点。因此我们可以得出结论,如果我们在函数(不是主函数)中有变量的地址,我们可以从该函数中永久更改该变量的值,这将反映在主函数中。
OBSERVATION 2-上述添加节点的方法在列表为空时失败。
int insert(struct LinkedList *root, int item)
struct LinkedList *temp = (struct LinkedList*)malloc(sizeof(struct LinkedList));
temp->data=item;
temp->next=NULL;
struct LinkedList *p=root;
if(p==NULL)
p=temp;
else
while(p->next!=NULL)
p=p->next;
p->next=temp;
return 0;
int main()
int m;
struct LinkedList *A=NULL; //initialise the list to be empty
cout<<"enter the element to be inserted\n";
cin>>m;
insert(A,m);
return 0;
如果你继续添加元素并最终显示列表,那么你会发现列表没有发生任何变化,仍然是空的。 我想到的问题是,在这种情况下,我们也传递了根节点的地址,那么为什么修改没有发生,因为永久修改和主函数中的列表没有变化。为什么?为什么?为什么?
然后我观察到一件事,当我写A=NULL
时,A
的地址变为0。这意味着现在A
没有指向内存中的任何位置。所以我删除了A=NULL;
这一行,并在插入函数中做了一些修改。
一些修改,(下面insert()
函数只能将一个元素添加到一个空列表中,只是为了测试目的而写了这个函数)
int insert(struct LinkedList *root, int item)
root= (struct LinkedList *)malloc(sizeof(struct LinkedList));
root->data=item;
root->next=NULL;
return 0;
int main()
int m;
struct LinkedList *A;
cout<<"enter the element to be inserted\n";
cin>>m;
insert(A,m);
return 0;
上述方法也失败了,因为在insert()
函数中,根存储与main()
函数中的A
相同的地址,但在root= (struct LinkedList *)malloc(sizeof(struct LinkedList));
行之后,存储在root
中的地址发生了变化。因此,现在root
(在insert()
函数中)和A
(在main()
函数中)存储不同的地址。
所以正确的最终程序应该是,
int insert(struct LinkedList *root, int item)
root->data=item;
root->next=NULL;
return 0;
int main()
int m;
struct LinkedList *A = (struct LinkedList *)malloc(sizeof(struct LinkedList));
cout<<"enter the element to be inserted\n";
cin>>m;
insert(A,m);
return 0;
但是我们不想要两种不同的插入函数,一种是当列表为空时,另一种是当列表不为空时。现在来了双指针,这让事情变得简单。
我注意到重要的一件事是指针存储地址 当与 '*' 一起使用时,它们在该地址给出值,但指针 他们有自己的地址。
现在是完整的程序,稍后解释概念。
int insert(struct LinkedList **root,int item)
if(*root==NULL)
(*root)=(struct LinkedList *)malloc(sizeof(struct LinkedList));
(*root)->data=item;
(*root)->next=NULL;
else
struct LinkedList *temp=(struct LinkedList *)malloc(sizeof(struct LinkedList));
temp->data=item;
temp->next=NULL;
struct LinkedList *p;
p=*root;
while(p->next!=NULL)
p=p->next;
p->next=temp;
return 0;
int main()
int n,m;
struct LinkedList *A=NULL;
cout<<"enter the no of elements to be inserted\n";
cin>>n;
while(n--)
cin>>m;
insert(&A,m);
display(A);
return 0;
以下是观察结果,
1. root存储指针A的地址(&A)
,*root
存储指针A
存储的地址,**root
存储A
存储地址的值。用简单的语言root=&A
、*root= A
和**root= *A
。
2.如果我们写*root= 1528
,那么这意味着存储在root
中的地址的值变为1528,因为存储在root
中的地址是指针A的地址(&A)
因此现在A=1528
(即存储在A
中的地址为1528)并且此更改是永久性的。
每当我们更改*root
的值时,我们确实在更改存储在root
中的地址的值,并且由于root=&A
(指针A
的地址)我们间接更改A
的值或存储在A
.
所以现在如果A=NULL
(列表为空)*root=NULL
,那么我们创建第一个节点并将其地址存储在*root
,即间接我们将第一个节点的地址存储在A
。如果 list 不为空,则一切都与之前使用单指针的函数相同,只是我们将 root 更改为 *root
,因为存储在 root 中的内容现在存储在 *root
中。
【讨论】:
【参考方案6】:让我们用这个简单的例子:
void my_func(int *p)
// allocate space for an int
int *z = (int *) malloc(sizeof(int));
// assign a value
*z = 99;
printf("my_func - value of z: %d\n", *z);
printf("my_func - value of p: %p\n", p);
// change the value of the pointer p. Now it is not pointing to h anymore
p = z;
printf("my_func - make p point to z\n");
printf("my_func - addr of z %p\n", &*z);
printf("my_func - value of p %p\n", p);
printf("my_func - value of what p points to: %d\n", *p);
free(z);
int main(int argc, char *argv[])
// our var
int z = 10;
int *h = &z;
// print value of z
printf("main - value of z: %d\n", z);
// print address of val
printf("main - addr of z: %p\n", &z);
// print value of h.
printf("main - value of h: %p\n", h);
// print value of what h points to
printf("main - value of what h points to: %d\n", *h);
// change the value of var z by dereferencing h
*h = 22;
// print value of val
printf("main - value of z: %d\n", z);
// print value of what h points to
printf("main - value of what h points to: %d\n", *h);
my_func(h);
// print value of what h points to
printf("main - value of what h points to: %d\n", *h);
// print value of h
printf("main - value of h: %p\n", h);
return 0;
输出:
main - value of z: 10
main - addr of z: 0x7ffccf75ca64
main - value of h: 0x7ffccf75ca64
main - value of what h points to: 10
main - value of z: 22
main - value of what h points to: 22
my_func - value of z: 99
my_func - value of p: 0x7ffccf75ca64
my_func - make p point to z
my_func - addr of z 0x1906420
my_func - value of p 0x1906420
my_func - value of what p points to: 99
main - value of what h points to: 22
main - value of h: 0x7ffccf75ca64
我们有这个 my_func 的签名:
void my_func(int *p);
如果你看一下输出,最后,h 指向的值仍然是 22,h 的值是一样的,尽管在 my_func 中它被改变了。怎么会?
好吧,在 my_func 中,我们正在操作 p 的值,它只是一个本地指针。 调用后:
my_func(ht);
在main()中,p会持有h持有的值,代表z变量的地址,在main函数中声明。
在 my_func() 中,当我们改变 p 的值以保存 z 的值时,z 是指向内存中某个位置的指针,我们已经为其分配了空间,我们并没有改变 h 的值,即我们已经传入,但只是本地指针 p 的值。基本上,p 不再保存 h 的值,它将保存 z 指向的内存位置的地址。
现在,如果我们稍微改变一下我们的例子:
#include <stdio.h>
#include <stdlib.h>
void my_func(int **p)
// allocate space for an int
int *z = (int *) malloc(sizeof(int));
// assign a value
*z = 99;
printf("my_func - value of z: %d\n", *z);
printf("my_func - value of p: %p\n", p);
printf("my_func - value of h: %p\n", *p);
// change the value of the pointer p. Now it is not pointing to h anymore
*p = z;
printf("my_func - make p point to z\n");
printf("my_func - addr of z %p\n", &*z);
printf("my_func - value of p %p\n", p);
printf("my_func - value of h %p\n", *p);
printf("my_func - value of what p points to: %d\n", **p);
// we are not deallocating, because we want to keep the value in that
// memory location, in order for h to access it.
/* free(z); */
int main(int argc, char *argv[])
// our var
int z = 10;
int *h = &z;
// print value of z
printf("main - value of z: %d\n", z);
// print address of val
printf("main - addr of z: %p\n", &z);
// print value of h.
printf("main - value of h: %p\n", h);
// print value of what h points to
printf("main - value of what h points to: %d\n", *h);
// change the value of var z by dereferencing h
*h = 22;
// print value of val
printf("main - value of z: %d\n", z);
// print value of what h points to
printf("main - value of what h points to: %d\n", *h);
my_func(&h);
// print value of what h points to
printf("main - value of what h points to: %d\n", *h);
// print value of h
printf("main - value of h: %p\n", h);
free(h);
return 0;
我们有以下输出:
main - value of z: 10
main - addr of z: 0x7ffcb94fb1cc
main - value of h: 0x7ffcb94fb1cc
main - value of what h points to: 10
main - value of z: 22
main - value of what h points to: 22
my_func - value of z: 99
my_func - value of p: 0x7ffcb94fb1c0
my_func - value of h: 0x7ffcb94fb1cc
my_func - make p point to z
my_func - addr of z 0xc3b420
my_func - value of p 0x7ffcb94fb1c0
my_func - value of h 0xc3b420
my_func - value of what p points to: 99
main - value of what h points to: 99
main - value of h: 0xc3b420
现在,我们实际上已经从 my_func 更改了 h 所持有的值,方法是这样做:
-
更改了函数签名
从 main() 调用:my_func(&h);基本上,我们将 h 指针的地址传递给双指针 p,在函数签名中声明为参数。
在 my_func() 中我们正在执行:*p = z;我们正在取消引用双指针 p,一级。基本上,这就像你会做的那样翻译:h = z;
p 的值,现在保存着 h 指针的地址。 h指针保存z的地址。
您可以同时举两个例子并加以区分。 因此,回到您的问题,您需要双指针才能修改您直接从该函数传入的指针。
【讨论】:
【参考方案7】:想想像 [HEAD_DATA] 这样的 head 的内存位置。
现在在第二种情况下,调用函数的 main_head 是指向该位置的指针。
main_head--->[HEAD_DATA]
在您的代码中,它将指针 main_head 的值发送给函数(即 head_data 的内存位置的地址) 您将其复制到函数中的 local_head 。 所以现在
local_head---> [HEAD_DATA]
和
main_head---> [HEAD_DATA]
两者都指向同一个位置,但本质上是相互独立的。 所以当你写 local_head = newnode; 你所做的是
local_head--/-->[HEAD_DATA]
local_head-----> [NEWNODE_DATA]
您只是用本地指针中的新内存地址替换了先前内存的内存地址。 main_head(指针)仍然指向旧的 [HEAD_DATA]
【讨论】:
【参考方案8】:在 C 中处理链表的标准方法是让 push 和 pop 函数自动更新头指针。
C 是“按值调用”,意味着参数的副本被传递到函数中。如果您只传递头指针,则调用者不会看到您对该指针所做的任何本地更新。两种解决方法是
1) 传递头指针的地址。 (指向头指针的指针)
2) 返回一个新的头指针,并依赖调用者更新头指针。
选项 1) 是最简单的,尽管一开始有点混乱。
【讨论】:
【参考方案9】:如果你花时间编写一个工作节点插入函数,答案会更明显;你的不是。
您需要能够写入在头部上以将其向前移动,因此您需要一个指向头部的指针的指针,以便您可以取消引用它以获取指向头部的指针并更改它。
【讨论】:
【参考方案10】:假设您必须进行某些更改并且这些更改应该反映在调用函数中。
例子:
void swap(int* a,int* b)
int tmp=*a;
*a=*b;
*b=tmp;
int main(void)
int a=10,b=20;
// To ascertain that changes made in swap reflect back here we pass the memory address
// instead of the copy of the values
swap(&a,&b);
同样我们传递链表头的内存地址。
这样,如果添加了任何节点并且 Head 的值发生了更改,那么该更改会反射回来,我们不必在调用函数内部手动重置 Head。
因此,这种方法减少了内存泄漏的机会,因为如果我们忘记在调用函数中更新 Head,我们会丢失指向新分配节点的指针。
除此之外,第二个代码将更快地工作,因为我们直接使用内存,因此不会浪费时间进行复制和返回。
【讨论】:
【参考方案11】:当我们在函数中将指针作为参数传递并希望在同一个指针中更新时,我们使用双指针。
另一方面,如果我们将指针作为参数传递给函数并在单个指针中捕获它,则必须将结果返回给调用函数才能使用结果。
【讨论】:
【参考方案12】:我认为关键在于它可以更轻松地更新链表中的节点。您通常必须跟踪以前和当前的指针,您可以使用双指针来处理这一切。
#include <iostream>
#include <math.h>
using namespace std;
class LL
private:
struct node
int value;
node* next;
node(int v_) :value(v_), next(nullptr) ;
;
node* head;
public:
LL()
head = nullptr;
void print()
node* temp = head;
while (temp)
cout << temp->value << " ";
temp = temp->next;
void insert_sorted_order(int v_)
if (!head)
head = new node(v_);
else
node* insert = new node(v_);
node** temp = &head;
while ((*temp) && insert->value > (*temp)->value)
temp = &(*temp)->next;
insert->next = (*temp);
(*temp) = insert;
void remove(int v_)
node** temp = &head;
while ((*temp)->value != v_)
temp = &(*temp)->next;
node* d = (*temp);
(*temp) = (*temp)->next;
delete d;
void insertRear(int v_)//single pointer
if (!head)
head = new node(v_);
else
node* temp = new node(v_);
temp->next = head;
head = temp;
;
【讨论】:
我已编辑您的帖子以修复代码格式。但是,当此问题的标签使用 C 语言时,您的代码是 C++。请考虑编辑您的代码,以便使用纯 C 语法(即new
而不是 malloc/calloc
、nullptr
而不是 NULL
等。 )。【参考方案13】:
假设我在卡片 1 上记下了您的家庭地址。现在,如果我想将您的家庭地址告诉其他人,我可以将地址从 card-1 复制到 card-2 并给 card-2,或者我可以直接给 card-1。无论哪种方式,该人都会知道地址并可以联系到您。但是当我直接给card-1时,可以在card-1上更改地址,但是如果我给card-2,则只能更改card-2上的地址,而不是在card-1上。
将指针传递给指针类似于直接访问 card-1。传递指针类似于创建地址的新副本。
【讨论】:
【参考方案14】:我认为您的困惑可能是因为这两个函数都有一个名为head
的参数。这两个head
实际上是不同的东西。第一个代码中的head
存储了头节点指针的地址(它本身存储了头节点结构的地址)。而第二个head
直接存储头节点结构的地址。而且由于这两个函数都返回新创建的节点(应该是新的头),我认为没有必要采用第一种方法。此函数的调用者负责更新他们拥有的头部引用。我认为第二个已经足够好并且看起来很简单。我会选择第二个。
【讨论】:
【参考方案15】:命名约定 - Head 是造成混淆的原因。
头是尾,尾是头。尾巴摇摇头。
Head 只是一个 Pointer,Data 是 Null - 而 Tail 只是 Data,Pointer 是 Null。
所以你有一个指向结构指针的指针。结构指针指向链接列表中的第一个节点结构。 这个指向第一个结构节点指针的指针称为 Head。最好叫 startptr 或 headptr。
当您抓住 startptr 时,您就抓住了链表。那么就可以遍历所有的struct节点了。
【讨论】:
以上是关于在链表中添加节点时使用双指针的原因是啥?的主要内容,如果未能解决你的问题,请参考以下文章