LeetCode | 19. 删除链表的倒数第 N 个结点

发布时间 2023-10-23 21:21:21作者: Guanz

1 相关标签

链表、双指针、C 语言

2 报错情况

2.1 题目

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

2.2 错误代码

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


struct ListNode* removeNthFromEnd(struct ListNode* head, int n){
    int distance = 0;   // 计数器
    struct ListNode *end = head, *del = head, *beforedel = head;

    // 定位删除的节点
    while(end){
        if(distance < n){
            ++distance;
        }
        else{
            beforedel = del;    // 定位删除节点前的节点
            del = del->next;
        }
        end = end->next;
    }

    // 删除节点
    beforedel->next = beforedel->next->next;
    free(del);

    return head;    
}

2.3 测试用例

[1]
1

2.4 报错内容

runtime error: member access within null pointer of type 'struct ListNode' [solution.c]

在struct ListNode类型的空指针中访问成员

3 Debug思路

3.1 排查出错原因

运行原有示例中的其他两组

[1,2,3,4,5]
2
[1,2]
1

均输出正确结果。

[1,2,3,5]
[1]

猜测是删除了第1个节点引发的报错,更改示例如下

[1,2,3,4,5]
5
[1,2]
2

运行后出现新的报错

=================================================================
==43==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000030 at pc 0x55fd91f42904 bp 0x7ffe184c15b0 sp 0x7ffe184c15a0
READ of size 4 at 0x602000000030 thread T0

#2 0x7f9486376082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082)

0x602000000030 is located 0 bytes inside of 16-byte region [0x602000000030,0x602000000040)
freed by thread T0 here:
#0 0x7f9486fbe40f in __interceptor_free ../../../../src/libsanitizer/asan/asan_malloc_linux.cc:122
#2 0x7f9486376082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082)
previously allocated by thread T0 here:
#0 0x7f9486fbea06 in __interceptor_calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cc:153
#4 0x7f9486376082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082)
Shadow bytes around the buggy address:
0x0c047fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c047fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c047fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c047fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c047fff8000: fa fa fd fd fa fa[fd]fd fa fa 00 00 fa fa 00 00
0x0c047fff8010: fa fa 00 00 fa fa 00 00 fa fa 04 fa fa fa fa fa
0x0c047fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==43==ABORTING

注释掉 free(del); 后再运行,输出错误结果

[1,3,4,5]

预期结果:[2,3,4,5]

[1]

预期结果:[2]

显然猜测正确

参考力扣官方给出的题解,前言中提到“头节点不存在前驱节点,需要在删除头节点时进行特殊判断”,这类题目通常做法是在头节点前“添加一个哑节点(dummy node)”,再参考力扣的视频题解得知,该题给出的链表不设置空的头节点,链表的头节点中直接存放第一个元素
链表结构
而错误代码编写时将链表的头节点作为一个空节点计算,代码在删除链表倒数第 n 个节点时删除的是第 2 个节点(如上图存放 2 的节点),而执行 free(del) 后又将第一个节点(head)的内存释放掉,使得返回的头节点为已被释放掉的内存,导致报错

3.2 寻求解决方法

由于头节点不存在前驱节点,仅存在指向头节点的指针,因此需要对删除头节点的情况进行特殊判断

// 删除节点
if(beforedel == del){
    head = del -> next; // del -> next 或 beforedel -> next 均可
}
else{
    beforedel -> next = beforedel -> next -> next;
}
free(del);

输出正确结果
输出
开销
开销

4 最终结果

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


struct ListNode* removeNthFromEnd(struct ListNode* head, int n){
    int distance = 0;   // 计数器
    struct ListNode *end = head, *del = head, *beforedel = head;

    // 定位删除的节点
    while(end){
        if(distance < n){
            ++distance;
        }
        else{
            beforedel = del;    // 定位删除节点前的节点
            del = del->next;
        }
        end = end->next;
    }

    // 删除节点
    if(beforedel == del){
        head = del -> next; // del -> next 或 beforedel -> next 均可
    }
    else{
        beforedel -> next = beforedel -> next -> next;
    }
    free(del);

    return head;    
}

5 总结

删除链表节点问题需要先确定给出链表的头节点是否存放了数据。对于头节点中存在数据的链表,需要另外考虑删除头节点的情况

官方题解采用添加哑节点的方法能让过程更简洁一些,在内存消耗方面也更占优势
官方题解开销