力扣-19. 删除链表的倒数第 N 个结点

1.题目介绍

给你一个链表,删除链表的倒数第 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]

示例 4:
输入:head = [1,2], n = 2
输出:[2]

2.题解(快慢指针)

2.1 初始代码

思路

这里的思路很简单,使用快慢指针的思想。这里注意,我要删除倒数第n个节点,应该要找到它的前一个节点才能顺利删除!
这里经过观察可以发现,当快指针快于慢指针n个节点时,当快指针指向结尾节点,慢指针指向的正是我们所要删除目标节点的前一个节点(涉及简单的种树问题)
所以我们正要使快指针先跑n个节点,再使快慢指针一起跑,最后进行删除操作即可。

但是这里又有一个问题,快指针起始指向的head指针指向第一个节点,当我们要删除第一个节点的时候,它向后跑n个节点直接跑到了nullptr,
而我们这里的判断 while (fast->next != nullptr) 就会发生空指针指向的报错!
在这里我最开始是将其单独列出讨论的,但是后面发现了更简单的写法(虚拟头结点),见2.2

代码一

//
// Created by trmbh on 2023-10-27.
//19. 删除链表的倒数第 N 个结点

#include<iostream>

struct ListNode {
    int val;
    ListNode *next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode *next) : val(x), next(next) {}
};

class Solution {
public:
    ListNode *removeNthFromEnd(ListNode *head, int n) {
        ListNode *fast = head, *slow = head, *temp = nullptr;
        if (head == nullptr) return head;

        // fast领先slow指针n个节点,最终使slow定位到待删除节点的(前一个节点,便于删除节点)
        for (int i = 0; i < n; i++) {
            if (fast -> next == nullptr) //说明删除的是第一个节点
            {
                temp = head;
                head = head -> next;
                delete temp;
                return head;
            }
            fast = fast->next;
        }

        // 快慢指针开始移动
        while (fast->next != nullptr){
            fast = fast -> next;
            slow = slow -> next;
        }

        // 节点删除操作
        temp = slow->next;
        slow -> next = temp -> next;
        delete temp;
        return head;
    }
};

int main(){
    ListNode node(2);
    ListNode head(1, &node);
    int n = 2;
    Solution solution;
    ListNode* p = &head;
    p = solution.removeNthFromEnd(p, n);
    while (p != nullptr){
        std::cout << p->val << ' ';
        p = p->next;
    }
}

2.2 代码优化

前面说了的问题,我想要快指针快于慢指针n个节点。但实际操作时,当我们要删除第一个节点的时候,会发生空指针指向的问题。
当然我们可以设置一个pre指针,始终指向慢指针的前一个指针,然后快指针快于慢指针n-1个节点(最后指向删除节点),这样最后也能实现目标,但要同时保管三个指针,且pre指针有很多重复的赋值操作。

关于为什么要使用虚拟头结点?无论head指向的节点有没有被删除,虚拟头结点都可以指向链表的头结点; 且虚拟头结点提供了一个支点,供我们设计pre指针,与cur拉开一个距离身位。
我们再思考一下,这里我只需要快指针快于慢指针n个节点,并没有要求二者从哪开始运动,我这里如果再添加一个虚拟头结点作为二者的起点,那么即使是最极端情况删除第一个节点的时候,最后快指针也只会指向尾结点,而不会指向nullptr!

class Solution {
public:
    ListNode *removeNthFromEnd(ListNode *head, int n) {
        ListNode *dummy = new ListNode(0,head); // 使用虚拟头节点,避免特殊处理
        ListNode *fast = dummy, *slow = dummy;

        // 将 fast 领先 slow 指针 n 个节点,是的 slow指针指向待删除节点的前一个节点
        for (int i = 0; i < n; i++) {
            fast = fast->next;
        }

        while (fast->next != nullptr) {
            fast = fast->next;
            slow = slow->next;
        }

        ListNode *temp = slow->next;
        slow->next = temp->next;
        delete temp;

        head = dummy->next;
        delete dummy;
        return head; // 返回链表的头节点
    }
};

2.3 使用栈保存信息

思路

力扣题解还提供了一种比较有意思的做法,在遍历链表的同时将所有节点依次入栈。
根据栈「先进后出」的原则,我们依次弹出栈的节点,弹出的第 n 个节点就是需要删除的节点,并且目前栈顶的节点就是待删除节点的前驱节点。
这样一来,删除操作就变得十分方便了。这里由于依然要考虑删除头结点的情况,所以这里也要设置虚拟头结点dummy.

代码

class Solution {
public:
    ListNode *removeNthFromEnd(ListNode *head, int n) {
        stack<ListNode*> stk;
        ListNode *dummy = new ListNode(0,head), *p = dummy, *temp = nullptr;
        while (p != nullptr){
            stk.push(p);
            p = p->next;
        }
        for (int i = 0; i < n - 1; i++) stk.pop();
        temp = stk.top();
        stk.pop();
        p = stk.top();
        p -> next = temp -> next;
        delete temp;
        head = dummy->next;
        return head;
    }
};
posted @ 2023-10-27 16:19  DawnTraveler  阅读(44)  评论(0编辑  收藏  举报