今天来刷 19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode) 这道题,文章内容分五个部分:
一、题目描述
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1
输出:[]
示例 3:
输入:head = [1,2], n = 1
输出:[1]
提示:
- 链表中结点的数目为 sz
- 1 <= sz <= 30
- 0 <= Node.val <= 100
- 1 <= n <= sz
二、解题思路
1、首先定义两个指针,快慢指针,都指向原链表的头节点。
2、把快指针先执行n次,当快指针为NULL时,慢指针指向的就是倒数第n个节点。
3、需要删除倒数第n个节点,所以需要其前驱结点指向其后继节点,这样就达到删除该节点的目的。
4、然后返回修改后的链表。
三、知识点
1、for条件语句
for语句的一般形式为:for(循环初始化表达式; 循环条件表达式; 循环执行表达式),其执行过程为:循环初始化表达式->循环条件表达式->循环体->循环执行表达式。例如:
for(int i = 0; i < 10; i++){
sum += i;
}
//执行过程为:
// (1) int i = 0; 循环初始化表达式
//(2)i < 10 循环条件表达式
//(3)sum += i; 循环体
//(4)i++ 循环执行表达式
2、scanf()
scanf ()命名含义是:Scan(扫描) 和 Format (格式),即 格式化输入。可以输入 整数(%d)、浮点数(%f)、字符(%c)、字符串(%s)等等。假如你在输入是,不小心按了多个数据,连续用scanf()函数获取数据的时候,会把后面输入的数据也写进去,比如:(后面解题有完整代码,这里将代码简化)
printf("请插入链表新节点数据,结束插入码值为-1:");
while(1){
scanf("%d\0",&val); //(1)
if(val == -1){
break;
}
//创建新节点
printf("%d",val) ;
struct ListNode* newNode = (struct ListNode*)malloc (sizeof(struct ListNode));
newNode->data = val;
newNode->next = NULL;
//新节点插入到链表中
pRear->next = newNode;
//更新尾部指针指向
pRear = newNode;
}
(1)当在窗口输入10 20 30(数据 空格 数据 空格 数据),然后回车,这里会把所有的数字都加入到链表中,而不是值把第一个数据10加入到链表中,舍弃其他的数据,这是因为当用户按下回车键后,scanf()函数的输入缓冲区被刷新,如果连续使用scanf函数,它会从输入缓冲区中读取之前的输入值,直到缓冲区中的数据被读取完毕。所以10 20 30(数据 空格 数据 空格 数据)会把所有的数字都加入到链表中。
3、结构体嵌套本类型的结构体指针变量
struct ListNode{
int data;
struct ListNode* next; //(1)
};
(1)当用户创建一个该类型的结构体变量,实际上 struct ListNode* next;指向的是其他该类型结构体变量的地址或者NULL。(这里可以看成就是一个指向同一结构体类型的指针变量,在32位系统中占4个字节),下面用代码说明一下:
struct ListNode{
int data;
struct ListNode* next;
};
// 用静态创建链表节点
struct ListNode* Node1 = {10,NULL};
struct ListNode* Node2 = {20,NULL};
Node1->next =Node; //(1)
(1)这时候的链表为 :10->20->NULL ,上面代码的内存布局示例:
struct ListNode* Node1 = {10,NULL};
+-------------+ <- 结构体Node1实例的起始地址
| 数据字段 (10) |
+-------------+
| 指针字段(NULL)| <- 初始化的时候指向NULL
--------------------------------------------
struct ListNode* Node2 = {20,NULL};
+-------------+ <- 结构体Node2实例的起始地址
| 数据字段 (20) |
+-------------+
| 指针字段(NULL)| <- 初始化的时候指向NULL
--------------------------------------------
Node1->next =Node2;
+-------------+ <- 结构体Node1实例的起始地址
| 数据字段 (10) |
+-------------+
| 指针字段(next)| <- 指向结构体Node2实例的起始地址
4、访问链表边界判断
下面的代码块是在struct ListNode* removeNthFromEnd(struct ListNode* head, int n);函数中的一部分,我这里犯了个错误:
for(i = 0; i < (n + 1) ; i++){ //(1)
if(fast == NULL){
return head->next;
}
fast = fast->next;
}
(1)我这里试图通过fast指针先走n+1步来定位到待删除节点的前一个节点,但是这样会有一个问题,就是当链表长度小于等于n+1时,我的代码可能会尝试访问空指针,可能会导致程序崩溃。
5、链表头节点特性
//删除倒数第n个节点
struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
struct ListNode* slow = head;
struct ListNode* fast = head;
int i;
// 快指针先走n步,并检查链表是否足够长
for(i = 0; i < n && fast != NULL; i++){
fast = fast->next;
}
// 如果n大于链表长度,则直接返回头结点(而非头结点的下一个结点)
if (fast == NULL) {
return head;
}
fast = fast->next;
while(fast){
fast = fast->next;
slow = slow->next;
}
struct ListNode* toRemove = slow->next;
slow->next = slow->next->next;
free(toRemove);
return head;
}
上面的代码实际上并没有直接修改head指针所指向的节点。head始终是原链表的头结点。执行了删除操作(即slow->next = slow->next->next;),修改的是链表内部结构,而head是这个链表的头节点,找到头结点就可以通过索引找到整条链表,所以返回head,就相当于返回修改后的链表的头节点,相当于返回了修改后的链表。
四、解题代码
我的代码会是完整的C代码,而不是题目中的某个功能模块,我会先把所有代码功能模块按顺序罗列出来。代码模块有:
1)void foreachLinkList(struct ListNode* head); //调试使用,遍历打印链表数据
2)struct ListNode* createLinkList(void); // 创建一个链表,窗口键盘输入链表节点数据
3)struct ListNode* removeNthFromEnd(struct ListNode* head, int n); //LeetCode题目答案,删除链表倒数第n个节点。
#include <stdlib.h>
#include <stdio.h>
#define USER_DEBUG
struct ListNode{
int data;
struct ListNode* next;
};
#ifdef USER_DEBUG
//调试使用,遍历打印链表数据
void foreachLinkList(struct ListNode* head){
if(NULL == head){
return;
}
printf("当前链表数据为:") ;
//辅助指针变量
struct ListNode* pCurrent = head->next;
while(pCurrent != NULL){
printf("%-5d",pCurrent->data);
pCurrent = pCurrent->next;
}
printf("\n");
}
#endif
// 创建一个链表,窗口键盘输入链表节点数据
struct ListNode* createLinkList(void){
struct ListNode* head = (struct ListNode*)malloc (sizeof(struct ListNode));
head->next = NULL;
//链表尾部指针
struct ListNode* pRear = head;
int val = -1;
printf("请输入链表新节点数据,结束输入码值为-1:");
while(1){
scanf("%d",&val);
if(val == -1){
break;
}
//创建新节点
struct ListNode* newNode = (struct ListNode*)malloc (sizeof(struct ListNode));
newNode->data = val;
newNode->next = NULL;
//新节点插入到链表中
pRear->next = newNode; //(1)head->next = newNode;(有什么区别?)
//更新尾部指针指向
pRear = newNode;
}
return head;
}
//删除倒数第n个节点
struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
struct ListNode* slow = head;
struct ListNode* fast = head;
int i;
for(i = 0; i < n ; i++){
fast =fast->next;
if(fast == NULL){
return head->next;
}
}
fast = fast->next;
while(fast){
fast = fast->next;
slow = slow->next;
}
struct ListNode* toRemove = slow->next;
slow->next = slow->next->next;
free(toRemove);//(2)
return head; //(3)return slow有什么区别?
}
int main(){
int n;
struct ListNode* testLinkList = createLinkList();
foreachLinkList(testLinkList);
printf("要删除的倒数节点为:");
scanf("%d",&n) ;
printf("删除链表倒数第%d个节点:\n",n);
removeNthFromEnd(testLinkList,n);
foreachLinkList(testLinkList);
return 0;
}
(1)当用户连续输入几个数据时,head->next = newNode;会丢弃之前指向的链表节点,重新指向最新的链表节点。
(2)释放删除节点的内存。
(3)返回head,始终返回链表的头结点地址,无论删除哪个节点,外部调用者始终可以从头开始遍历链表。而返回slow,当删除的节点时非头节点时,slow重新指向链表后面新的节点时,会丢失链表前面的数据。
输出结果:
五、说明
由于笔者水平有限,文章内容可能会存在错误,如果读者发现有误或者觉得表达不清楚的地方,请务必在评论区指出,互相学习,互相督促,互相成长,感谢!