代码随想录刷题-链表-删除链表的倒数第N个节点

删除链表的倒数第N个节点

本节对应代码随想录中:代码随想录,讲解视频:链表遍历学清楚! | LeetCode:19.删除链表倒数第N个节点_哔哩哔哩_bilibili

习题

题目链接:19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cNZhXKXu-1679135703683)(null)]

  • 输入:head = [1,2,3,4,5], n = 2;输出:[1,2,3,5]
  • 输入:head = [1], n = 1;输出:[]
  • 输入:head = [1,2], n = 1;输出:[1]

直观解法

这道题目还挺简单的,题意也很明确。需要注意的是最好使用虚拟头结点,否则示例2这种就需要单独处理会麻烦一点。比较容易想到的就是既然要删倒数第 n 个节点,那我们首先遍历以下链表看看总共有多少个节点,然后再根据总节点数量 size 和 n 的大小遍历到待删节点的前一个节点执行删除操作即可。

这里需要注意的就是遍历的边界条件是到了待删节点的前一个节点开始执行删除操作。当遍历到待删节点本身的时候是无法进行删除的,因为我们无法获得待删节点的前一个节点是什么。

class Solution {
   public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* tmp = head;
        int size = 0;   // 记录链表长度
        // 计算链表长度
        while (tmp != nullptr) {
            tmp = tmp->next;
            size++;
        }
        tmp = dummyHead;
        // 找到待删除节点的前一个节点
        for (int i = 0; i < size - n; i++) {
            tmp = tmp->next;
        }
        ListNode* delNode = tmp->next;
        tmp->next = tmp->next->next;
        delete delNode;
        return dummyHead->next;
    }
};
  • 时间复杂度:O(n)。其中n为链表的长度。因为算法需要遍历整个链表两次,时间复杂度与链表长度成正比。
  • 空间复杂度:O(1)。因为算法只使用了常数级别的额外空间,即三个指针变量和一个虚拟头节点。无论输入的链表有多长,算法所需的额外空间都是固定的,与链表长度无关。

可以优化的点:

  • 今天看官方题解突然发现 ListNode* dummyHead = new ListNode(0);dummyHead->next = head; 这两句其实可以用 ListNode* dummyHead = new ListNode(0, head); 这一句话来实现。因为 ListNode 的构造函数中有这样一句 ListNode(int x, ListNode *next) : val(x), next(next) {} 可以直接指定节点的 next,不过写成两句更加直观一点。

双指针

LeetCode 上题目描述中进阶中说“你能尝试使用一趟扫描实现吗?”。一开始没想出来,看了题目的提示:Maintain two pointers and update one with a delay of n steps.才明白。

例如0 1 2 3 4 5,我们可以用一个指针 cur 来遍历,用另一个指针 pre 慢 cur 指针 n+1 步遍历。比如 n=2时,当 cur=3时,pre=0(虚拟头结点),然后两个指针一起移动,当 cur 遍历到5后面的 nullptr 时,pre 刚好遍历到待删节点4的前一个节点3的位置。

如果是慢 n 步遍历,那么 pre 会遍历到待删节点4的位置,此时是无法执行删除节点4的操作的,因此是慢 n+1步。

class Solution {
   public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyNode = new ListNode(0);
        dummyNode->next = head;
        ListNode* cur = dummyNode;
        ListNode* pre = dummyNode;
        // cur先向前移动n+1步
        for (int i = 0; i < n + 1; i++) {
            cur = cur->next;
        }
        // 然后cur和pre一起向前移动
        while (cur != nullptr) {
            cur = cur->next;
            pre = pre->next;
        }
        ListNode* delNode = pre->next;
        pre->next = pre->next->next;
        delete delNode;
        return dummyNode->next;
    }
};
  • 时间复杂度:O(n)。其中n为链表的长度。因为算法需要遍历整个链表一次,时间复杂度与链表长度成正比。
  • 空间复杂度:O(1)。因为算法只使用了常数级别的额外空间,即三个指针变量和一个虚拟头节点。无论输入的链表有多长,算法所需的额外空间都是固定的,与链表长度无关。

但是写完之后又有点疑惑,这还算是一趟扫描吗,因为其中有 for 循环和 while 循环。仔细琢磨后发现确实算是一趟扫描。

直观解法的解法中,在第一趟扫描中,需要计算链表的长度,以便确定需要删除节点的前一个节点。而在第二趟扫描中,才真正开始删除节点。因此,这个解法需要进行两趟扫描。而双指针解法使用了两个指针来记录待删除节点的前一个节点,而不需要事先计算链表的长度。

举个极端点的例子,在1000个节点中,我想删除倒数第1个节点即节点1000,在第一个解法中,需要先遍历1000个节点得知链表长度为1000,然后再遍历一趟到节点999的位置执行删除操作。而在第二个解法中,只需要 cur 指针先向前走2步,然后 pre 指针和 cur 指针再开始一起走,最后 cur 指针走到节点1000的下一个即 nullptr 时,pre 刚好走到节点999的位置。在这个例子中,解法一走了1001+999=1999步,而解法二只走了1001+2=1003步!

为什么是1001而不是1000?因为从1000走到下一步 nullptr 还需要一步

如果你还是有点不太理解,看看下面的代码,它没有使用 for 循环而是使用了 size 进行计数,其实和上面的解法是一个意思。

class Solution {
   public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* cur = dummyHead;
        ListNode* pre = dummyHead;
        int size = 0;
        while (cur != nullptr) {
            // size > n说明慢指针可以开始移动了
            if (size > n)
                // 循环结束后pre指向待删除节点的前一个节点
                pre = pre->next;
            else
                size++;
            cur = cur->next;
        }
        ListNode* delNode = pre->next;
        pre->next = pre->next->next;
        delete delNode;
        return dummyHead->next;
    }
};
  • 时间复杂度:O(n)。其中n为链表的长度。因为算法需要遍历整个链表一次,时间复杂度与链表长度成正比。
  • 空间复杂度:O(1)。因为算法只使用了常数级别的额外空间,即三个指针变量和一个虚拟头节点。无论输入的链表有多长,算法所需的额外空间都是固定的,与链表长度无关。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值