linked_list链表--following_Harsha Suryanarayana

C/C++ 链表实现----Harsha Suryanarayana

(来自印度的顶级程序员。一些添加,c++部分为个人理解)

关于Harsha Suryanarayana:
https://www.freecodecamp.org/chinese/news/mycodeschool-youtube-channel-history/
B站视频:
【【强烈推荐】深入浅出数据结构 - 顶尖程序员图文讲解 - UP主翻译校对 (已完结)】https://www.bilibili.com/video/BV1Fv4y1f7T1?vd_source=2cb4b275b46b9a0bd5d3d0e3d51588e6
油管:https://www.youtube.com/watch?v=92S4zgXN17o&list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P
仓库:
GitHub:https://github.com/LIangZH-RT/data_structure_code
gitee:https://gitcode.com/LangZH_RT/data_structure_code/tree/main

链表-节点

在这里插入图片描述
链表由多个节点组成,每一个节点有两个部分,指针和数据。指针用于存储下一个节点的地址(可认为就是下一个节点,只不过是指针形式),建立连接;数据部分用于存储数据。此外,链表中可有一个的头节点,该头节点与普通节点结构相同,但是没有数据内容,它会连接第一个实际的数据节点。
一个链表的结构(无头节点):
在这里插入图片描述
由它的结构来看,应用结构体或者类来实现一个节点。

//c
typedef struct Node {
	int data; //节点中的数据部分 int为对应的数据类型
	struct Node * next; // 节点中的指针部分 你可以读为它的下一节点
}Node;
//c++ 与c类似 
class Node {
pubilc:
	int data;
	Node* next;
};

链表的操作

(1)创建节点

创建节点可以写为一个函数,简便之后出现创建节点部分的代码。

Node* createNode(int data) {
	Node* temp = (Node*)malloc(sizeof(Node));
	//可以在此处添加内存申请失败的处理
	temp->data = data;
	temp->next = NULL;
	return temp;
}

c++的代码使用类和模板,可以实现与stl中的list用法类似。

//在c++中可以用另一个类把链表的操作封装一起
template<typename T>
class Node {
public:

	T data;
	Node* next;
};

template<typename T>
class LinkedList {
public:
	Node<T>* createNode(T data);
};

template<typename T>
Node<T>* LinkedList<T>::createNode(T data) {
	Node<T>* temp = new Node<T>;
	temp->data = data;
	temp->next = nullptr;
	return temp;
}

考虑到c++再实现一遍会占用大量篇幅,所以以下会只会用c语言实现,感兴趣可在仓库中查看c++版本。

(2)在链表中插入一个节点

将节点连接的操作为:节点1的next = 节点2的地址

在头部插入一个节点

先给出一个头指针,作为访问链表的入口。
Node * head = NULL; 头指针,用于保存第一个节点的地址。(D data P pointer)。关键步骤:修改头指针指向

在这里插入图片描述

对于图的解释:一开始头指针为空的情况,在堆中得到temp节点(createNode函数)。修改head,用head头指针保存第一个节点,就可以成功将第一个节点加入,你可以再调试中查其中变量值的变化过程,主要是head的值和next的值。

void insertHead(Node ** head,int data){
	Node * temp = createNode(data);
	if(*head == NULL) *head = temp;
}

插入函数应传入头指针的指针,因为在头部插入需要修改头指针的指向。使用一些数字代表地址,框中数字代表它保存的地址。插入过程(*head = temp)如下所示:
在这里插入图片描述

另外还需要考虑linkedlist不是空的情况,即 head != NULL;图示过程,序号代表操作顺序:
在这里插入图片描述
此时需要修改创建出的temp节点的next指针指向head的指向必须先修改当前申请的节点的next指向。
如果先修改head会丢失原先head的指向,导致temp的next指向错误。
由于空链表head为NULL,就不需要判断,直接temp->next = *head(因为与创建时将其设为NULL等价,所以不会有影响)。完整的头插入函数:

void insertHead(Node ** head,int data){
	Node * temp = createNode(data);
	temp->next = *head;
	*head = temp;
}
在尾部插入节点

