链表添加函数中为什么要用指向链表指针的指针

本文详细解析了C++中如何实现链表的尾部添加节点功能,对比了不同参数传递方式的效果,包括值传递、指针传递及引用传递的区别,并通过实验验证了各种方法的有效性。
部署运行你感兴趣的模型镜像

        在看书的时候有个往链表里添加节点的函数,代码中考虑到可能给出的头指针为空,并做另外一些处理。具体代码如下:


#include <iostream>
#include <string>
using namespace std;

struct ListNode
{
	int val;
	ListNode* next;
};

void AddToTail(ListNode** pHead, int value);

int main() {
	// TODO
}

void AddToTail(ListNode** pHead, int value) {
	ListNode* pNew = new ListNode();
	pNew->val = value;
	pNew->next = NULL;

	if (*pHead == NULL) {
		*pHead = pNew;
	}
	else {
		ListNode* p = *pHead;
		while (p->next != NULL) {
			p = p->next;
		}
		p->next = pNew;
	}
}


        网上其他人的博客中对函数AddToTail的参数的描述跟书中如出一辙:第一个参数pHead是一个指向指针的指针,当向一个空链表插入一个节点时,新插入的节点是链表的头指针,此时会改动头指针,因此必须把pHead参数设置为指向指针的指针。

        为什么呢?在以前学习C++的时候,我们只知道在参数中,以传值的形式作为参数的变量在函数体内被修改之后,出了函数体就会失效,准确的说这个变量没有被修改过,因此需要传入该变量的指针或者使用引用传参的方式。可是上述AddToTail中已经是一个指针了啊?于是我测试了一下,不使用指针的指针会怎样:

#include <iostream>
#include <string>
using namespace std;

struct ListNode
{
	int val;
	ListNode* next;
};

void AddToTail(ListNode* pHead, int value);

int main() {
	// TODO
	ListNode* head = NULL;
	AddToTail(head, 10);
	if (head != NULL) {
		cout << head->val << endl;
	}
	else {
		cout << "head is NULL.." << endl;
	}
	
}

void AddToTail(ListNode* pHead, int value) {
	ListNode* pNew = new ListNode();
	pNew->val = value;
	pNew->next = NULL;

	if (pHead == NULL) {
		pHead = pNew;
	}
	else {
		ListNode* p = pHead;
		while (p->next != NULL) {
			p = p->next;
		}
		p->next = pNew;
	}
}


        运行结果如下



        作为指针pHead竟然真的没被修改过!

        其实真的很好理解,既然你懂得函数中的值传参,假设int a,作为参数传入的时候没被修改,所以需要用指向a的指针,那么应该也可以理解,指针变量pHead作为参数传入的时候被修改无效,因此需要用指向pHead的指针,只不过pHead本身就是一个指针了,所以才存在有指针的指针看起来稍微复杂一点的说法。因为,指向a的指针作为参数传入进去时,如果你对它进行修改,其实也是无效的,但是修改指针指向的内容的修改是有效的,也即,(&a)对a取地址得到的指针传入进去之后,此时你修改这个指针也是没有什么实际作用的,原因我等下会说。但是,你修改指针指向的内容这就有效了,因此通常我们在函数体内是修改对指针取内容后的内存,即*(&a)。所以,你对指针pHead的修改时无效的,只有对指向pHead的指针指向的内容(很绕吧,其实就是pHead),这时候才是有效的,因此AddToTail的第一个参数必须用指针的指针。


        现在来说说为什么对值传参在函数体内的修改无效。因为a传进去的时候会被复制了一份copy,此后的修改都是在临时变量copy上,出了函数体copy被销毁,a还是原来的a,根本就没被修改过,所以才会值传参对变量的修改无效。要使得对a的修改有效,一方面是传入a的地址,也就是对指向a的指针作为值传参(反正修改的不是a的指针,修改了也无所谓,反正只是修改a的指针的copy),此时a的指针的copy指向的内容也是a,因此对copy指向的内容修改会导致a的内容也被修改,check!另外一种方式就是引用传参,引用传参往往要比值传参高效,因为它是直接将a作为参数传入进去,而少了对a进行复制这部分的开销,既然传入进去的是a,那么对a的修改肯定也生效。


        为了证明上述废话,我将代码2中的AddToTail函数的第一个参数也作为引用参数传入(指向指针的指针肯定正确啦,就不测试了),此时预测的结果是修改有效。代码如下:

#include <iostream>
#include <string>
using namespace std;

struct ListNode
{
	int val;
	ListNode* next;
};

void AddToTail(ListNode* &pHead, int value);

int main() {
	// TODO
	ListNode* head = NULL;
	AddToTail(head, 10);
	if (head != NULL) {
		cout << head->val << endl;
	}
	else {
		cout << "head is NULL.." << endl;
	}
	
}

