3、(链表)19. 删除链表的倒数第 N 个结点

本文详细解析了如何使用C语言解决LeetCode中的链表问题,包括快慢指针技巧、for循环、scanf输入处理、链表操作以及边界条件的考虑。
摘要由CSDN通过智能技术生成

今天来刷 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重新指向链表后面新的节点时,会丢失链表前面的数据。
输出结果:
请添加图片描述

五、说明

  由于笔者水平有限,文章内容可能会存在错误,如果读者发现有误或者觉得表达不清楚的地方,请务必在评论区指出,互相学习,互相督促,互相成长,感谢!

  • 16
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林时小卡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值