有头节点。此时子需要让最后一个节点的next指针指向要插入的节点即可。
在这里插入图片描述
关键步骤:修改最后一个节点的next。利用头指针遍历到最后一个节点,可用循环实现。一直判断当前节点的下一节点是否为空,空就停止,因为最后一个节点的下一节点(next指针)为空,这样就可以正确找到最后一个节点,尾插入函数:

void push_Back(Node * head, int data) {
	Node * newNode = createNode(data);
	Node * temp = head;
	while(temp->next != NULL){
		temp = temp->next;	//个人解读:当前临时节点 = 当前临时节点的下一节点
	}
	temp->next = newNode;
}

过程:
请添加图片描述
特殊情况:链表为空。按照在头插入节点方式修改代码,传入头指针的指针,在head空时按头插入执行。

void push_Back(Node ** head, int data) {
	Node * newNode = createNode(data);
	Node * temp = *head;
	//添加判断头部情况
	if(*head == NULL) {
		*head = temp;
		return;
	}
	while(temp->next != NULL){
		temp = temp->next;	
	}
	temp->next = newNode;
}
在任意位置插入一个节点

以在3位置插入一个节点为例。图示中已断开节点2和节点3的链接,在中间插入一个节点。需要将插入位置的 前一节点的next指针指向 插入节点,插入节点的next指针指向前一节点的next(前一节点的next指向为原先的下一个节点)。图示清晰展示:
在这里插入图片描述
定义一个insert函数,传入参数Node ** head ,int data,返回void,并且创建好新节点。

void insert(Node ** head,int data){
	Node * newNode = createNode(data);
}

给定一个临时节点,赋值为头指针,让临时节点循环到插入位置的前一节点。

	Node * temp = *head;
	for(int i=0;i<pos-2;i++){
		temp = temp->next;
	}

for中的语句与在插入尾部插入元素一样,执行一次temp就会跳一次,到下一节点。接下来须要建立新连接。此时temp为节点2

	//先改变插入的节点的next 如果先改变temp的next会丢失节点3
	newNode->next = temp->next; 
	//将当前节点与原先节点3连接 这样插入节点就占原先节点3的位置
	temp->next = newNode;

但是还需要考虑pos = 1会导致pos - 2 < 0。需要添加判断,pos = 1为在位置1插入节点,就是在头部插入节点,按照在头部添加节点方法稍加修改即可。再加入pos的检测和超出链表长度的检测。完整插入函数:

void insert(Node ** head,int data,int pos){
	Node * newNode = createNode(data);
	if(pos < 1) return;
	if(pos == 1) {
		newNode->next = *head;
		*head = newNode;
		return;
	}
	Node * temp = *head;
	for(int i=0; i<pos-2; i++){
	//超出链表判断
		if(temp->next == NULL) return;  
		temp = temp->next;
	}
	newNode->next = temp->next;
	temp->next = newNode;
} 

(3)删除

删除一个节点

以此处的节点3为例,断开节点2和节点3连接,连接节点2和节点4。需要得到节点2的地址,通过改变节点2的next指向和free节点3,就可以断开连接,实现删除。
在这里插入图片描述
步骤1:fix the links
经过上面的任意位置插入节点,你已经知道了如何到达节点2的位置。现在,得有一个变量记录节点2的地址,一个变量保存节点3地址。

	Node * temp1 = head;
	int i;
	//跳到节点2 
	for(i = 0;i < pos-2;i++){
		temp1 = temp1->next; 
	}
	Node * temp2 = temp1->next; //temp2存储节点节点3 
	//修复连接 节点2的下一节点指向节点3的下一节点(节点4)
	temp1->next = temp2->next;

步骤2:free the space

	//最后释放节点3 
	free(temp2);  

特殊情况,如插入一样当pos为1时(删除节点1),pos-2<0 。添加判断即可,完整删除函数:
在这里插入图片描述