void AddToTail(ListNode* &pHead, int value) {
	ListNode* pNew = new ListNode();
	pNew->val = value;
	pNew->next = NULL;

	if (pHead == NULL) {
		pHead = pNew;
	}
	else {
		ListNode* p = pHead;
		while (p->next != NULL) {
			p = p->next;
		}
		p->next = pNew;
	}
}

        只是简单的在代码2中的函数声明和定义中,第一个参数加入了"&"表示使用一个引用参数,结果如下图,check!


您可能感兴趣的与本文相关的镜像

ACE-Step

ACE-Step

音乐合成
ACE-Step

ACE-Step是由中国团队阶跃星辰(StepFun)与ACE Studio联手打造的开源音乐生成模型。 它拥有3.5B参数量,支持快速高质量生成、强可控性和易于拓展的特点。 最厉害的是,它可以生成多种语言的歌曲,包括但不限于中文、英文、日文等19种语言

<think> 首先,用户的问题是关于遍历循环链表是否需要使用双重指针以及原因。我需要参考用户提供的引用信息来回答这个问题。 用户提供的引用: - [1]: 讨论了循环单链表和双向循环链表的特性,提到循环链表可以从任意节点开始遍历整个链表,设置尾指针更高效。 - [2]: 定义了不同类型的链表:单向链表、双向链表和循环链表。提到双向链表有前驱和后继指针。 - [3]: 展示了双向循环链表的销毁函数,使用指针遍历和释放节点。 - [4]: 描述了使用双重循环遍历链表删除重复元素,使用了四个指针:first, second, p, pre。 用户的问题焦点是遍历循环链表是否需要“双重指针”。这里的“双重指针”可能指的是指针的指针(如 **ptr),或者在遍历过程中使用多个指针变量(如两个指针)。 从引用[4]中,我看到它描述了删除重复元素时使用四个指针,但这似乎是针对一般链表,不是特指循环链表。而且它提到了“双重循环”,指的是嵌套循环,而不是双重指针。 在C语言中,“双重指针”通常指的是指向指针的指针(double pointer),例如 node **ptr。这在链表操作中常用于修改头指针或处理指针的引用。 现在,针对遍历循环链表: - 遍历链表本质上是访问每个节点一次。 - 对于一个循环链表,无论是单链表还是双向链表,遍历通常需要一个指针指向当前节点。 - 从一个起始点开始,移动指针直到回到起始点。 标准遍历循环链表不需要双重指针: - 对于一个单链表循环链表,遍历只需要一个当前指针(current)。从某个节点开始,移动到 next 直到回到起始节点。 - 对于双向链表循环链表,同样可以使用一个当前指针。 但是,在某些操作中,可能需要多个指针: - 例如,在删除节点时,可能需要前驱指针(prev)来更新链接。 - 在引用[3]中,销毁双向循环链表时,使用了一个指针p和一个临时指针pp,但不是双重指针(pointer to pointer)。 - 在引用[4]中,删除重复元素使用多个指针,但这是嵌套循环操作,不是单纯的遍历。 关键点:单纯的遍历操作(只读访问每个节点)不需要双重指针;但如果在遍历过程中需要修改链表结构(如插入、删除),可能需要额外指针或双重指针。 原因: - 双重指针(**ptr)常用于需要修改头指针或传递指针引用的函数中。 - 在遍历时,如果只是访问数据,一个指针足矣。 - 循环链表的特点:可以从任何节点开始,遍历所有节点。设置一个起始点,移动到 next 直到返回起点。 参考引用: - [1] 说循环链表可以从任意结点开始遍历。 - [2] 描述遍历方式。 - [3] 销毁函数使用单指针遍历。 - [4] 使用多个指针处理删除操作,但不是必须双重指针。 所以,我的回答应是:遍历循环链表通常不需要双重指针;一个指针即可。但如果在操作中需要修改链表,可能需要额外机制。 回答结构: 1. 解释什么是遍历循环链表。 2. 说明是否需要双重指针:一般情况下不需要。 3. 解释原因。 4. 结合引用信息。 5. 如果需要,给出示例。 6. 生成相关问题(根据系统指令)。 系统级指令回顾: - 使用中文回答。 - 行内数学表达式用 $...$,如 $O(1)$。 - 独立公式用 $$...$$。 - 语法正确。 - 生成相关问题。 - 引用的段落末尾添加引用标识,如 [^1]。 用户上一次问题和我的回答:用户的问题是新的,没有上一次上下文;用户说“上述引用仅供参考...请尽可能多的参考用户上一次的问题和你的回答”,但在这个对话中,这是第一个用户消息,所以没有历史。我应该基于当前引用回答。 最终回答: - 清晰结构:逐步解释。 - 确保真实可靠。 - 结束时生成相关问题。 </think> ### 遍历循环链表是否需要双重指针及原因 在数据结构中,循环链表是一种特殊的链表形式,其中尾结点的指针指向头结点,形成一个“环”。遍历链表是指访问链表中每个结点的操作。用户询问遍历循环链表是否需要使用“双重指针”(即指针的指针,如 `node **ptr`),我将基于提供的引用信息和链表原理逐步分析这一问题。 #### 1. **什么是双重指针?** - 双重指针(double pointer)是指一个指针指向另一个指针的地址,常用于修改指针本身(如头指针的地址)。在 C/C++ 中,语法为 `node **ptr`,其中 `ptr` 存储了指针的地址。 - 在链表操作中,双重指针常用于: - 需要修改头指针的场景(如插入或删除头结点)。 - 避免在函数中传递指针副本时导致的错误(因为函数参数是值传递)。 - 例如,在引用[4]中提到删除重复元素时使用了多个指针(如 `first`, `second`, `p`, `pre`),但这不是双重指针;这里是多个单指针变量,用于处理嵌套循环和结点删除。 #### 2. **遍历循环链表是否需要双重指针?** - **一般情况下,单纯的遍历操作不需要双重指针。** - 无论是单循环链表还是双向循环链表,遍历的本质是访问每个结点一次。这可以通过一个当前指针(如 `current`)完成。 - 遍历过程:从任意结点开始(如头结点或尾结点),不断移动到下一个结点(`current = current->next`),直到返回起始结点(循环结束)。 - 例如,在引用[1]中提到,循环链表可以从任意结点开始遍历整个链表,且设置一个尾指针即可高效操作(时间复杂度 $O(1)$ 访问头结点)[^1]。 - 在引用[3]的双向循环链表销毁函数中,遍历仅使用了一个指针 `p` 和一个临时指针 `pp`,没有涉及双重指针[^3]。 - **在特定操作中可能需要双重指针,但不是遍历本身所需。** - 如果遍历过程中涉及链表结构的修改(如删除结点),可能需要额外机制: - 对于单循环链表,删除结点时需要维护前驱结点的指针(如 `prev`),以避免链接断裂。如果删除操作会影响头指针,则双重指针可用来修改头指针的地址。 - 对于双向循环链表(如引用[3]),因为有前驱指针(`prev`),删除结点更简单,通常只需要单指针或临时变量,无需双重指针[^3]。 - 在引用[4]的删除重复元素场景中,使用了多个指针(但不是双重指针),因为双重循环(nested loop)需要同时跟踪多个位置[^4]。然而,这属于复杂操作,不是基本遍历。 - **原因总结:** 1. **遍历是只读操作**:单纯遍历不需要修改指针地址,一个当前指针 `current` 足以访问所有结点。循环链表的“环”结构确保了遍历的完整性(从任意点开始都能回到原点)。 2. **时间复杂度高效**:遍历循环链表的时间复杂度为 $O(n)$(n 为结点数),使用单指针即可实现,无需双重指针增加复杂度。 3. **链表类型不影响基本遍历**: - 单循环链表:一个 `current` 指针移动即可。 - 双向循环链表(如引用[2]所述):同样只需一个指针,利用 `next` 或 `prev` 方向遍历[^2]。 4. **双重指针的适用场景有限**:仅在需要动态修改头指针地址时有用(如插入新头结点),这在遍历中很少涉及。滥用双重指针会增加代码复杂性和错误风险。 #### 3. **示例说明:遍历循环链表的简单代码** 以下为一个单循环链表的遍历示例(C 语言),仅使用单指针: ```c // 定义链表结点 typedef struct Node { int data; struct Node *next; } Node; // 遍历函数:从头结点开始,打印所有数据 void traverse(Node *head) { if (head == NULL) return; // 空链表处理 Node *current = head; // 只需要一个指针 do { printf("%d ", current->data); // 访问当前结点 current = current->next; // 移动到下一个 } while (current != head); // 循环结束条件:回到头结点 } ``` - **为什么不用双重指针?** 这里 `traverse` 函数只需访问数据,不修改链表结构。如果需要删除结点(如引用[4]的场景),可添加一个 `prev` 指针,但仍无需双重指针[^4]。 #### 4. **关键结论** - **不需要双重指针**:遍历循环链表的核心操作是顺序访问,使用一个单指针足矣。双重指针要用于需要间接修改指针本身的场景(如头指针变更),这在遍历中不常见。 - **最佳实践**:优先使用单个指针遍历,以提高代码可读性和效率。只有在高级操作(如动态头指针修改)中才考虑双重指针。 如果需要进一步讨论链表操作优化,可参考相关引用或提出具体场景。
评论 21
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值