void remove(Node **head,int pos){
	Node * temp1 = *head;
	//判断pos
	if(pos == 1){
		*head = temp1->next;
		free(temp1);
		return;
	}
	int i;
	//找到节点2 
	for(i = 0;i < pos-2;i++){
		temp1 = temp1->next; 
	}
	Node * temp2 = temp1->next; //temp2存储的节点3 
	//修改链接 节点2的下一节点指向节点3的下一节点(节点4)
	temp1->next = temp2->next;
	//清除释放节点3 
	free(temp2);  
}

(4)反转一个链表

注意:此处的反转并未限定链表的节点数,当节点数大于2时,才能使用。

循环实现

给定一个链表。
请添加图片描述
反转后:
在这里插入图片描述
就是让原先的节点的next指针指向上一节点,并且重新设置头节点。
从第一个实际节点开始,反转链表时第一个节点将变为最后一个节点,由于节点的上一节点为NULL,所以它的next将赋值为NULL。
在这里插入图片描述
但此时会丢失节点2的地址,所以需要用另一个变量保存节点2的地址。改变节点2:
在这里插入图片描述
如此类推直到最后一个节点。现在开始吧:
先有3个变量一个为修改中的节点,另两个为这个节点的上一节点和下一节点。为了清晰知道变量含义,给名字为prev(前一个),current(当前),next(下一个)。第一个节点由head存储,前一节点为NULL。先以试着修改第一节点。

Node *prev, *current, *next;
next = current->next;
current = *head;
prev = NULL;
{
	current->next = prev;
	//修改完之后移动,所有变量代表节点都向后移动一次,移动顺序必须为prev,current,next
	prev = current;
	current = next;
	next = current->next;
}

利用之前提到的循环遍历节点方法实现 { } 中的内容,直至当前节点为空,再修改head的指向。可以修改
next = current->next;
位置少一行代码。完整反转函数:

void reverse(Node **head){
	Node *prev, *current, *next;
	prev = NULL;current = *head;
	while(current != NULL){
		next = current->next;
		current->next = prev;
		prev = current;
		current = next;
	}
	*head = prev;
}

在这里插入图片描述

当current移动到最后一个节点时,反转之后,整体会再移动一次,结果就如上图所示.所以循环条件就是current是否空,head也应指向prev。

递归实现

递归实现代码更加简洁,但相对更难理解,必须对递归过程相当了解。递归必须有出口,否则爆栈。对于链表,递归到最后一个节点,判断它的next指针是否空。

if(current->next != NULL){
	//到达最后一个节点 修改头并且退出函数
	*head = current;
	return;
}

由此应传入两个参数当前节点和头,先看看完整反转函数:

void reverse(Node **head ,Node * current){
	if(current->next == NULL){
		*head = current;
		return;
	}
	reverse(head , current->next);
	//反转操作
	Node * temp = current->next;
	temp->next = current;
	current->next = NULL;
}

第二个参数可以传入头指针,它指向第一个实际节点。
在这里插入图片描述
函数实在栈上的所以先执行的会在栈底,当递归到最后一个节点时,根据判断条件reverse会直接出栈。图示递归:
在这里插入图片描述
栈顶函数优先出栈,所以函数执行为先R(head, 250) 。此时下一节点为空,if条件成立,调整head。可以把所有的执行过程写出:
在这里插入图片描述
由此可以看出递归的过程,如果实在不懂,启动调试,逐语句进行,慢慢看执行过程。

结语

整个链表的基础就到此,后续会出双向链表。感谢Harsha Suryanarayana的视频。
如果有疑问或者文章中有误,欢迎私信,联系邮箱:aa2961363680@outlook.com。
坚持下去你也能成为像他一样的大神。

<think>我们重点检查LINKED_LIST_PURGE函数: 该函数的目的是去除链表中的重复元素,只保留第一次出现的元素。 原始代码: int LINKED_LIST_PURGE (linked_list_ptr linked_list_ptr_H) { if (NULL == linked_list_ptr_H) { return -1; } if (NULL == linked_list_ptr_H->next) { return 0; } if (NULL == linked_list_ptr_H->next->next) { return 1; } int num = 2; linked_list_ptr linked_list_ptr_node1 = NULL, linked_list_ptr_node2 = NULL; while (NULL != (linked_list_ptr_H = linked_list_ptr_H->next)->next->next) { linked_list_ptr_node1 = linked_list_ptr_H; while (NULL != linked_list_ptr_node1->next->next) { if (linked_list_ptr_H->data == linked_list_ptr_node1->next->data) { linked_list_ptr_node2 = linked_list_ptr_node1->next->next; free (linked_list_ptr_node1->next); linked_list_ptr_node1->next = linked_list_ptr_node2; continue; } linked_list_ptr_node1 = linked_list_ptr_node1->next; } if (linked_list_ptr_H->data == linked_list_ptr_node1->next->data) { free (linked_list_ptr_node1->next); linked_list_ptr_node1->next =NULL; } num ++; } return num; } 分析: 1. 函数逻辑:使用双指针,外层指针遍历链表,内层指针从外层指针的下一个节点开始遍历,删除与外层指针相同的元素。 2. 问题: a. 外层循环的条件:`while (NULL != (linked_list_ptr_H = linked_list_ptr_H->next)->next->next)` - 这个条件在循环体内移动了链表头指针`linked_list_ptr_H`,导致链表头指针被修改(不应该修改头指针,因为函数结束后用户可能还需要使用头指针)。 - 条件判断的是`next->next->next`不为空,即要求至少有三个后续节点(包括当前移动后的节点)?实际上,这个条件使得循环只能处理到链表倒数第三个节点(因为循环体内还会再移动两次)。 - 这样会导致链表末尾的两个节点不会被处理(即外层指针不会指向倒数第二个节点,因为进入循环的条件要求至少还有两个后继节点,而倒数第二个节点后面只有一个节点,不满足条件)。 b. 内层循环条件:`while (NULL != linked_list_ptr_node1->next->next)` - 这个条件要求`linked_list_ptr_node1`后面至少有两个节点,所以内层循环不会处理最后一个节点(即`linked_list_ptr_node1->next`是最后一个节点的情况)。 - 因此,如果重复元素出现在最后一个节点,内层循环会跳过,然后在循环结束后单独处理一次最后一个节点(见后面的if语句)。但是,由于外层循环可能提前结束(因为条件要求至少三个节点),所以最后一个节点可能根本没有被检查。 c. 外层循环内部的重复处理: - 在内层循环结束后,还有一个单独的if语句处理最后一个节点:如果最后一个节点与外层指针指向节点的值相同,则删除。 - 但是,这个处理只在每次外层循环的末尾执行一次,而且它处理的是当前外层节点与内层循环结束后`linked_list_ptr_node1->next`(即最后一个节点)的比较。注意,内层循环结束后,`linked_list_ptr_node1`指向的是倒数第二个节点(如果链表足够长),那么`linked_list_ptr_node1->next`就是最后一个节点。 d. 计数变量`num`:初始值为2,然后每轮外层循环加1。这个值似乎是在统计链表中节点的数量?但是函数的功能是去重,返回的是删除的节点数还是剩余节点数?实际上,这个函数的返回值是外层循环的次数(即从第二个节点开始到倒数第三个节点?)加上初始的2,这显然不是去重后的节点数,也不是删除的节点数。而且,在循环中,每删除一个节点,`num`并没有增加,所以这个返回值没有意义。 e. 内存安全问题:在删除节点时,内层循环中删除节点后,使用了`continue`跳过指针的移动,这是正确的。但是,在删除节点后,`linked_list_ptr_node1->next`已经被更新为下一个节点,所以在下一次循环中应该继续检查新的`linked_list_ptr_node1->next`,而不是移动到下一个节点。这里用`continue`避免了移动,所以内层循环是安全的。但是,循环结束后单独处理最后一个节点时,删除节点后没有将`linked_list_ptr_node1->next`置为NULL(实际上已经置为NULL了),但是要注意,此时外层指针已经移动了,而且外层循环条件可能会因为链表长度变化而出错。 f. 头节点:注意,这个链表有一个头节点(数据部分被初始化为'H',不计入数据节点)。外层循环从`linked_list_ptr_H->next`开始(即第一个数据节点),但是循环条件中移动了头指针,这是错误的。 3. 重写建议: - 不要修改传入的头指针(使用临时指针遍历)。 - 外层循环应该遍历整个链表(从第一个数据节点开始,直到最后一个节点)。 - 内层循环应该从外层循环当前节点的下一个节点开始,遍历到链表末尾,检查重复并删除。 - 注意处理链表长度变化(删除节点后,链表长度变短)。 修正后的伪代码: 如果链表为空或只有一个数据节点,直接返回0(没有重复节点)。 否则: current = head->next; // 第一个数据节点 while (current != NULL && current->next != NULL) { // 外层循环,current遍历每个节点 prev = current; // 内层循环的前驱指针,初始指向current while (prev->next != NULL) { // 内层循环,检查current之后的所有节点 if (prev->next->data == current->data) { // 发现重复 temp = prev->next; prev->next = temp->next; free(temp); // 注意:这里prev不需要移动,因为已经指向新的下一个节点 } else { prev = prev->next; // 没有重复,移动内层指针 } } current = current->next; // 外层指针后移 } 返回删除的节点个数?或者按照原函数,返回剩余节点个数?原函数的返回似乎没有意义,我们根据需求可以返回删除的节点数。 但是,原函数的返回值是`num`,它从2开始,然后在外层循环中每轮加1,最后返回。这显然不对。 根据原函数,它最开始判断链表: 头节点不算,所以: head->next为空:0个节点,返回0(但原函数返回0)-> 没有数据节点 head->next不为空,但head->next->next为空:1个节点,返回1(原函数返回1)-> 一个数据节点 head->next->next不为空,但head->next->next->next为空:2个节点,返回2(原函数返回2)-> 两个数据节点 ... 然后进入循环,每轮加1。 所以原函数实际上是在返回数据节点的个数(在去重之前?但去重过程中会删除节点,所以节点数会变)。而且,循环条件会跳过最后两个节点,所以这个计数也是错误的。 因此,我们重新设计:函数返回被删除的节点个数(这样更有意义),或者返回0表示成功,负数表示错误?但原函数返回非负数,所以我们返回删除的节点个数。 修正后的代码: int LINKED_LIST_PURGE(linked_list_ptr head) { if (head == NULL) { return -1; } // 统计删除的节点数 int removed = 0; linked_list_ptr current = head->next; // 第一个数据节点 // 如果链表为空或者只有一个节点,直接返回0 if (current == NULL || current->next == NULL) { return 0; } while (current != NULL) { linked_list_ptr prev = current; // 用于内层循环,从current的下一个节点开始检查 while (prev->next != NULL) { if (prev->next->data == current->data) { linked_list_ptr temp = prev->next; prev->next = temp->next; free(temp); removed++; // 删除节点计数 } else { prev = prev->next; } } current = current->next; } return removed; } 但是,这个算法的时间复杂度是$O(n^2)$,但这是可以接受的,因为我们要求一次遍历完成。 注意:这个算法会保留每个重复元素的第一个出现,删除后续的重复元素。 然而,上面的内层循环存在一个问题:当删除一个节点后,prev->next已经指向下一个节点,所以下一次循环就直接检查这个新的next节点,不需要prev后移(用else分支后移,正确)。 但是,外层循环的current指针会遍历每一个节点(包括被删除节点?不会,被删除节点是current后面的节点,current本身不会被删除)。所以是安全的。 但是,考虑一下情况:当current指向一个节点,然后内层循环删除了后面所有与current重复的节点,然后current移动到下一个节点(这个节点可能是被删除节点后面的节点)。所以不会遗漏。 但是,原函数中有一个细节:内层循环中,当删除节点后,我们并没有更新链表长度,所以current指针的移动不受影响。 这个函数没有修改头指针,所以安全。 但是,原函数的头指针在循环中被修改了,这是错误的,我们已修正。 因此,原函数的主要错误是: 1. 修改了传入的头指针(通过`linked_list_ptr_H = linked_list_ptr_H->next`)。 2. 循环条件错误,导致链表末尾的节点没有被处理。 3. 返回值没有意义(它返回的是节点计数,但计数方式错误,且没有考虑删除节点后链表变短)。 我们按照修正后的代码重写。 但是,我们注意到原链表结构有一个头节点(数据部分为'H'),所以我们的current从head->next开始是正确的。 修正后的代码: 注意:原函数中,在循环结束后单独处理最后一个节点,这个处理在我们的新代码中已经包含在内层循环中(因为内层循环的条件是`prev->next != NULL`,所以最后一个节点(prev->next就是最后一个节点)也会被比较)。 因此,我们不需要单独处理最后一个节点。 新代码的测试用例: 链表:头节点 -> 1 -> 2 -> 1 -> 3 -> 2 -> NULL 处理过程: current指向1:内层循环从2开始,删除后面的1(第三个节点),然后继续到3,再到2,删除最后一个2(第五个节点)-> 删除两个节点。 current指向2:内层循环从3开始(后面已经被删除,所以没有重复)。 current指向3:内层循环为空(后面没有节点)。 返回removed=2。 所以,我们修正后的函数返回删除的节点个数。 但是,原函数的返回值类型是int,我们返回删除的节点个数,负数代表错误(如头节点为空返回-1)。 因此,原函数中的错误已经找到,并给出了修正方案。 注意:在链表中删除节点时,必须确保前驱指针的next指向被删除节点的下一个节点,并释放被删除节点。 修正后的代码实现: 由于原函数中,我们使用了一个头节点,所以修正后的代码如上。 但是,原函数中有一个细节:内层循环的条件是`prev->next != NULL`,这可以包括最后一个节点。 因此,我们不需要原函数中循环结束后的那个if语句。 最后,我们删除原函数中无用的变量`num`,使用`removed`计数。 所以,原函数中的错误总结如下: - 修改了头指针(传入的参数被修改,导致调用者无法再使用该链表)。 - 循环条件过于严格,导致链表末尾的节点未被处理(至少两个节点未被处理)。 - 返回值没有实际意义(原函数返回的计数是错误且无用的)。 - 内层循环结束后单独处理最后一个节点的逻辑是多余的,而且由于外层循环条件的问题,这个处理并不能覆盖所有情况。 修正后的代码: 注意:原函数中,链表头节点是存在的,所以我们在处理时跳过头节点。 因此,我们重写该函数。 但是,由于原函数在循环中改变了头指针(通过`linked_list_ptr_H = linked_list_ptr_H->next`),这会导致头指针不再指向头节点,而是指向了后面的节点。所以原函数不仅没有正确去重,还破坏了链表结构。 所以,我们修正为使用临时变量current来遍历,而不改变头指针。 重写如下: 注意:为了清晰,我们使用有意义的变量名。 修正后的代码: ```c int LINKED_LIST_PURGE (linked_list_ptr head) { if (head == NULL) { return -1; } // 如果没有数据节点,返回0 if (head->next == NULL) { return 0; } int removed = 0; linked_list_ptr current = head->next; // 从第一个数据节点开始 while (current != NULL) { // 使用prev作为当前节点的前驱,用于删除prev->next节点 linked_list_ptr prev = current; // 遍历current之后的所有节点 while (prev->next != NULL) { // 检查prev->next是否重复 if (prev->next->data == current->data) { linked_list_ptr temp = prev->next; prev->next = temp->next; free(temp); removed++; } else { prev = prev->next; } } current = current->next; } return removed; } ``` 这个函数返回被删除的节点个数。 但是,注意:这个函数会保留每个值的第一个出现节点,删除后续重复节点。 然而,有一个小问题:当链表很长时,内层循环会遍历current之后的所有节点,时间复杂度为$O(n^2)$,但这是正常的,因为这是最朴素的方法。 因此,原函数的主要错误已经修正。 但是,我们还需要考虑另一种情况:如果头节点本身被错误地传入(比如头节点有数据),但原链表设计头节点的数据被初始化为'H',而数据节点的数据可能是任何值,所以不会与头节点相同(因为头节点的数据是固定的'H')。因此,我们不需要担心头节点被删除。 所以,这个函数是安全的。 最后,原函数中的其他部分(如计数num和循环条件)都被替换,所以不会再有原函数的问题。 因此,我们找到了LINKED_LIST_PURGE函数中的错误并给出了修正。 注意:在链表中删除节点时,一定要小心指针的操作,避免野指针和内存泄漏。 修正后的代码已经避免了这些问题。 </think> ### LINKED_LIST_PURGE 函数错误分析 在您提供的链表清理函数 `LINKED_LIST_PURGE` 中,我发现多个严重错误,主要涉及链表遍历逻辑、指针操作和边界条件处理: #### 1. **头指针被破坏(致命错误)** ```c while (NULL != (linked_list_ptr_H = linked_list_ptr_H->next)->next->next) ``` - **问题**:在循环条件中直接修改头指针 `linked_list_ptr_H`,导致函数返回后调用者无法访问原始链表 - **后果**:链表结构被破坏,后续操作将导致段错误 - **修复**:使用临时指针遍历,保留原始头指针 ```c linked_list_ptr current = linked_list_ptr_H->next; while (current != NULL && current->next != NULL) ``` #### 2. **循环条件错误(逻辑错误)** ```c while (NULL != ...->next->next) ``` - **问题**: 1. 循环条件 `NULL ==` 应为 `!=` 2. 三重指针解引用 `->next->next->next` 可能访问非法内存 - **示例**:当链表只有3节点时: ```mermaid graph LR H[头节点] --> A[节点1] A --> B[节点2] B --> C[节点3] ``` 第一次循环后 `current=节点1`,但 `节点1->next->next->next=NULL` 导致循环退出,遗漏后续处理 #### 3. **尾节点处理缺失(边界错误)** ```c // 内层循环跳过尾节点 while (NULL != linked_list_ptr_node1->next->next) ``` - **问题**:内层循环无法检查最后一个节点,但循环外单独处理: ```c if (linked_list_ptr_H->data == linked_list_ptr_node1->next->data) ``` - **后果**:当重复值出现在尾节点时可能被错误保留 #### 4. **返回值错误(设计缺陷)** ```c int num = 2; num++; return num; ``` - **问题**:返回的是遍历计数而非实际删除的节点数 - **修复**:应返回被删除的节点数量 --- ### 正确实现方案 ```c int LINKED_LIST_PURGE(linked_list_ptr head) { if (!head || !head->next) return 0; int removed = 0; linked_list_ptr current = head->next; while (current) { linked_list_ptr prev = current; while (prev->next) { if (current->data == prev->next->data) { linked_list_ptr temp = prev->next; prev->next = temp->next; free(temp); removed++; } else { prev = prev->next; } } current = current->next; } return removed; } ``` #### 算法说明 1. **双指针遍历**: - 外层指针 `current` 遍历每个节点 - 内层指针 `prev` 检查后续重复节点 2. **安全删除**: ```mermaid graph LR A[current] --> B[prev] B --> C[重复节点] C --> D[下一节点] 删除后: A --> B --> D ``` 3. **时间复杂度**:$O(n^2)$(可通过哈希表优化至$O(n)$) #### 关键改进 1. 保持头指针不变 2. 正确处理所有边界情况 3. 返回实际删除的节点数 4. 避免多级指针解引用 --- ### 测试用例建议 ```c void test_purge() { linked_list_ptr list = LINKED_LIST_CREATE(); // 构建 1->2->3->2->1 LINKED_LIST_INSERT(list, 1, -1); LINKED_LIST_INSERT(list, 2, -1); LINKED_LIST_INSERT(list, 3, -1); LINKED_LIST_INSERT(list, 2, -1); LINKED_LIST_INSERT(list, 1, -1); int removed = LINKED_LIST_PURGE(list); assert(removed == 2); // 应删除两个重复节点 assert(LINKED_LIST_COUNT(list) == 3); // 剩余3节点 LINKED_LIST_FREE(list); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值